Skip to content

Instantly share code, notes, and snippets.

@karol-majewski
Last active May 24, 2020 23:22
Show Gist options
  • Save karol-majewski/4186d95ed69bc7ff1f9cf9cca89672ef to your computer and use it in GitHub Desktop.
Save karol-majewski/4186d95ed69bc7ff1f9cf9cca89672ef to your computer and use it in GitHub Desktop.
What's the difference between composition and coupling?

I will use a contrived example.

The problem

We need to increment some numbers. Given an integer x, our function should return x + 1.

Coupling

Programmer 1 reads the requirements and creates the following implementation:

const add1 = (x: number) => x + 1;

Some time passes. The Business™️ realizes the opposite functionality is now needed. Now we need to decrement numbers as well. To support that, we can change what we have, or we can add new code.

Solution #1: change existing code

Programmer one considers it.

- const add1 = (x: number) => x + 1;
+ const add1 = (x: number, remove: boolean) =>
+   if (remove) {
+     return x - 1
+   } else {
+     return x + 1;
+   }

This gets the job done. However, Programmer 1 heard that boolean flags are bad, so the boolean gets replaced with a union of string litrals. Clean code, right?

- const add1 = (x: number) => x + 1;
+ const add1 = (x: number, mode: 'add' | 'remove') =>
+   if (mode === 'add) {
+     return x + 1
+   } else {
+     return x - 1;
+   }

Programmer 1 realizes there are a few problems now.

  • Unless a default argument for mode is provided, all callers will have to change.
  • The function is called add1, but confusingly it also subtracts.
  • If we decide to make the function name reflect its purpose, all callers will have to change.
  • The function is not unary anymore, so it cannot be used in the point-free style. If we curry it so that it stays > unary, all existing callers will have to change.
  • We introduced quite a big diff in relation to what we achieved. The reviewers will have more to read.

There's too many breaking changes. Programmer 1 decides to just add another function instead. After all, > duplication is cheaper than the wrong abstraction.

Solution #2: add more code

const remove1 = (x: number) => x - 1;

Programmer 1 saves the day again. Sure, the codebase just grew by a tiny amount. Who cares? The code is straightforward and easy to review. We didn't have to change any existing callers. Success all the way!

Now, the The Business™️ realized we don't always want to increment by one. The users should be able to put any number of items in the cart! How cool.

Programmer 1 is faced with the same dillema again. Our existing code is overspecialized — it's doing more than we need. We can only change the existing code or introduce new code. There is no other way.

But Programmer 1 doesn't mind. Shipping new code is their favorite thing to do.

Composition

Programmer 2 looks at the problem and recognizes its two-layered nature.

  • In its nature, this task is about adding numbers. That's the problem-agnostic part.
  • There is, however, a specific side to it. The Business™️ is convinced we only want to deal with ones at this point.

Programmer 2 designs their solution with respect to the layers they just discovered. Instead of creating the entire solution in one breath, they approach the problem in two steps.

// This part is problem-agnostic and will never change.
const add = (x: number) => (y: number) => x + y;

// This part is problem-specific and subject to change
const increment = add(1);

Same story happens. The Business™️ wants Programmer 2 to support the opposite functionality. Programmer 2 is happy to oblige — all it takes is reusing add and repurposing it to satisfy the new requirements.

const decrement = add(-1);

No existing callers were harmed in the making of this function. The reviewers won't have much to read neither.

If The Business™️ changes its opinion and decides we want to support any number instead of just ones, there is nothing we have to do. We already have the solution in place.

Takeaways

  • Composition preserves boundaries. Coupling blurs boundaries. Composed functions preserve their purpose, identity, and public interfaces. Coupling is arranged marriage.
  • Composition allows growth. Coupling slows you down.
  • Once you introduce coupling, there is no way back. It's a downward spiral. Whenever there are new requirements, you are forced to pick between changing existing code or introducing new code with duplicated functionality.
  • Generic code should be separated from problem-specific code. Generic code changes infrequently, if at all. Problem-specific code evolves constantly. If a system can grow without making collateraly (accidental) changes, it will evolve faster, because there will be fewer opportunities to introduce bugs. The code review process will become faster, because there will be less diffs to read.
  • Maximize the amount of cheap code in your codebase. Generic code is cheap. It' read-only. It can be replaced with a library. Problem-specific code is custom-tailored, requires domain knowledge and documentation. Thus, it is expensive. Aim to maximize the amount of problem-agnostic code you can write once and never touch again.
  • Duplication is not cheaper then the right abstraction. We often say that duplication is cheaper than the wrong abstraction. I totally agree. Somehow nobody ever defines wrong, though. A good abstraction will save you time and money.
  • Don't miss the forest for the trees. Don't just code whatever you were told. Recognize what the nature of the problem is. Instead of rushing into implementation, figure out what the generic part of the problem is and extract it. There is always one.
  • If you find yourself adding flags, then you just proved your abstraction incorrect.

Appendix

If you think the Solution #1 is crazy and no sane person would ever do such a thing, I agree. I have no idea why it comes naturally to many programmers. There are books and essays that address that code smell, and yet, it is still a popular and widely accepted way of solving problems.

As an exercise for the reader, I encourage you to look at the places where boolean flags are used in your codebase. Once you see the pattern, you see it everywhere.

[[showroom-3.0.md]] #design

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment