While reviewing my previous post another great discussion, which may arise from paying attention to your references, came to my mind: Don’t Repeat Yourself (DRY) vs Coupling. Each time you add a new reference it means that you want to call the code from the other assembly, therefore you are coupling the two assemblies.
We have always been told that we should not tolerate duplication. We should always DRY our code. We have even become obsessed with it and each time we see few lines of code, which resemble we immediately extract them into a routine and have it called from the two places. What we often don’t realize is that we’ve just coupled those two places. DRY comes at a cost, and that cost is coupling.
Loosely coupled designs is another desiderate that we have. Lack of coupling means that the elements of our code are better isolated from each other and from change. In general the looser the coupling is the better the design is, because it is less resistant to change. And, change is certain!
On the other hand, of course that duplication is bad. It is one of the primary enemies of a well-designed system. It usually adds additional work, additional risk and additional unnecessary complexity. It is even more problematic when the code is copied & pasted and then small changes are done on it. The code is not identical, but it has teeny-tiny, hard to observe variations. Like an equals (
=) operator changed with a differs (
!=) or a greater or equals (
>=) changed with a strict greater (
<). The code is not identical, and the commonality is not abstracted. In such a context abstracting the commonality in an interface and making all the callers to depend on the abstraction, but not on the implementation, which should be well encapsulated / hidden, is the key to a good design. Here we couple them together, we pay the coupling cost, but with clear benefits.
Don’t tolerate coupling without benefits! Don’t pay a cost if it doesn’t bring you something back. If by DRYing stuff out you don’t make things simpler but rather more complex, then something may be wrong in your design. In this example if we have created a correct abstraction, which truly represents the commonality then changes of its implementation should not trigger changes into its callers, and changes in the callers will not trigger changes into the implementation.
Coming back to my previous post where I’ve talked about the benefits that come from monitoring our references, which is just another way of managing the dependencies in our code, we can have this discussion on the example there, too.
The application there is well isolated from the frameworks it uses. It has more types of UI. One was in WPF as a desktop app, and one was just a console app. It may have had a web UI as well. Another thing that I’ve emphasised there was that we might have some rules that say which references are allowed and which aren’t. Here is the diagram of the references:
When we are working on the WpfApplication we might find ourselves rewriting some of the code that we’ve already had written in the ConsoleApplication. The first thing that comes to mind would be to reference the assembly and reuse the code. But we can’t. Its against the rules, because we want that the different UIs to be independent. Making references among them would mean that the WPF needs the console UI, or even more strange the web UI would need the desktop UI. So, we are left with two options:
- duplicate the code in both assemblies
- create a new assembly (CommonUI) and put the code there
Option 2. reduces duplication, but it still creates coupling. Now all the UI assemblies will reference this common thing, and if it is not well abstracted then we may have indirect dependencies among the UIs. A change in WPF may trigger a change in the common assembly, which will trigger a change in the console or in the web UI. Tricky! We should see if it pays off. If it is just about some helpers it might be better to tolerate the duplication and don’t increase the coupling. On the other hand if it is something that has to be commonly presented in all the UIs of our application, then abstracting and encapsulating it in a common assembly might make our design better.
This is also a good example that we should try to DRY as much as possible in a bounded context, but we should not DRY among different context, because we will couple them together with low benefits.
In the end it is again about making the correct tradeoffs and realising that each time we make a decision we are also paying a cost. Dan North puts this very nicely in a talk that I like very much called Decisions, Decisions