Friday, January 18, 2008

Prefer Loose Coupling

This is the 3rd post in the Design Principles to Live By series.

Coupling is the degree to which software components depend on each other.  Tight coupling is a high degree of dependence.  In practical terms, it means one component needs intimate knowledge of how another works in order to successfully interact with it.  As much as possible, tight coupling should be avoided.  Tight coupling has many negative consequences.  It can make testing difficult, increase complexity and limit re-use.  Code which is loosely coupled is code which does not need to understand the details of the other components in the system.  Interaction should be through well-defined interfaces and semantics.

When one component is heavily reliant upon another, it cannot be tested except in combination with the other.  Testing become a much more complex task.  Not only does this make life difficult for the test team, but it deters the use of unit tests.  Tight coupling can also reduce the testable surface.  If ComponentA explicitly creates and then utilizes ComponentB and may even rely on side effects of ComponentB, that seam cannot be tested.  ComponentB cannot be replaced which means any interaction with it cannot be tested.  Less testing means less confidence in the product and ultimately a resistance to change later.

Each component that is tightly coupled to another component increases the complexity of the system.  To understand the system requires understanding the details of each coupled component.  Increased complexity in turn increases initial development time, difficulty of change, and thus the resistance to change.  Complex units are harder to understand, design, and build.  Loosely coupled components can be developed and changed independently of each other.  Tightly coupled components musts be created in a coordinated fashion.  This implies a delay to one delays the other and a change to one changes the others.  Change is similarly a cascaded endeavor.  Changing ComponentB requires also changing ComponentA.  Anyone relying on ComponentA may also need to be changed.  What should be a simple fix turns into a long series of changes and trust in the system decays.  Because testing cannot be done in isolation, testing the fix also becomes harder.  Because of this, fixes that might be useful are rejected as too costly.

Finally, tightly coupled components are hard to re-use.  Using one part requires using many others.  The bigger the coupled system, the less likely it is to fit the need.  Large systems have a greater likelihood of requiring unacceptable dependencies or use models.  It is hard to understand the side-effects of utilizing a large, complex code based.  A small, independent component is easier to trust.

Tight coupling is not just a lack of interfaces.  It is requiring an understanding of the semantics of the interface.  It is possible to have a tightly coupled set of components which all have well-defined interfaces.  DirectX Media Objects (DMOs) were envisioned to be small, independent pieces of the DirectShow media pipeline.  Unfortunately, each DMO had its own initialization interfaces.  There was no standard way to set up the objects.  Once set up, the data could flow in an abstracted manner, but setting up a pipeline required knowing what each DMO was so it could be configured properly.  This is a tightly coupled system.  Each application had to know specifically which DMOs it was using and couldn't use others without first being programmed to understand them. 

Another example was a video player application we were working on at one point.  It was supposed to have an abstracted pipeline to playback could be done via MediaFoundation or DirectShow or other pipelines.  One proposed design utilized PropVariants heavily in its interfaces.  Not only are they onerous to use from C++ but they make for terrible interfaces.  They look flexible, but in reality cause tight coupling.  Each user of the interface has to understand what values will be put into those PropVariants. 

Consider this rule of thumb.  If you ever see code like this:

switch(type) {

typeA: do something;

typeB: do somethingElse;

default: error;

}

Stop!  Your code is too tightly coupled.  This isn't the only tight coupling pattern, but it is one of them.  Another is if you are reaching into the guts of an object.  If you are accessing the members of an object and not through a method/property, you are too tightly coupled.

3 comments:

  1. Object-oriented design and design patterns can seem complex. There are a lot of ideas and cases to consider.

    ReplyDelete