Software Design/Code smells/Object access chain

Checklist questions:

  • Could an object access chain like foo.getBar().doBaz() be encapsulated behind a single function call like doBazOnBar() on object foo?

This is also known as the Law of Demeter or the "rule of one dot".

This code smell could be removed by applying Hide Delegate or Extract Function refactorings described in Refactoring.[1][2]

Coupling and easiness of change

edit

Code with object access chains depends on the classes of all intermediate objects in the chain. In case they are not common reusable data structures such as collections, this makes refactoring these classes harder, e. g. by amplifying the amount of code changes needed.[3]

This factor is more important when the enclosing object is used outside of the codebase where it is defined, i. e. when it resides in a library. Hiding an object access chain behind a function allows refactoring the library and the dependant projects at different times. Refactoring of the API of the enclosing object may still be needed, however, to avoid divergence in terminology. For example, if there is a function addMonitoringSync(Sync) in some class and the concept of "monitoring" is renamed to "metrics", the class may still need to rename the function to addMetricsSync(Sync) sooner or later, though it may give the dependant projects time to migrate, e. g. by deprecating the old function while adding a new one with the updated name.

If the enclosing object is used only within a single codebase and automated refactorings are possible, this factor may reverse: see section Change amplification below.

Information hiding and complexity

edit

Encapsulating object access chains reduces the API surface of the enclosing object and thus makes it easier to test it and to reason about all possible state transitions of the enclosing object if the child object is mutable.

Code repetition

edit

All occurrences of the same object access chain are repeated code. See practice Don't repeat logic in several places.

Cognitive load and semantics

edit

Providing a function that encapsulates an object access chain reduces the cognitive load if the last function call in the chain changes the state of the respective object. When observing an object access chain, readers have to prove for themselves that the state-changing call doesn't break the abstraction provided by the previous objects in the chain. For example, consider the following code:

class Registry {
  fun getListeners(): List<Listener>
  fun addListener(listener: Listener)
}

// somewhere else
registry.getListeners().add(listener)
// vs.
registry.addListener(listener)

When readers see the access chain registry.getListeners().add(listener) they may not be sure that the logic of classes Registry and Listener doesn't require to notify newly added listeners immediately about some events that may have already happened with an object registry, for example:

registry.getListeners().add(listener)
if (registry.isInitialized()) {
  listener.registryInitialized()
}

Or, that the collection of Listeners in the Registry is not copy-on-write and therefore requires an update via a separate setListeners operation:

registry.setListeners(registry.getListeners().append(listener))

Developers may have to read the API documentation for classes Registry and Listener, or their source code to clarify this question for themselves.

On the other hand, when developers see registry.addListener(listener) readers may be sure (if they trust the developers of the Registry class) that function addListener() handles everything that is needed for adding a listener to a registry.

Encapsulating an object access chain as a single function is an opportunity to give this function a descriptive (or a colorful) name which better reflects the semantics of the operation than an object access chain.

Debuggability

edit

In programming environments with a null pointer, if one of the objects in the chain is null and null dereferencing occurs, it may be unknowable which exact dereferencing in the chain failed, making debugging harder. For example, in Java, this is the case until OpenJDK 14.[4]

Discoverability

edit

In the context of the example from section Cognitive load and semantics above, when a developer wants to add a listener to a registry, they may type registry.add... to discover the appropriate function via the autocompletion feature in their IDE. If there are intermediate objects in the chain, for example, registry.observability.listeners.add() it's harder to discover the functionality.

Robustness in concurrent environment

edit

Object access chain on a thread-safe object may expose parts of the object's state bypassing lock protection. If the exposed object is not immutable, there is a race condition possible.[5] Thus, avoidance of object access chains on mutable objects used in a concurrent environment protects the code from a certain class of concurrency bugs, i. e. increases the robustness of the code.

Why not

edit

Code and interface size, steepness of the learning curve

edit

Functions that hide the object access chains are boilerplate. They increase the amount of code in the root class, as well as its API size, which may increase the steepness of the learning curve for the class.

Cognitive load, apparency of dependencies and semantics

edit

Avoiding object access chains may increase the cognitive load imposed on users as well as decrease it (see section Why § Cognitive load and semantics above). Exposing parts of the state and functions on the sub-object through distinct functions on the root object may obscure the dependency between these parts of the state and functions, making the users check that for themselves in the API documentation or the source code of the class. Exposing a lot of weakly clustered functions on the root class may make grasping the abstraction embodied by it harder.

Consistency

edit

If the object exposes object access to one of its parts anyway, providing access to other parts using object access chains rather than via encapsulating functions on the root class may be taken as a form of structural alignment in the code and the interface of the class.

Change amplification

edit

When the enclosing object is used within a single codebase and automated refactorings are available, encapsulating object access chains within functions may only increase the change amplification. When the structure or names of objects in the chain are modified, this should may also need to be reflected in the name of the encapsulating function to preserve the consistency in naming.

When the codebase in question is a library or the automated symbol renames are not available in the programming environment, this factor may reverse: see the section Coupling and easiness of change above.

Using both approaches

edit

Many of the factors contributing to why object access chains should be considered a code smell: information hiding and complexity, cognitive load and semantics, robustness in concurrent environment are largely (or completely) irrelevant when the leaf object in the access chain is immutable. On the other hand, an unprotected object access chain to a mutable object in some cases, e. g. in a concurrent environment is not just a matter of design but a bug.

edit

See also

edit

References

edit
  1. Refactoring: Improving the Design of Existing Code (2 ed.). 2018. ISBN 978-0134757599. https://martinfowler.com/books/refactoring.html.  Chapter 7, "Hide Delegate" section
  2. Refactoring: Improving the Design of Existing Code (2 ed.). 2018. ISBN 978-0134757599. https://martinfowler.com/books/refactoring.html.  Chapter 6, "Extract Function" section
  3. Refactoring: Improving the Design of Existing Code (2 ed.). 2018. ISBN 978-0134757599. https://martinfowler.com/books/refactoring.html.  Chapter 3, "Message Chains" section
  4. "JEP 358: Helpful NullPointerExceptions".
  5. "Code review checklist for Java concurrency: Non-trivial mutable object is not returned from a getter in a thread-safe class?".