Software Design/Don't repeat logic in several places

Checklist questions:

  • Can repeated logic be extracted into a function?
  • Can repeated logic be encapsulated in a wrapper (adapter) object?
  • Can design of an abstract class be changed to avoid logic repetition in the implementations or at the usage sites?

This practice reflects the Don't repeat yourself principle.

The repeated logic will be a source of change amplification should it be changed in the future, thus making the code harder to change.[1]

Logic repetition welcomes bugs via typing mistakes and forgetting to update some places in the code where the logic occurs when the latter needs to be changed.

Duplication means every time readers see these copies, they need to compare them carefully to see if there is any difference,[1] thus contributing to the cognitive load.

A common byproduct of removing logic repetition is improving reusability of the code: if similar logic is already needed in several places in the codebase, it's quite likely that more such places will emerge in the future as the codebase grows. The abstractions that have been created to remove the logic repetition (see the next section) can then be reused in the new places.

Ways to avoid logic repetition

edit

Extract a function

edit

When the duplicated logic has no outputs (only side effects) or a single output it simply can be extracted as a function. This corresponds to Extract Function refactoring.

Why not

edit

Extracting a function reduces locality of the logic which might make the logic harder to follow (especially if the duplicated logic doesn't present a self-contained subproblem and there is no good semantic name for the extracted function)

Extracting a function makes readers of the code to navigate more.

See also the practice Prohibit overriding implementation of a function which has similar justification.

When the duplicated logic has multiple outputs or manifests in different places in the code, extracting a function might be ineffective in removing the repeated logic. In these cases, extracting a state-composing object or a decorator helps to consolidate the repeated logic.

Why not

edit

Shift the interface boundary

edit

Sometimes an API effectively requires a certain sequence of actions to be performed on the receiver object or the arguments before calling some function:

class Data {
  fun normalize() { ... }

  /** Expects the other data to be normalized */
  fun aggregate(other: Data) { ... }
}

// Code appears in many places
d1.aggregate(d2.normalize())

The normalization step can be pushed within the Data's responsibility scope:

class Data {
  /** As a side effect, normalizes the other data. */
  fun aggregate(other: Data) {
    other.normalize()
    ...
  }
}

The difference of this technique from extracting a function is that no new functions are added, but the existing functions are reorganized and their semantics are potentially changed.

Why not

edit

Putting some boilerplate logic behind the interface boundary is a logical equivalent of denormalization in databases. If there are cases when the extra logic should not be performed before the main logic then additional functions should be added to the interface, increasing the API size. If the extra operations have side effects putting them behind the interface boundary complicates the semantics of the functions and creates an opportunity for mistakes if developers don't notice or forget about the side effects. To mitigate that, the side effects could be reflected in the names of the functions: for example, the changed function in the example above may be called normalizeOtherAndAggregate().

It may be impossible to remove the extra semantics (the side effects) from the specification of the interface in the future, thus making it harder to change.

If the additional logic is idempotent then adding more variants of the function can be avoided at the expense of performing some unneeded work in some cases, thus reducing the runtime efficiency.

When several functions share some logic, they can be merged into a single function with extra parameters controlling the difference in behavior.

Why not

edit

Add a layer of abstraction

edit

It's almost always possible to eliminate logic duplication by adding a new layer of abstraction: see the "fundamental theorem of software engineering".

edit

References

edit
  1. 1.0 1.1 Refactoring: Improving the Design of Existing Code (2 ed.). 2018. ISBN 978-0134757599. https://martinfowler.com/books/refactoring.html.  Chapter 3, "Duplicated Code" section