
I have been struggling for some time to explain to fellow developers the merits of loose coupling. It's usually a hard argument to make. To argue for loose coupling, it is necessary to overcome the arguments against it: high degrees of loose coupling in otherwise small projects (small number of classes, small number of developers, little dependence on third-party tools, few database tables, very short development cycle, etc.) is almost never going to give much bang for the buck. However, as software projects mature, an architectural tenacity toward loose coupling in the early stages of a project can mean big (BIG) payoffs later.
Why don't more developers embrace loose coupling? To be sure, it's harder than tightly coupling your code. From the average developer's perspective, tighter coupling means that features are coded faster, bugs are squashed more quickly, direct relationships between objects are easier to conceptualize for architects and easier to visualize for developers, IntelliSense for .NET developers works better, and problems are tracked down more quickly. Try arguing against all that! The truth is, direct relationships between systems and components simplifies life on smallish projects. However, as a project grows, tight coupling will likely cause some of the most painful growing pains you will face as a software developer.
What is Loose Coupling?
If you're still scratching your head because you don't quite understand what loose coupling is, there are lots of reasonably good definitions on the web (this is a pretty good one and this one is even better if you don't mind the academic slant). In short, loose coupling is a design goal that seeks to reduce the interdependencies between components of a system with the goal of reducing the risk that changes in one component will require changes in any other component. Hold on, isn't this is same tired idea my 1st year computer science professor called encapsulation? Got it, thanks. Not exactly - encapsulation in an OOP sense generally aims to guide (or force) users of your components to use your classes correctly and makes it harder to abuse the class's functionality by hiding the implementation. Loose coupling is a much more generic concept intended to increase the flexibility of a system, make it more maintainable, and make the entire framework more 'stable'.
How do you measure coupling? Not easily, I'm afraid. The degree of coupling between two components can be qualitatively measured by determining the effects of changing the contract or interface between two components or systems. For example:
- What would happen if you added or deleted a property from a class?
- What would happen if you changed the data type of a column in a table?
- What would happen if you added a column to a table?
- What would happen if you changed you asynchronous email component?
- What would happen if your database server changed from SQL Server to mySQL or Oracle?
The degree of pain that each of these changes would cause you is a good qualitative measurement of how tightly coupled your components are to the components they depend on.
Symptoms of Tightly Coupled Systems
If you have ever deployed a seemingly innocuous change only to find that other parts of your system broke when you did, then you have likely experienced one of the chief symptoms, in my experience, of tight coupling. If you have ever experienced a problem where it was hard to fix a problem because of the number of changes to other components that were required to make the change safely, then you have experienced the symptoms of tight coupling. If you find yourself repeatedly choosing "the poor man's solution" because the real solution was just too risky or required too many components to change, then you were likely the victim of tight coupling.
For the reasons I gave previously, tight coupling generally speeds the development of smallish projects. The pains caused by tight coupling aren't generally felt until the project starts to grow. As features (and developers) are added to a project, the original design purity grows cloudy in imperceptible increments. The pure evil of tight coupling becomes most apparent once the product has grown in the number of features that have been bolted on and has expanded past the first few developers that set the vision for the project's architecture.
The problem is, it is only when you have reached the stage in a project when you are feeling the pains of tight coupling that it is reaching the stage where it becomes very difficult to remedy. This is why tight coupling is such an insidious problem - even experienced and otherwise conscientious developers will be lulled by the comforts of tight coupling. It feels good. Control just feels nice.
Because your earlier design and coding choices have led to a system that is difficult to maintain without considerable grief, tight coupling is a notorious form of technical debt. In fact, in my experience, a considerable portion of redesign-from-the-ground-up projects are initiated because of high degrees of tight coupling making incremental improvement seemingly impossible or impractical (but resist this temptation at all costs).
Loose Coupling In Your Projects
Let's make sure we are on the same page. You can never completely loosely couple everything - that's a given. At some point, some part of your software will be VERY tightly coupled to something else. The point isn't to loosely couple everything. The primary goal is to reduce the amount of work and the risk involved when you change the interfaces between components of your system. Using this as our goal, we should look for areas where changes will cause lots of downstream work. You also need to consider the likelihood that changes to a particular system will occur. Face it, most people aren't going to change their database servers often, so maybe that's not your main focus. However, the licensing tool, EDI message parser, logging framework, email product, etc. are all great candidates for loose coupling.
Look a little closer - you will also find that there are interfaces between the components inside your software that should be loosely coupled. In fact, these are likely to be the areas of tight coupling that are the most difficult to fix.
Very simplistically, you should look for ways to maintain a consistent interface that hides changes that occur. If you send email notifications from your software, a loose coupling approach would be to build an interface around your email component of choice exposing only the functionality you need for your application. If done correctly, you could easily change email component providers with few changes to your application. In this simple example, only the implementation of the email interface wrapper would need to change - create an object of a different type, set different properties, and call a different method to send the email. By loosely coupling, we didn't eliminate all the work required to make a change, but we were able to isolate the work to a very manageable, easily testable, low risk, and quickly implemented project.
Keep in mind that loose coupling comes at a price. Consider these issues:
- Consider the cost of implementing loosely coupled interfaces versus a more tightly coupled solution. For sure, loosely coupled interfaces mean not only more code but also more complex code.
- Tightly coupled interfaces are easier to conceptualize for architects, easier to code for developers, and easier to troubleshoot when problems are suspected.
- The cost of tightly coupled interfaces will be measured in frustration, not necessarily in the developer time it will take to fix the tight coupling. However, ghastly amounts of developer time and increased project risk are two of the usual suspects. With tight coupling, you will find that seemingly simple changes turn into complex coding projects and become very difficult to test.
- The cost of uncoupling tightly coupled interfaces down the road can be very costly and will likely be very risky.
- How likely is it that a tightly coupled interface will need to change in future versions of your code.
As you get a good feel for loose coupling, you will actually learn to take advantage of the looseness of the application. The looseness that you will exploit will be the same parts that traditionally make loose coupling more difficult. Consider being able to make components of your system pluggable - you could replace pieces interchangeably. Consider how testing a large application could be made more simple if you replaced a complex business object or data layer object with a simple mock that would enable test cases to exercise other components of the system.
Evolution of the Software Developer
In a great blog post from last year, Jeremy Miller chronicles the maturation process of a software developer from the perspective of loose coupling. Most experienced developers have intuitively known about this progression even if they couldn't put it into words. To paraphrase, Jeremy explains that developers start with the "Just Make It Work" philosophy, fixing problems wherever they realize there might be a problem. Quickly, the developers who wriggle out of the primordial coding goo and begin to realize that there are 'correct' places for things, and strive to put things in their 'correct' place. Still more time passes and these same developers begin to realize that loose coupling is important in all but very simple projects. For those developers who live long enough, their loose coupling fu evolves naturally into the use of inversion of control.
To help get you further along the path, work through thought experiments
to consider how you will change systems later and how you will need to
go about making the required changes. As you do, weigh the costs and
benefits of loose coupling.
With any luck, in future posts I will work through some concrete examples of places where you should consider loose coupling and how to implement it.