What is coupling in programming? Is it something we want to avoid when we design and write code? If so, why? And more importantly, how? Let’s take a look at what coupling is and how it affects codebases.
What Is Coupling?
Let’s start with a definition from the venerable Design Patterns: Elements of Reusable Object-Oriented Software, AKA the Gang of Four (GoF) book.
coupling – The degree to which software components depend on each other.
So, coupling isn’t always a bad thing, is it? The components inside an application have to rely on each other, or it’s just a collection of unrelated stuff. If the code window in your IDE can’t rely on the operating system to open a file, you won’t get a lot of work done.
So, when is coupling in programming a problem?
Let’s go back to the GoF.
Classes that are tightly coupled are hard to reuse in isolation, since they depend on each other…Loose coupling increases the probability that a class can be reused by itself and that a system can be learned, ported, modified, and extended more easily.
Tight coupling is what we need to avoid. If components share too many dependencies, maintenance suffers. So, they’re difficult to understand and even worse, to modify.
A Brief Example of Coupling
Tightly Coupled Senders and Receivers
Let’s take a look at the code for opening a text file and printing its contents to a command line session. The sender and receiver example in the “Behavioral Patterns” chapter of the GOF book inspired this example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
namespace ConsoleApp1 { class Program { static void Main(string[] args) { Receiver receiver = new Receiver(@"\\Mac\Home\Documents\test.txt"); receiver.StartReading(); Console.WriteLine("Press any key to exit."); Console.ReadKey(); } } public class Receiver { private readonly Sender _sender; public Receiver(string filename) { _sender = new Sender(filename, this); } public void StartReading() { _sender.StartSending(); } public void Receive(string message) { System.Console.WriteLine(message); } } public class Sender { private readonly System.IO.StreamReader _file; private readonly Receiver _receiver; public Sender(string filename, Receiver receiver) { _file = new System.IO.StreamReader(filename); _receiver = receiver; } public void StartSending() { Task.Factory.StartNew(() => { string line; while ((line = _file.ReadLine()) != null) { _receiver.Receive(line); } _file.Close(); }); } } } |
If we look at the Program class, the code looks like a decent design for a straightforward tool. We pass Reader the name of the file to read and then tell it to start reading. There are simpler ways to print a text file to the console without creating two new classes. But if we look at this example as the first iteration of a general utility, it’s not bad.
But what happens when we want to add options for different sources and destinations? Maybe we want to send the contents of the file to a printer. Or, read from more than one file at a time. Perhaps we want to open a binary file and display its contents in hex. With this design, we have to make a code change to change the name of the file!
With this design, the receiver accepts the file name and creates the sender. The sender accepts a reference to the reader and passes the file contents back to it. Both classes know details about each other, and the receiver has to know the name of the file to read. Just explaining the design in a few sentences sounds complicated.
Loosely Coupled Senders and Receivers
If you’ve been working with C# or any other object-oriented language for a while, you know what’s next: interfaces.
First, let’s create interfaces for our sender and receiver.
1 2 3 4 5 6 7 8 9 10 11 |
public interface ISender { void AddReceiver(IReceiver receive); void StartSending(); } public interface IReceiver { void Receive(string message); } |
Here again, there are better ways to do this, but this implementation is short and lets us focus on coupling.
The IReceiver interface is as simple as it can be. It accepts a message. An IReceiver can do anything with a message. It might write it to another file, print it to the console, even change the data and pass it to another IReceiver.
ISender has two methods makes a few promises. It accepts a reference to an IReceiver, which implies it will save it and send messages. It has a StartSending method instead of Read, suggesting that it will handle getting and delivering the messages to IReceivers in its own thread.
Now, let’s refactor our original classes.
So, here is our new receiver.
1 2 3 4 5 6 7 8 |
public class ConsoleReceiver : IReceiver { public void Receive(string message) { System.Console.WriteLine(message); } } |
To say it’s simpler than the old version is an understatement. Its only concern is to accept a string message and print it to the console. It doesn’t create the sender or start it. It has no state of its own.
Now, let’s look at the new sender:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class TextFileSender : ISender { private readonly System.IO.StreamReader _file; private readonly List<IReceiver> _receivers = new List<IReceiver>(); public TextFileSender(string filename) { _file = new System.IO.StreamReader(filename); } public void AddReceiver(IReceiver receiver) { _receivers.Add(receiver); } public void StartSending() { Task.Factory.StartNew(() => { string line; while ((line = _file.ReadLine()) != null) { _receivers.ForEach(receiver => receiver.Receive(line) ); } _file.Close(); }); } } |
The new sender has gained some extra functionality, but only at the cost of a few new lines of code. It now holds a list of receivers and will deliver messages to all of them. We moved the receiver from the constructor to a new method, which introduces the possibility of starting to send messages when there are no receivers. But, using a list protects us from crashing. If there are no items, the sender quietly discards the message.
But, something is missing. Who creates the sender and receiver?
Adding a Commander
We’ve delegated some of the work to the Main function. The GoF calls this pattern a Commander.
1 2 3 4 5 6 7 8 9 10 |
static void Main(string[] args) { ISender sender = new TextFileSender(@"\\Mac\Home\Documents\test.txt"); IReceiver receiver = new ConsoleReceiver(); sender.AddReceiver(receiver); sender.StartSending(); Console.WriteLine("Press any key to exit."); Console.ReadKey(); } |
So, the Main function creates the sender and the receiver(s), connects them, and starts the processing. If we want to get messages from a different source or send them to one or more new destinations, we can reuse the interfaces and make a small modification to main.
Coupling and Cohesion
How can we apply this example to real-world code?
Software engineers often contrast coupling with another software design concept: cohesion. Coupling is how much components depend on each other. Cohesion is a measure of how much the parts of a component belong to together. The two properties are inversely proportional.
Tight coupling leads to low cohesion. Loose coupling leads to high cohesion.
We lowered the coupling in our example by separating concerns between the sender and the receiver. The receiver doesn’t need to know where the data comes from. So, it doesn’t need to create the sender and hold a reference to it. The sender needs to know where to send the data, but it doesn’t need to create the receiver or know how many there would be.
We ended up pushing some of the logic embedded in the two classes up into main. In production code, you would replace main with a formal class called something like a dispatcher or a manager. You might even push the list of receivers into it.
Measuring Coupling
NDepend can help you improve your code by showing you opportunities to remove tight couplings and increase cohesion.
Efferent coupling shows types that rely heavily on others. This metric highlights design problems since a type that interacts with a large number of objects has too many concerns.
Afferent coupling shows methods that are used heavily in a program. Taken on its own, it’s not a measure of code quality, but it may point out areas where you should separate a method into its own component, or break it down into more than one.
Don’t Get Burned
Coupling affects your ability to modify and maintain your code. If objects are tightly coupled, it’s difficult to reason about how it works. Also, changes tend to ripple across many objects. This tutorial introduced the basic concepts, but there’s a lot more to learn. Take a look at your code and see if you can find areas where you can lower coupling and increase cohesion.
Hi, thanks for this blog post. Do you know of any metrics to summarize the overall level of coupling in larger codebase?