16

Assume we have a software module A that implements a function F. Another module B implements the same function as F'.

There are a number of ways to get rid of the duplicate code:

  1. Let A use F' from B.
  2. Let B use F from A.
  3. Put F into its own module C and let both A and B use it.

All of these options generate additional dependencies between modules. They apply the DRY principle at the cost of increasing coupling.

As far as I can see, coupling is always increased or at leased moved to a higher level when applying DRY. There seems to be a conflict between two of the most basic principles of software design.

(Actually I don't find it surprising that there are conflicts like that. This is probably what makes good software design so difficult. I do find it surprising that these conflicts are normally not addressed in introductory texts.)

Edit (for clarification): I assume that the equality of F and F' is not just a coincidence. If F will have to be modified, F' will likely have to be modified in the same way.

Frank Puffer
  • 6,459

5 Answers5

17

All of these options generate additional dependencies between modules. They apply the DRY principle at the cost of increasing coupling.

Why yes they do. But they decrease coupling between lines. What you get is the power to change the coupling. Coupling comes in many forms. Extracting code increases indirection and abstraction. Increasing that can be good or bad. The number one thing that decides which you get is the name you use for it. If looking at the name leaves me surprised when I look inside then you haven't done anyone any favors.

Also, don't follow DRY in a vacuum. If you kill the duplication you're taking responsibility for predicting that these two uses of that code will change together. If they are likely to change independently you've caused confusion and extra work for little benefit. But a really good name can make that more palatable. If all you can think of is a bad name then please, just stop now.

Coupling will always exist unless your system is so isolated that no one will ever know if it works. So refactoring coupling is a game of choosing your poison. Following DRY can pay off by minimizing the coupling created by expressing the same design decision over and over in many places until it's very difficult to change. But DRY can make it impossible to understand your code. The best way to salvage that situation is finding a really good name. If you can't think of a good name I hope you're skilled at avoiding meaningless names

candied_orange
  • 119,268
3

There are ways to break explicit dependencies. A popular one is to inject dependencies in runtime. This way, you obtain DRY, remove coupling at cost of static safety. It is so popular nowadays, that people do not even understand, that that's a tradeoff. For example, application containers routinely provide dependency management immensely complicating software by hiding complexity. Even plain old constructor injection fails to guarantee some contracts due to lacking type system.

To answer the title - yes, it is possible, but be prepared for consequences of runtime dispatch.

  • Define interface FA in A, providing functionality of F
  • Define interface FB in B
  • Put F in C
  • Create module D to manage all dependencies (it depends on A, B and C)
  • Adapt F to FA and FB in D
  • Inject (pass) wrappers to A and B

This way, the only type of dependencies you would have is D depending on each other module.

Or register C in application container with built-in dependency injection and enjoy fortunes of autowiring slowly growing runtime classloading loops and deadlocks.

Basilevs
  • 3,896
1

I'm not sure that an answer without further context makes sense.

Does A already depend on B or vice versa? — in which case we might have an obvious choice of home for F.

Do A and B already share any common dependencies that might be a good home for F?

How large/complex is F?  What else does F depend upon?

Are modules A and B used in the same project?

Will A and B end up sharing some common dependency anyway?

What language/module system is being used: How painful is a new module, in programmer pain, in performance overhead?  For example, if you're writing in C/C++ with the module system being COM, which causes pain to the source code, requires alternate tooling, has implications on debugging, and has performance implication (for inter-module invocations), I might take some serious pause.

On the other hand if you're talking about Java or C# DLLs that combine rather seamlessly in a single execution environment, that's another matter.


A function is an abstraction, and supports DRY.

However, good abstractions need to be complete — incomplete abstractions may very well cause the consuming client (programmer) to make up the shortfall using knowledge of the underlying implementation: this results in tighter coupling than if the abstraction were offered instead as more complete.

So, I would argue to look to create a better abstraction for A and B to depend upon than simply moving one single function into a new module C

I'd be looking for a set of functions to tease out into a new abstraction, which is to say, I might wait until the code base is further along in order to identify a fuller/more complete abstraction refactoring to do rather than one based on a single function code tell.

Erik Eidt
  • 34,819
0

The answers here that focus on all of the ways you can “minimize” this problem are doing you a disservice. And “solutions” simply offering different ways to create coupling are not truly solutions at all.

The truth is that are failing to understand the very problem you have created. The issue with your example has nothing to do with DRY, rather (more broadly) with application design.

Ask yourself why modules A and B are separate if they both depend on the same function F? Of course you will have problems with dependency management/abstraction/coupling/you-name-it if you commit to a poor design.

Proper application modeling is done according to behavior. As such, the pieces of A and B that are dependent on F need to be extracted into their own, independent module. If this is not possible, then A and B need to be combined. In either case, A and B are no longer useful to the system and should cease to exist.

DRY is a principal that can be used to expose poor design, not cause it. If you can’t achieve DRY (when it truly applies - noting your edit) due to the structure of your application, it is a clear sign that the structure has become a liability. This is why “continuous refactoring” is also a principal to follow.

The ABC’s of other design principals (SOLID, DRY, etc) out there are all used to make changing (including refactoring) an application more painless. Focus on that, and all of the other problems begin to disappear.

user3347715
  • 3,234
0

All of these options generate additional dependencies between modules. They apply the DRY principle at the cost of increasing coupling.

I have a different opinion, at least for the third option:

From your description:

  • A needs F
  • B needs F
  • Neither A nor B need each other.

Putting F in a C module doesn't increase coupling since both A and B already need C's feature.

mouviciel
  • 15,491