avatarLuís Soares

Summary

The article argues against sharing code as the solution to code repetition, suggesting alternatives like cohesion, code hotspots, and the law of proximity.

Abstract

The article criticizes the overuse of code sharing as a solution to repetition, claiming it can lead to low cohesion and code hotspots. Instead, it suggests focusing on the law of proximity and creating groups of related code by functionality. The author advises against making technical things global and shared, as it can increase the surface area of the code and make it more use-case-driven. The article also discusses the importance of contextuality, self-documenting code, and reducing the surface area of the code. It also argues against the abuse of the DRY principle, suggesting that it is often used as a solution to the wrong problem.

Opinions

  • Code sharing is overrated and abused as a solution to code repetition
  • Low cohesion can lead to code hotspots, which can harm modularity
  • The law of proximity can be used to create groups of related code by functionality
  • Technical things should not be made global and shared, as it can increase the surface area of the code and make it more use-case-driven
  • The DRY principle is often abused as a solution to the wrong problem

Stop sharing code

Sharing code is overrated and it’s abused as the solution to code repetition. What’s the alternative? The answer is related to cohesion, code hotspots, and the law of proximity.

Photo by Kamesh Vedula on Unsplash

Cohesion

Cohesion refers to the degree to which the elements inside a module (e.g. a class) belong together. High cohesion means all the code in a module is related; low cohesion means that the module contains unrelated pieces of code.

Cohesion

Modules with low cohesion can lead to code hotspots — they’re like black holes of code and attract every feature. They harm modularity because they bind multiple features together. They make your codebase ill. Since there are multiple reasons to change hotspots, they grow indefinitely and are often a source of merge conflicts.

There are several reasons why code grows into hotspots. The most common reason is low cohesion, which means that the hotspot contains several unrelated parts and lacks modularity. Such hotspots attract many commits because they have too many responsibilities and those responsibilities tend to be central to your domain, which is why they change. Software Design X-Rays

A visual display of code hotspots (CodeScene)

There are two types of hotspots:

➡️ Business hotspots centralize many business operations (e.g. UserService) into one place. They have very low cohesion because they only share a broad business concept.

➡️ Technical hotspots centralize technicalities (e.g. a file full of enums); they are like the Hungarian notation on steroids because both highlight the technical type in place. Sorting technicalities by categories gives them too much spotlight and puts them ahead of high-level abstractions, which is backward thinking. You should avoid technical hotpots for similar reasons that you avoid technical stories — both focus on the wrong thing. They have no cohesion because nothing binds them together, except the technical side of it. These are the focus of the article.

Law of proximity

According to the law of proximity (one of the Gestalt Laws), things that are close together seem more related than things that are spaced farther apart. What does this have to do with code cohesion? I hypothesize that this law is not exclusive to the visual system. If you accumulate lots of unrelated technicalities in a file you start creating fake groups only because they’re put together.

Gestalt Laws of Perceptual Organization

My advice is to use the law of proximity to your advantage. Prefer creating groups of related code by functionality. Items that work closely together are placed next to each other. A bad group is “HTTP filters” because it’s technical. A good group is “Activate contract” because it highlights an actual business operation. This doesn’t necessarily mean that all related functionality should lie in the same place. This is only to make you think about the logical groups you want to create, which form your code's conceptual model. You should ask in which directions your code grows when new functionality arrives. If a place only grows due to technicalities, you have a code hotspot.

Pull the things that are unrelated further apart, and put the things that are related closer together. Modern Software Engineering

Contextuality

Segregating and sharing technicalities creates coupling between features. Tech files have too many unrelated reasons to change, which should ring a bell. Very few things, if any, need to be globally available (if it’s a shared concern like logging or monitoring, use dependency injection). If you need to share, you may be doing it wrong.

What’s the solution to avoid the centralization of technical artifacts? Split those files. Distribute their content. Put it into context. Then, make it private if possible.

In physics, the principle of locality states that an object is influenced directly only by its immediate surroundings. Principle of locality

Here are a few examples:

  • Enums: If you have a file full of enums, you can distribute them; put them together with the entities and APIs that they belong to.
  • Queries: files full of queries have many reasons to change. Make sure you distribute them where they belong to.
  • Exceptions and errors: Why do you have files or folders full of exceptions? My advice is to put exceptions/errors near where they belong. Find them an owner. If an exception is thrown by a use case or an entity, then consider moving the exception near it. Learn more.
  • Constants: Who hasn’t seen those typical files full of constants? I’m generally against constants, let alone files full of them. Even worse, I heard stories of endpoints that provide constants. Constants are a programming detail and code should not float around them. Put constants in their own contextual place. Even better: inline them if you can. Learn more.
  • Data transfer objects: Serializers, models (e.g. MVC models), request/response models, and other DTOs should be placed where they belong — their creator or logical owner. This helps to create logical groups and thus reduces the code surface area. Some models can even be made private. (De)serializers are a good example of that. (Note that entities and value objects are not merely DTOs and thus can be global.)
  • Utilities and helpers: Creating shared utilities is an antipattern, and is worsened if you pack them all in a file. It assumes things are going to be reused. Are you recreating something that’s part of the language standard library? Does it need to be shared? Isn’t it a symptom of a split decision?

Other bad examples include files full of queries, validators, and converters. The bottom line is that code should not be tech-driven but rather feature-driven.

Split software by functionality: not by technicality

Surface area

Making technical things global and shared leads to having thousands of fragmented pieces apart with no clear distinction between code hierarchy and API surface. A good practice is to reduce the surface area of the code and also make it more use-case-driven. If we look at the app’s use cases as a set of icebergs, the tests should only see what’s above the water; the technical details should lie underwater. Don’t create icebergs full of technical stuff. It doesn’t feel right. Instead, distribute it where it belongs.

The best modules are deep: they have a lot of functionality hidden behind a simple interface. A deep module is a good abstraction because only a small fraction of its internal complexity is visible to its users. A Philosophy of Software Design

Making things contextual helps to self-document your code as well because you can easily grasp the constituents of each use case — its algorithms, models, errors, and more importantly, its API.

What about DRY?

Sharing code is often abused as the solution to DRY. Repeated code is the problem, but the solution might be another. Start by asking:

  • Are you sure you’re not sharing something that’s natively part of the language or could easily be built with it? I’ve seen lots of cases like that, especially related to the manipulation of strings, lists, and maps.
  • Are you sure it needs to be shared in the first place? Possibly, that code can be copy-pasted as not all code duplication is knowledge duplication — there’s no harm in repeating code if it can evolve in different directions. Be aware that duplication is far cheaper than the wrong abstraction.
  • Are you sure you didn’t slice a decision across multiple places? The need to share something can be a smell of that. If you properly encapsulate code decisions, maybe you don’t need to share anything (related code should live together).

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. The Pragmatic Programmer

Prefer duplication over the wrong abstraction. The Wrong Abstraction

If sharing/DRY is really the solution then consider that there are multiple ways to do it besides creating a big shared function (e.g. perhaps you can create smaller functions/components and compose them; you could consider polymorphism for the parts that vary).

Shared code with lots of clients leads to fear to change “because who knows who’s using this?”. Once you notice, you’re into boolean parameters for different modes of operation. Even with good tests, you may refrain from refactoring (also, you don’t know if everyone has good tests).

Shared libraries

Creating shared libraries and using them across a bunch of microservices is sharing code at an alarming level. It also t couples all users of the shared code, creating pain and fear when changing it. On top of that, it requires maintenance tasks like distributing and keeping dependencies up-to-date. If you need to share libraries across microservices, you might have the wrong set of microservices.

So if you are creating a microservice-based system, with each service being independently deployable, and each service having its own deployment pipeline, you should not apply DRY between microservices. Don’t share code between microservices. Modern Software Engineering

Conclusion

I hope it’s clear by now that I’m not against DRY. I only believe that we are using the wrong solution to achieve it. DRY is not stuffing everything together in files or libraries and sharing with everyone. DRY is preventing that need in the first place. Also, people will jump too soon to share things that may look the same by mere coincidence but will evolve in different directions. Then, you need to make the shared code conditional, and that’s when hell begins.

Further reading

Code Quality
Refactoring
Modularity
Cohesion
Hotspots
Recommended from ReadMedium