15

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,411
  • 5
  • 21
  • 38
  • Assuming development went in this order (A, then F, then B), I'd first implement F in A, then if it turned out that another use case in B existed, I might then consider moving F to a generic (utility) module C. If after a while I get a lot of entries in the utility module, and a pattern develops, that might be a sign that the functionality there should be broken into a different module D... – jrh Jul 15 '18 at 17:43
  • 2
    ... I think DRY can be a very useful strategy, but this question illustrates an inefficiency with DRY. Some (e.g., OOP enthusiasts) might argue that you should copy/paste F into B just for the sake of maintaining the conceptual autonomy of A and B but I haven't ran into a scenario where I would do that. I think copy/pasting code is just about the worst option, I can't stand that "short term memory loss" feeling where I was just so sure that I already wrote a method / function to do something; fixing a bug in one function and forgetting to update another can be another major issue. – jrh Jul 15 '18 at 17:46
  • ... Also in theory A and B could inherit from some other object C, which implemented the function, but I would not recommend doing that for just one function. If you specify a programming paradigm you might get more specific answers, though I'd be interested in a more generic "module based" answer too. – jrh Jul 15 '18 at 18:01
  • 3
    There arelots of this OO principles contradicting each other. In most case you have to find a reasonable trade-off. But IMHO the DRY principle is the most valuable. As @jrh wrote: having same behavior implemented at multiple places is a maintenance nightmare that should be avoided at any cost. Finding out that you forgot to update one of the redundant copies in production can take down your business. – Timothy Truckle Jul 15 '18 at 18:33
  • 2
    @TimothyTruckle: We shouldn't call them OO principles because they apply to other programming paradigms as well. And yes, DRY is valuable but also dangerous if overdone. Not only because it tends to create dependencies and thus complexity. It is also often applied to duplicates caused by coincidence which have different reasons to change. – Frank Puffer Jul 15 '18 at 19:22
  • @FrankPuffer one other thing that might be worth noting, if you did decide at first to put F in C (implying that you didn't see any reason for A and B to differ in functionality at the first phase, or for YAGNI's sake), and you later decided that A and B both need their own flavor of F, you could at that point copy/paste it and edit it (it's easier to copy/paste than it is to unify the result of a bad copy/paste). Alternatively if this is an uncommon case it might not be so bad to make the changes in some variant of F called G (that might even call F at some point), and put both F and G in C. – jrh Jul 15 '18 at 19:33
  • 1
    ... also sometimes when I've run into this scenario I've been able to break up F into parts that can be useable for what A needs and what B needs. – jrh Jul 15 '18 at 19:36
  • 1
    Coupling is not inherently a bad thing, and is often necessary to decrease errors and increase productivity. Were you to use a parseInt function from the standard library of your language within your function, you would be coupling your function to the standard library. I haven’t seen a program that does not do this in many years. The key is to not create unnecessary couplings. Most often, an interface is used to avoid/remove such a coupling. For instance, my function can accept an implementation of parseInt as an argument. However, this is not always necessary, nor is it always wise. – Joshua Jones Jul 16 '18 at 01:48
  • @jrh `you should copy/paste F into B just for the sake of maintaining the conceptual autonomy of A and B but I haven't ran into a scenario where I would do that.` Both are valid approaches depending on the context. For example, just because `Person` and `Country` both have a `string Name` property doesn't mean that they **share** the same reused property. If a country's name becomes an `int`, that doesn't mean that people's names also get turned into `int`. You should only abstract **reused logic** and you should avoid abstracting **currently similar logic**. – Flater Jul 16 '18 at 09:26
  • @jrh: To summarize: If the expected outcome of a change to A is applying the same change to B, then they share logic (abstracting is the best approach). If the expected outcome is that a change to A does not imply a change to B, then they do not chare logic (copy paste is the best approach) – Flater Jul 16 '18 at 09:29
  • @Flater true, it's hard to say in a hypothetical situation without context. I'd still probably try to salvage any shared behavior in `Name` if it was complex enough and let both classes use it. If it were just a "property" with little behavior I might copy/paste it. Though to be fair I am currently maintaining a codebase where the author copy/pasted an entire giant class including duplicated *mathematical function implementations* that have no reason to differ between implementations, so I've been living the "copy/paste cleanup" life for a while now. – jrh Jul 16 '18 at 16:41

5 Answers5

16

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
  • 102,279
  • 24
  • 197
  • 315
  • 1
    Just to make sure I understand your point correctly, let me put it a bit differently: If you assign a good name to the extracted code, the extracted code is no longer relevant for understanding the software because the name says it all (ideally). The coupling still exists at a technical level but not at a cognitive level. Therefore it is relatively harmless, right? – Frank Puffer Jul 16 '18 at 16:28
  • 1
    Nevermind, my bad: https://meta.stackexchange.com/a/263672/143358 – Basilevs Jul 16 '18 at 18:57
  • @FrankPuffer better? – candied_orange Jul 16 '18 at 19:37
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
  • 1,221
  • 9
  • 11
  • 1
    +1, but with the explicit caveat that this is usually a bad tradeoff. That static safety is there for your protection, and circumventing it with a bunch of spooky action at a distance is just asking for hard-to-track-down bugs further down the line when your project complexity grows a bit... – Mason Wheeler Jul 15 '18 at 20:53
  • 1
    Can DI really be said to break depenency? Even formally, you need an interface with F's signature to implement it. But still,if module A is rwally using F from C it depwnds on it, regardless of C being injected at runtime or directly linked.DI does not break the dependency, bug only defer failure if the dependenc is not provided – max630 Jul 16 '18 at 05:38
  • @max630 it replaces implementation dependency with contractual one, which is weaker. – Basilevs Jul 16 '18 at 18:45
  • @max630 You are correct. DI cannot be said to break a dependency. DI is, in fact, a method of *introducing* dependencies, and is really orthogonal to the question asked in regard to coupling. A change in F (or the interface encapsulating it) would still require a change in both A and B. – user3347715 Jul 19 '18 at 17:11
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
  • 33,282
  • 5
  • 57
  • 91
  • 2
    Is not it dangerous to couple abstractions and dependency graph? – Basilevs Jul 15 '18 at 19:53
  • `Does A already depend on B or vice versa? — in which case we might have an obvious choice of home for F.` This assumes that A will always rely on B (or vice versa), which is a very dangerous assumption. The fact that OP sees F as not inherently part of A (or B) suggests that F exists without being inherent to either library. If F belongs to one library (e.g. a DbContext extension method (F) and an Entity Framework wrapper library (A or B)), then OP's question would be moot. – Flater Jul 16 '18 at 09:30
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,084
  • 11
  • 16
  • Do you suggest to have exactly one module in every application? – Basilevs Jul 16 '18 at 17:14
  • @Basilevs Absolutely not (unless it’s warranted). I suggest having as many completely decoupled modules as possible. After all, that’s the entire purpose of a module. – user3347715 Jul 16 '18 at 18:11
  • Well, question implies modules A and B to contain unrelated functions, and to be already extracted accordingly, why and how should they cease to exists? What is your solution to the problem as stated? – Basilevs Jul 16 '18 at 18:14
  • @Basilevs The question implies A and B have not been modeled properly. It is this inherent deficiency that is *causing* the problem in the first place. Certainly the simple fact that they *do* exist isn’t evidence that they *should* exist. That’s the point I’m making above. Clearly, an alternative design is necessary to avoid breaking DRY. It’s important to understand that the very purpose of all of these “design principals” is to make an application easier to change. – user3347715 Jul 16 '18 at 18:24
  • Was a ton of other methods, with completely different dependencies modeled bad and have to be redone, just because of this single non-accidental one? Or do you assume that, modules A and B contain a single method each? – Basilevs Jul 16 '18 at 18:28
  • @Basilevs You are making an awful lot of assumptions. The question was about applying a principal (DRY) to hypothetical software, and my answer (the only truly correct one listed so far) attempts to explain how the example has been contrived in such a way to create the problem. – user3347715 Jul 17 '18 at 01:01
  • @Basilevs Two modules sharing a dependency isn’t a DRY problem. It’s a design problem of which the impossibility of applying DRY is a symptom. It’s critical the questioner understands that or they are doomed to continue to run into/cause this issue. The entire purpose of creating modules is to model a system into *independent*, composable units based on *behavior*. – user3347715 Jul 17 '18 at 01:11
  • @Basilevs I cannot be more clear. The solution to the problem is to redesign the system. That is the only actual solution. Anything else is simply a workaround, not a solution. Modules A and B have outlived their usefulness when they need to become coupled. – user3347715 Jul 17 '18 at 07:31
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,473
  • 1
  • 37
  • 64