tsJensen

A quest for software excellence...

C# Basics: const vs readonly vs static

In this 7th installment of C# Basics, I’m going to cover the differences between the modifiers const, readonly, and static, specifically in reference to class members where the developer wishes to use the fields or properties in client code that cannot change the value of that member.

To illustrate and discuss the differences between each modifier, I’ve put together a contrived set of classes with the least amount of code I can think to add in order to review and to examine the differences in the IL (intermediate language) taken from Ildasm.exe. (Visit this nice page to learn how to use Ildasm.exe from with Visual Studio.)

using System;

namespace ReadOnlyConstant
{
  public class MyConstOnly
  {
    // assigned at declaration and used at compile time only
    public const int Age = 20;
  }

  public class MyReadOnly
  {
    // can only be assigned in declaration or constructor
    public readonly int Age = 0;
    public MyReadOnly()
    {
      Age = 20;
    }
  }

  public static class MyStaticOnly
  {
    // can be assigned within class but "readonly" for client
    private static int _age = 20;
    public static int Age { get { return _age; } }
  }
}

The goal in each class above is to create a public field or member that can be read by client code but cannot be changed by client code. Looking through the IL produced by compiling this code can also be instructive even if you do not fully understand each and every IL instruction. Take a look here at these classes under the compiled covers and notice that the MyConstOnly class does not have a getter method to retrieve Age nor is it's value set in the .ctor but only noted by the compiler in the .field definition for use by the compiler later should client code use it. Then read through to the client code and see its IL code as well.

// =============== CLASS MEMBERS DECLARATION ===================

.class public auto ansi beforefieldinit ReadOnlyConstant.MyConstOnly
       extends [mscorlib]System.Object
{
  .field public static literal int32 Age = int32(0x00000014)
  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method MyConstOnly::.ctor

} // end of class ReadOnlyConstant.MyConstOnly

.class public auto ansi beforefieldinit ReadOnlyConstant.MyReadOnly
       extends [mscorlib]System.Object
{
  .field public initonly int32 Age
  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       25 (0x19)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ldc.i4.0
    IL_0002:  stfld      int32 ReadOnlyConstant.MyReadOnly::Age
    IL_0007:  ldarg.0
    IL_0008:  call       instance void [mscorlib]System.Object::.ctor()
    IL_000d:  nop
    IL_000e:  nop
    IL_000f:  ldarg.0
    IL_0010:  ldc.i4.s   20
    IL_0012:  stfld      int32 ReadOnlyConstant.MyReadOnly::Age
    IL_0017:  nop
    IL_0018:  ret
  } // end of method MyReadOnly::.ctor

} // end of class ReadOnlyConstant.MyReadOnly

.class public abstract auto ansi sealed beforefieldinit ReadOnlyConstant.MyStaticOnly
       extends [mscorlib]System.Object
{
  .field private static int32 _age
  .method public hidebysig specialname static 
          int32  get_Age() cil managed
  {
    // Code size       11 (0xb)
    .maxstack  1
    .locals init ([0] int32 CS$1$0000)
    IL_0000:  nop
    IL_0001:  ldsfld     int32 ReadOnlyConstant.MyStaticOnly::_age
    IL_0006:  stloc.0
    IL_0007:  br.s       IL_0009

    IL_0009:  ldloc.0
    IL_000a:  ret
  } // end of method MyStaticOnly::get_Age

  .method private hidebysig specialname rtspecialname static 
          void  .cctor() cil managed
  {
    // Code size       8 (0x8)
    .maxstack  8
    IL_0000:  ldc.i4.s   20
    IL_0002:  stsfld     int32 ReadOnlyConstant.MyStaticOnly::_age
    IL_0007:  ret
  } // end of method MyStaticOnly::.cctor

  .property int32 Age()
  {
    .get int32 ReadOnlyConstant.MyStaticOnly::get_Age()
  } // end of property MyStaticOnly::Age
} // end of class ReadOnlyConstant.MyStaticOnly

// =============================================================

You can read the MSDN full explanations of each modifier but here’s the basics:

const
Can only be assigned a value in declaration and can only be a value type or string. Use the const modifier when you KNOW the value won’t change. If you think it might change at a later date and your assembly is distributed as a compiled library, consider one of the other modifiers to assure that you don’t have a value you didn’t expect in your client code. (See use code sample below.)

readonly
Can assign a value at declaration or in the class constructor. It is important to note that if you use a reference type with modifiable members, your client code can still modify those members even if it cannot assign a value to the readonly reference. Note in the IL above that the initialization of the declared value occurs in the .ctor before the assignment in the .ctor, so if you are wondering which would be better, now you have some insight into that question.

static
Can assign the value of the private member anywhere within the class code. Note the initialization of the value in the static .ctor of the class. You could also assign the value in some other method later but with the public property implementing only a get, the client code cannot assign a value.

And here is the client code and it’s IL just below it. The most important point to note in the IL is that the client code is compiled with the const’s literal value, NOT a get to the class. This is why you must watch for the use of a const that could change with a new library. Make sure you compile your client code against that new library when you get it or you could be very sorry when the library is using one const compiled value and you’re using another.

namespace TestConsole
{
  class Program
  {
    static void Main(string[] args)
    {
      // compiler will replace with constant value
      // If the referenced assembly is changed to 40 and this is
      // not compiled again against that new assembly, the value
      // for mcoAge will still be 20. (See IL below.)
      int mcoAge = MyConstOnly.Age;

      MyReadOnly mro = new MyReadOnly();
      int mroAge = mro.Age;

      int msoAge = MyStaticOnly.Age;

      Console.WriteLine("{0} {1} {2}", mcoAge, mroAge, msoAge);
    }
  }
}

// output: 20 20 20

// and here is the IL with some of my own comments

// =============== CLASS MEMBERS DECLARATION ===================

.class private auto ansi beforefieldinit TestConsole.Program
       extends [mscorlib]System.Object
{
  .method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       53 (0x35)
    .maxstack  4
    .locals init ([0] int32 mcoAge,
             [1] class [ReadOnlyConstant]ReadOnlyConstant.MyReadOnly mro,
             [2] int32 mroAge,
             [3] int32 msoAge)
    IL_0000:  nop
    IL_0001:  ldc.i4.s   20   //NOTE: literal value assigned - no mention of MyConstOnly class
    IL_0003:  stloc.0
    IL_0004:  newobj     instance void [ReadOnlyConstant]ReadOnlyConstant.MyReadOnly::.ctor()
    IL_0009:  stloc.1
    IL_000a:  ldloc.1
    IL_000b:  ldfld      int32 [ReadOnlyConstant]ReadOnlyConstant.MyReadOnly::Age
    IL_0010:  stloc.2
    IL_0011:  call       int32 [ReadOnlyConstant]ReadOnlyConstant.MyStaticOnly::get_Age()
    IL_0016:  stloc.3
    IL_0017:  ldstr      "{0} {1} {2}"
    IL_001c:  ldloc.0
    IL_001d:  box        [mscorlib]System.Int32
    IL_0022:  ldloc.2
    IL_0023:  box        [mscorlib]System.Int32
    IL_0028:  ldloc.3
    IL_0029:  box        [mscorlib]System.Int32
    IL_002e:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object,
                                                                  object,
                                                                  object)
    IL_0033:  nop
    IL_0034:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method Program::.ctor

} // end of class TestConsole.Program

// =============================================================

Meaningless Meetings Meander Morosely Mostly

Recently I came across an interesting list of top ten time wasters at work. I jotted them down but have lost the source, so I apologize for not providing the link. Here’s their list:

  1. Instant Messaging
  2. Over-Reliance on Email
  3. Meandering Meetings
  4. Short Gaps Between Meetings
  5. Reacting to Interruptions
  6. Ineffective Multi-Tasking
  7. Disorganized Workspace
  8. Personal Communication
  9. Web Surfing "Breaks"
  10. Cigarette/Coffee Breaks

It’s a decent list that one might find on many top ten sites, I’m sure. But it’s not terribly accurate. You see, the biggest time waster in the enterprise is the Meaningless, Meandering Meeting Machine. So here’s my revised list:

  1. Thinking about meetings.
  2. Planning meetings.
  3. Doodling in meetings.
  4. Talking about meetings.
  5. Scheduling and rescheduling meetings.
  6. Meetings.
  7. Follow-up meetings.
  8. Looking or asking for minutes from meetings.
  9. Assuming people will do what was agreed to in meetings.
  10. Making lists about meetings.

I have seen good, productive employees become consumed by the Meaningless Meetings Machine. It is an endless recursive function leading to enterprise stack overflow. Sometimes executives are lost for months in the Machine and when employees smart enough or lucky enough to stay away from the Machine are asked if they have seen Mr. Soandso, they say, No, and hurriedly move on lest they too be sucked into the Machine.

Seek to Establish a Corporate Culture of Honesty

In April of this year, an executive of another company told me in a telephone conversation, "Everybody lies. You just have to get used to it."

I was stunned by the statement and have given it a lot of thought since then. It reminded me of a scene from Babylon 5 that went something like this:

Garibaldi: Everybody lies.
Edgars: That's a very sad view of the universe, Mr. Garibaldi.
Garibaldi: Yeah, well, it's the only one I got. And it works for me.
Edgars: The truth will be revealed in a couple of days. How many people can say that?
Garibaldi: I don't know. But I think the last guy got thirty pieces of silver for the same job.

Dishonesty in this technological age has even produced its own vocabulary, specifically the verb "to blag." The third definition of "blag" in the Urban Dictionary:

To convince another person that all the stuff you just made up is in fact true and worthy. Example: "Caught in a tight spot, Harry blagged his way through the conversation and somehow got the job."

I do not wish to accept the dim view that everybody lies. And I certainly do not wish to get used to it, in any way shape or form. And yet there is growing evidence both anecdotally in my own professional career and globally that the enterprise often creates a dishonest culture. Author Brian Amble in Management Issues filed a piece in 2005 entitled Corporate Culture Encourages Lying. The author mentions "blagging" and makes these three very interesting points:

"The rot starts at the top, revealing a surprisingly ambivalent attitude amongst...bosses towards the honesty – or otherwise - of their staff."

"The vast majority of company directors and senior managers believe it is wrong for their employees to lie to them. But almost half are comfortable with those same employees telling untruths on their behalf to their customers – with female bosses even more tolerant of this sort of behavior than their male colleagues."

"[Microsoft] encourages decision-making [using a] technology-based process that creates permanent digital records and maintains the integrity of the information on which those decisions are based."

In my experience blagging is most especially prevalent in the world of software development where management clings to the notion, and consultants sell the latest process magic to reaffirm the assumption, that building software is like building a house, sufficiently predictable and repeatable so as to accurately establish a firm budget, timeline and resource commitment before ground is broken. Software artisans then find themselves in the very uncomfortable position of feeling pressured to lie to their patron bosses who clearly do not wish to know that building software is often as much an unpredictable art as it is in any way a predictable science.

Software craftsmen who boldly stand their ground and declare, "I don't know how long it will take to build that," are more often attacked and labeled malcontent and misfit, shunned by managers who insist that they can bend reality to their will while they ignore what their experts have told them. And lower management who know better are caught in the ultimate catch 22. They may believe their lead developers or software architects when they say, "we cannot deliver all these features on that date," but they cannot report that fact to their own managers or customers, so they choose to lie in order to protect and even advance their own career knowing that if the implementation team fails, they may avoid accountability by throwing the developers under the bus, asserting the implementation team is incompetent and ought to be replaced.

Given the impossible circumstances in which software craftsmen find themselves, they often resort to lying to themselves and their bosses asserting that they can produce some piece of software, despite the general lack of specific requirements and understanding of the problem domain or the target users, within the time budget and resource constraints imposed by management. And then in order to avoid being exposed as just another blagger, the software artisan (or collectively the team) works impossible hours, makes imprudent quality compromises and desperately seeks for external circumstances which can be blamed for delays. Knowing that such responsibility diverting opportunities always arise, the software artisan begins to accept and even embrace the culture of lies produced by her circumstances until she does not even recognize the truth.

I know that this scenario is not universal. There are organizations who take great steps, incorporating technology and constant vigilance, to avoid the traps of dishonesty. These organizations are led by honest people who genuinely listen to their people and avoid the trap of boxing their software craftsmen into a set of assumptions that are untenable at best and downright foolish at worst. Dr. Rhonne Sanderson said it best in an article by Marcia A. Reed-Woodard entitled Don't lie to me: dishonesty can ruin professional and personal relationships:

"Although lying provides an easy out in the short-term, it comes with serious repercussions," says Dr Rhonne Sanderson, a Dallas--Fort Worth area licensed psychotherapist. He maintains that the fallout from lying can hurt others, ruin relationships, as well as rob the liar of integrity, credibility, confidence, and self-esteem. "Lying only exacerbates the real problem."

If we let honesty govern our dealings with our fellow man, we will all be the better for it. We do not need to sacrifice civility for honesty, nor should we mistake an honest disagreement for disrespect or insubordination, for when we do, we encourage others to be dishonest through their silence.

We can boldly speak the truth and expect others to do so. We can hold ourselves and others to account. We need not settle for the notion that everybody lies. And we certainly do not need to get used to it. We can and ought to do and be better.

C# Thread Safe Class Essentials

In this post I'm recording my own code sample for future reference. It is not meant to be perfectly usable or even a representation of best practices but a practical reminder note of the most common thread safe code I end up writing. The code is a thread safe singleton class that demonstrates the use of the lock keyword, the Interlocked class, ReadWriterLockSlim class, and one of the new concurrent collections available in .NET 4.0.

In the code below, I've only shown the ConcurrentDictionary being used. There is a gold mine of power in the other concurrent types to be explored in other posts.

using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Threading;

namespace SafeThreads
{
  public sealed class DataCache
  {
    // Thread Safe Singleton Pattern
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++
    
    //volatile to insure assignment before read access
    private static volatile DataCache _instance;
    
    //the sync root object - clever name
    private static object _threadBolt = new object();

    private DataCache() { } //cannot create outside this

    public static DataCache Instance
    {
      get
      {
        if (null == _instance)
        {
          lock (_threadBolt)
          {
            if (null == _instance)
            {
              _instance = new DataCache();
            }
          }
        }
        return _instance;
      }
    }
    

    // Thread Safe with lock
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++

    private long _callCount = 0;
    private long _updateCount = 0;

    public long UpdateCountWithLock(long callCount)
    {
      lock (_threadBolt)
      {
        _callCount += callCount;
        _updateCount++;
        return _updateCount;
      }
    }

    // Thread Safe Interlocked class
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++

    public long UpdateCountWithInterlocked(long callCount)
    {
      Interlocked.Add(ref _callCount, callCount);
      Interlocked.Increment(ref _updateCount);
      return _updateCount;
    }

    private Tuple<int, string> _currentItem = 
      new Tuple<int, string>(0, string.Empty);

    public Tuple<int, string> CurrentItemInterlocked
    {
      get { return _currentItem; }
      set
      {
        Interlocked.Exchange<Tuple<int, string>>(ref _currentItem, value);
      }
    }


    // Thread Safe ReaderWriterLockSlim
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++

    private ReaderWriterLockSlim _slimLock = new ReaderWriterLockSlim();
    private Dictionary<string, int> _keys = new Dictionary<string, int>();
    private List<string> _values = new List<string>();

    public int Count
    {
      get
      {
        _slimLock.EnterReadLock();
        try
        {
          return _values.Count;
        }
        finally
        {
          _slimLock.ExitReadLock();
        }
      }
    }

    public string this[int index]
    {
      get
      {
        _slimLock.EnterReadLock();
        try
        {
          if (_values.Count > index && index > -1)
            return _values[index];
          else
            return null;
        }
        finally
        {
          _slimLock.ExitReadLock();
        }
      }
    }

    public string this[string key]
    {
      get
      {
        _slimLock.EnterReadLock();
        try
        {
          if (_keys.ContainsKey(key))
          {
            return _values[_keys[key]];
          }
          else
            return null;
        }
        finally
        {
          _slimLock.ExitReadLock();
        }
      }

      set
      {
        _slimLock.EnterWriteLock();
        try
        {
          if (_keys.ContainsKey(key))
          {
            _values[_keys[key]] = value;
          }
          else
          {
            _values.Add(value);
            _keys.Add(value, _values.Count - 1);
          }
        }
        finally
        {
          _slimLock.ExitWriteLock();
        }
      }
    }

    //demonstrate upgrade from read to write lock
    public AddUpdateResult AddOrUpdate(string key, string value)
    {
      AddUpdateResult result = AddUpdateResult.Unchanged;
      _slimLock.EnterUpgradeableReadLock();
      try
      {
        if (_keys.ContainsKey(key))
        {
          if (value == _values[_keys[key]])
            return result;
          else
            result = AddUpdateResult.Updated;
        }
        else
          result = AddUpdateResult.Added;

        //upgrade to write lock
        _slimLock.EnterWriteLock();
        try
        {
          if (result == AddUpdateResult.Updated)
          {
            _values[_keys[key]] = value;
          }
          else
          {
            _values.Add(value);
            _keys.Add(value, _values.Count - 1);
          }
        }
        finally
        {
          _slimLock.ExitWriteLock();
        }
        return result;
      }
      finally
      {
        _slimLock.ExitUpgradeableReadLock();
      }
    }

    public enum AddUpdateResult
    {
      Added,
      Updated,
      Unchanged
    };


    // Thread Safe Collections
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++

    private ConcurrentDictionary<string, string> _concurrentCache = 
      new ConcurrentDictionary<string, string>();

    public ConcurrentDictionary<string, string> ConcurrentCache
    {
      get { return _concurrentCache; }
    }
  }
}

DomainAspects: Aspect Oriented Library Update - Configuration by Code and Convention

A year ago I posted a library that I enjoy using in one form or another called DomainAspects, an aspect oriented infrastructure or template for domain driven design in .NET applications. The library allows you to isolate cross cutting concerns such as exception logging and authorization from domain or business specific concerns. I promised to write more about the library but I got distracted with work and other topics.

In this post, I will introduce a new version of the library in which I mark the domain classes internal in order to isolate the implementation of the domain interface from the outside world making it impossible to create an instance of the domain implemenation directly. This change helps to enforce the use of the DomainAspects proxy wrapper to create and dispose of the domain object.

internal class ClassroomDomain : IClassroomDomain
{
  public List<string> GetStudents()
  {
    return new List<string>();
  }
}

I also wanted to explore the idea of getting away from XML configuration in part because Castle Project shuns it while supporting it for backward compatibility and in part because managing runtime configuration files can be problematic and convention is generally easier to support. Here's the aspect oriented service components being registered in the DomainFactory's static constructor:

container = new WindsorContainer(); //no more XML

container.Register(
  Component.For<IPrincipalProvider>()
  .ImplementedBy<PrincipalProvider>()
  .Named("PrincipalProvider")
  .LifeStyle.Transient,
  
  Component.For<IOperationAuthorizer>()
  .ImplementedBy<OperationAuthorizer>()
  .Named("OperationAuthorizer")
  .LifeStyle.Transient,
  
  Component.For<IOperationAuditor>()
  .ImplementedBy<OperationAuditor>()
  .Named("OperationAuditor")
  .LifeStyle.Transient,
  
  Component.For<OperationInterceptor>()
  .Named("OperationInterceptor")
  .LifeStyle.Transient);

Now, the DomainFactory's Create method uses the convention that the implementation has the same name without the "I" prefix on the interface name.

/// <summary>
/// Use this factory method to create a transient (one time use) instance.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T Create<T>() where T : class
{
  var svc = container.GetService<T>();
  if (null == svc)
  {
    //resolution of type by convention MyClass implements IMyClass
    Type interfaceType = typeof(T);
    var implementedByName = interfaceType.AssemblyQualifiedName
      .Replace("." + interfaceType.Name + ",",
					"." + interfaceType.Name.Substring(1) + ",");
    Type implementedByType = Type.GetType(implementedByName);
    if (null == implementedByType) 
      throw new TypeAccessException(string.Format("Type {0} could not be found.", implementedByName));

    container.Register(Component.For(interfaceType)
      .ImplementedBy(implementedByType)
      .Named(implementedByType.Name)
      .Interceptors(InterceptorReference.ForKey("OperationInterceptor")).Anywhere
      .LifeStyle.Transient);
  }
  if (!isConfigured) throw initializationException;
  return container.Resolve<T>();
}

Now I remove the app.config from my projects and run my tests XML-config-free. There is a lot more fun stuff to explore in this library, so check back soon for more info on DomainAspects.

You can download the new code here: DomainAspects_v1_1.zip (30.25 KB).

Fear Corrupts Process and Assures Failure

A certain angel investor chose three companies in which to invest a relatively large sum. In the first, he invested $10 million. In the second, he invested $5 million. And in the third, he invested $1 million. A year later, the first had doubled in value because it had boldly used the capital to execute on its business plan. The second company had also doubled in size because they had taken the investment and worked very hard to grow their business. The third company had run into some problems and was afraid of losing their capital, so they did nothing and had nothing to show for it. The angel investor liquidated his interest in the third company and those people are now looking for work.

Yes, I’ve borrowed the plot for that story from someone much wiser than me. But it applies in today’s world of technology and software development as much as it applies to the context for which it was originally told. Fear paralyzes us into inaction or into making unwise choices which almost always result in the corruption of otherwise sound business processes and just as often leads to complete and utter failure.

One of the greatest fears in software development that corrupts process and assures failure is the fear to tell the client or customer no. In the world of software development, especially in internal enterprise software development, the fear of saying no can be so paralyzing that we often place impossible burdens on teams already pushed to their limits. The fear of saying no can lead us to short circuit our process, skip critical steps and gloss over real analysis and careful design. The fear of saying no can lead to general discord when a team member objects and the manager angrily brushes her off, dismissing her concerns as irrelevant or inconsequential. This fear leads to chaos and assured failure. Failure to deliver on time. Failure to communicate and manage realistic client expectations. Failure to establish credibility and confidence with the client and the organization. Failure to keep and foster a well organized, happy and productive team.

So how can one overcome this fear? You just jump. How does the diver overcome his fear of heights to jump from a platform so high the first time? He just jumps. How does a skydiver overcome her fear of falling to her death from an airplane? She just leaps or is pushed. And then training and preparation take over and fear is overcome. You may never completely overcome the fear of saying no. But if you jump, take a leap of faith, just do it, you’ll find that you have not died and that you will have an easier time doing it the next time.

When we overcome our fear and take a leap of faith, we can accomplish great things. Let us work like the first two companies in our story and succeed.

Software Architecture: Use Cases, Not Frameworks

I enjoy continually learning from Robert Martin (Uncle Bob). Sometimes I disagree with him but usually only until I’ve thoroughly processed what he is trying to teach me. In his latest missive on 8thlight.com, Uncle Bob writes a stunningly cogent brief called Screaming Architecture in which he makes a critical point: software architecture is all about the use cases of a system and NOT the framework.

“Architectures are not (or should not) be about frameworks. Architectures should not be supplied by frameworks. Frameworks are tools to be used, not architectures to be conformed to. If your architecture is based on frameworks, then it cannot be based on your use cases.”

Focus on the use cases and perhaps even group them into logical hierarchies with tidy “40,000 foot level” epochs that describe the thing you are building. Is it a customer relationship management system? The first thing we should notice in an architecture, whether document or code, is that this is a CRM system. Presentation, delivery, storage and communication frameworks are all secondary, just plumbing, not the architecture.

“A good software architecture allows decisions about frameworks, databases, web-servers, and other environmental issues and tools, to be deferred and delayed.”

I’m not completely at this point in my journey toward architectural perfection but I recognize the need to strive for this idea of producing architectures that are designed to solve the problems of the users first rather than designed fit within the strictures of a framework while addressing the use cases after the fact.

I recommend you read the full article. Take Uncle Bob’s advice, as I intend to do, and focus more strongly on architecture for your use cases on your next project. Use frameworks carefully and work to keep your business code independent of your frameworks and thus imminently more testable. And don’t forget your tests.