Programming Practices – Issues with Deep Composition Hierarchies

designprogramming practices

Apologies if "Composition Hierarchy" isn't a thing, but I'll explain what I mean by it in the question.

There isn't any OO programmer who hasn't come across a variation of "Keep inheritance hierarchies flat" or "Prefer composition over inheritance" and so on. However, deep composition hierarchies seem problematic as well.

Let's say we need a collection of reports detailing the results of an experiment:

class Model {
    // ... interface

    Array<Result> m_results;
}

Each result has certain properties. These include the time of the experiment, as well as some meta-data from each stage of the experiment:

enum Stage {
    Pre = 1,
    Post
};

class Result {
    // ... interface

    Epoch m_epoch;
    Map<Stage, ExperimentModules> m_modules; 
}

Ok, great. Now, each experiment module has a string describing the outcome of the experiment, as well as a collection of references to experimental sample sets:

class ExperimentalModules {
    // ... interface

    String m_reportText;
    Array<Sample> m_entities;
}

And then each sample has… well, you get the picture.

The problem is that if I am modeling objects from my application domain, this seems like a very natural fit, but at the end of the day, a Result is just a dumb container of data! It doesn't seem worthwhile to create a large group of classes for it.

Assuming that the data structures and classes shown above correctly model the relationships in the application domain, is there a better way to model such a "result", without resorting to a deep composition hierarchy? Is there any external context that would help you determine whether such a design is a good one or not?

Best Answer

This is what the Law of Demeter / principle of least knowledge is about. A user of the Model shouldn't necessarily have to know about ExperimentalModules' interface to do their work.

The general problem is that when a class inherits from another type or has a public field with some type or when a method takes a type as parameter or returns a type, the interfaces of all those types become part of the effective interface of my class. The question becomes: these types that I depend on: is using those the whole point of my class? Or are they just implementation details that I have accidentally exposed? Because if they are implementation details, I've just exposed them and allowed users of my class to depend on them – my clients are now tightly coupled to my implementation details.

So if I want to improve cohesion, I can limit this coupling by writing more methods that do useful stuff, instead of requiring users to reach into my private structures. Here, we might offer methods to search for or filter results. That makes my class easier to refactor, since I can now change the internal representation without a breaking change of my interface.

But this is not without a cost – the interface of my exposed class now becomes bloated with operations that aren't always needed. This violates the Interface Segregation Principle: I shouldn't force users to depend on more operations than they actually use. And that is where design is important: Design is about deciding which principle is more important in your context, and about finding a suitable compromise.

When designing a library that must maintain source-compatibility across multiple versions, I'd be more inclined to follow the principle of least knowledge more carefully. If my library uses another library internally, I would never expose anything defined by that library to my users. If I need to expose an interface from the internal library, I'd create a simple wrapper object. Design Patterns like the Bridge Pattern or the Facade Pattern can help here.

But if my interfaces are only used internally, or if the interfaces I would expose are very fundamental (part of a standard library, or of a library so fundamental that it would never make sense to change it), then it might be useless to hide these interfaces. As usual, there's no one-size-fits-all answers.

Related Topic