21

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?

6
  • 8
    Yes, exactly. The problems people attempt to fix by replacing inheritance with composition don't magically go away just because you changed one feature aggregation strategy out for another. They're both solutions to managing complexity, and that complexity is still there and has to be handled in some way. Nov 4, 2016 at 19:57
  • 8
    I don't see anything wrong with deep data models. But I also don't see how you could have modeled this with inheritance since it's a 1:N relationship at each layer.
    – usr
    Nov 4, 2016 at 21:29
  • Might not apply to your concrete problem, but I suggest to also take a look at Aggregate Root. Nov 7, 2016 at 9:20
  • The rule should say "beware of complexity - it'll eat you alive". Jul 28, 2023 at 7:54
  • I don't see the problem with the model shown. As long as you follow encapsulation, the composition graph should be as large and deep as necessary. In other words, no, "deep composition hierarchies" are not bad in itself.
    – JacquesB
    Sep 1, 2023 at 14:12

8 Answers 8

19

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.

6
  • You mentioned a major concern I was having; the idea that I had to reach far into the implementation in order to do something useful at higher levels of abstraction. A very useful discussion, thanks. Nov 4, 2016 at 20:27
  • "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?" - This is totally irrelevant in terms of inheritance tree depth. Your depicted problem is about encapsulation. Indeed, "Law of Demeter" (LoD) is a good principle to follow in order to implement proper encapsulation.
    – BionicCode
    Jul 27, 2023 at 9:54
  • But LoD does not care about inheritance or composition or tree depths. I don't know why this answer was accepted and received any upvotes. I just came here because someone referenced this questionable answer. Again, the tree depth is not affected by making members private or public or hide them behind interfaces. This makes this answer a wrong answer. Semantically and technically wrong.
    – BionicCode
    Jul 27, 2023 at 9:54
  • In fact, LoD is the main reason we end up with deep hierarchy trees. Because, how do you eliminate the hierarchy tree? You put all your code into a single class. Principles like "Single responsibility" SRP and "Law of Demeter" (LoD) make us to spread code across multiple classes and levels of class hierarchies.
    – BionicCode
    Jul 27, 2023 at 10:11
  • @BionicCode There's nothing inherently wrong with inheritance or composition. I mentioned the LoD because it provides an unified way to look at these concerns: what is the effective API surface of this object? Am I implicitly including APIs I don't control? Small and explicit APIs are simpler and help with loose coupling. I agree that LoD-style encapsulation doesn't change the complexity of the overall object graph, but it does keep the public API of an object focused. I don't think the LoD leads to messy code, though blind application does lead to lots of pointless wrapper methods.
    – amon
    Jul 27, 2023 at 11:02
6

Aren't “deep composition hierarchies” bad too?

Of course. The rule should actually say "keep all hierarchies as flat as possible."

But deep composition hierarchies are less brittle than deep inheritance hierarchies, and more flexible to change. Intuitively, this should come as no surprise; family hierarchies are the same way. Your relationship with your blood relatives is fixed in genetics; your relationships with your friends and your spouse are not.

Your example doesn't strike me as a "deep hierarchy." A form inherits from a rectangle which inherits from a set of points which inherits from x and y coordinates, and I haven't even gotten a full head of steam yet.

3
  • Tree depths is not the problem. If the depth reflects proper specialization then everything is fine. It has no impact on the compiler or design in terms of architecture. The problem stems from programmers using inheritance when it is semantically incorrect. That said, in this context a hierarchy depth of 2 can be already too deep if inheritance is the wrong choice.
    – BionicCode
    Jul 27, 2023 at 10:05
  • Additionally, the fact that you are not able to quantify what depth is regarded as too deep shows that you don't have a case here. Depth is only a indicator that can show that inheritance was used wrong. But after reviewing your class design you may come to the conclusion that all is well. Finally, your claims about "brittleness" are also made up and bare of any substance and lack further explanation. Even the flexibility part is wrong. You can always replace an implementation with a new specialization, especially when you define variables using the base type.
    – BionicCode
    Jul 27, 2023 at 10:06
  • Composition vs. inheritance is basically about semantics and about violating the "Liskov substitution principle" (LSP). Tree depth for both inheritance and composition is not relevant.
    – BionicCode
    Jul 27, 2023 at 10:06
4

Paraphrasing Einstein:

keep all hierarchies as flat as possible, but not flatter

Meaning if the problem you are modeling is like that, you shouldn't have qualms about modeling it as it is.

It the app were just a giant spreadsheet, then you don't have to model the composition hierarchy as it is. Otherwise do it. I don't think the thing is infinitelly deep. Just keep the getters that return collections lazy if populating such collections is expensive, like asking for them to a repository that pulls them form a database. By lazy I mean, don't populate them until they are needed.

1

The answers, especially the accepted one are wrong. It starts with the fact that all answers accept and base their ideas on the implication made by the title of the question "Aren't "deep composition hierarchies" bad too?" that deep inheritance trees are bad.

It's also the fact that nobody is able to quantify "too deep", which could be a good indicator that there is no thing like "too deep".
When inheritance is used wrong a depth of one level is already too deep.

The point is that hierarchy depth doesn't matter at all.
Going back to the beginning of a problem that asked "how to reuse functionality defined and exposed by a type" we end up with two solutions: inheritance and composition.

Why is there "inheritance"?

Inheritance is the act of specialization: you have a most general class, the base class. This base class contains the most general functionality that allows the most types of specializations. Now by specializing the base type (aka inheritance) we can add more specialized functionality to the functionality of the base type.
We are extending the base type's functionality vertically by adding more hierarchy levels. We are extending the hierarchy horizontally when we add more implementations of a parent superclass (siblings).
Inheritance naturally implies a semantic relationship synonymous to a is a relationship.
Inheritance for example enables polymorphism and covariance/contravariance.

Why is there "composition"?

Composition is the act of extending functionality by composing a type out of many types.
Composition naturally implies a semantic relationship synonymous to a has a relationship.
In that, composition is the semantically opposite of inheritance.
Composition does not enable polymorphism or covariance/contravariance.

Does graph depth matter?

Theoretically and if this makes perfect sense, we could continue to vertically extend the functionality infinitely. Usually this won't happen because we naturally reach the maximum level of specialization very early. At this point we usually only extend the superclass/parent horizontally by adding specialized siblings that are usually the various implementations of the (abstract) parent.

Same applies to composition: if we have complex data structures and complex logic and many responsibilities we naturally end up with a very deep dependency graph aka object graph. This is absolutely fine and even encouraged, especially when following fundamental OO design principles like Single Responsibility or Law of Demeter.

It's important to understand that due to the nature of composition it is very likely that composition results in very deep dependency graphs.

As a conclusion: depth of class hierarchy is a natural product of applying advanced class design principles and design patterns. A deep hierarchy is generally synonymous for a complex application with complex functionality.
The best examples for this kind of complexity are very advanced high-level application frameworks.

An application framework has to provide a high-level container that the client application can run in without having to deal with OS level details.
It provides the interface to the low-level OS functionality. For example, OS level input message loops are converted to high-level mouse or keyboard events.
For managed languages (if the language uses a garbage collector to manage the application memory), this means an extra level in the hierarchy for converting unmanaged OS level APIs or functionality to managed high-level framework APIs.
UI objects like a button are high-level objects that depend on low-level infrastructure like a rendering engine.

All those low-level details are hidden from the framework API (or client). This fundamental functionality is usually encapsulated in classes that are part of a class hierarchy. With application frameworks, before the hierarchy reaches the first high-level class we are usually already five levels deep down in the inheritance tree.

Deep inheritance hierarchies are commonly found in very complex applications or frameworks, but are more rare in libraries.

However, because of the high complexity that goes in tandem with deep inheritance trees, deep hierarchies come with complex maintainability issues as more inheritance usually means more reused functionality, which in turn means a higher probability of bugs. Also the probability of classes inheriting functionality that they don't need increases. At the same time a deeper tree means more reused functionality, which is a good thing.

For example, the general guideline, that is also implemented with Microsoft's code metric analyzer, is to try to avoid exceeding an inheritance depth of 6. This number is only a guide and should help to prevent excessive inheritance. As mentioned before, complex code base requires complex class design. But unless you are building a framework your type hierarchy naturally won't exceed a depth of 6. Studies were not able to determine a concrete number that proofs to be generally optimal.

For composition there is no such constraint. From an inheritance perspective, composition always has a depth of 1. The only depth that matters in the context of composition is the depth of the dependency graph. However, the same applies to inheritance too. Usually class inheritance involves composition (but composition does not involve inheritance). And the same can be said about the depth of the dependency tree: the more complex the application becomes the more dependencies a type will have (tree width) and because each type comes with its own dependencies the deeper the tree will become.

Is flat really good?

The opposite design, that is commonly considered bad code or a code smell, would be to keep the hierarchy or graph depth flat e.g. depth of 1, by moving all the application's code into a single class. It can't get any flatter. This shows how we can improve code by adding more levels to the hierarchy/dependency graph: we encapsulate and we hide implementation details and decouple dependencies and eliminate duplicate code and improve reusability.

When use composition and when inheritance?

The problem is not inheritance. Likewise, composition is not the hammer that fits every problem.
The idea was never to replace inheritance with composition.
The problem is that most, especially beginners only see inheritance. And there are cases where inheritance is semantically wrong (same applies to composition).
For example, a House that inherits from Basement, Stairway and Attic is a semantically wrong construct that will lead to bad design and inconvenient usage of the type. Inheritance is always a is a relationship that clearly doesn't apply to a complex object like a house and its components.

The reason why composition is considered to feel more natural is the fact that most of the object in our world are compositions of other objects.

A football does not inherit from cow to get the leather surface. It's a composition of leather, air and glue. Where leather itself is a composition, glue is a composition and air is a composition. The composition tree gets deeper and deeper when going down to an atomic level. This big depth is natural to composed objects.

In this sense, a House is more intuitive to construct and to use when using composition (i.e. a has a relationship): a House is a composition of a Basement, a Stairway and an Attic.

You may introduce more specialized houses like a Warehouse or Hotel by using specialization aka inheritance like Hotel inherits from House. Now the semantics are correct because a Hotel is a House. But a House is not a Stairway. A House has a Stairway.

This example shows that because of the semantic differences between inheritance and composition that they are not interchangeable. Although it is technically possible semantics add a very strict constraint.

Another indicator when to favor composition over inheritance is when we have to violate the Liskov substitution principle (LSP). If inheritance leads to overriding inherited members in a way that changes the base functionality we must use composition instead (or at least refrain form inheritance or adjust the class design).

Important: inheritance and composition are not mutual exclusive. We can combine both. A class that is a member of an inheritance tree can itself use composition to implement its functionality. In fact, this is very common practice. For example, while Warehouse inherits from House, Warehouse can be composed of Crane and LoadingRamp.

Bottom line

So if the implied logic of the question is: "because deep inheritance trees are bad ==> deep composition graphs must be bad too", then we can surely say the the premise or hypothesis is already wrong and thus the logical conclusion must be wrong too.

The tree depth in both cases, inheritance and composition, is absolutely irrelevant in terms of functionality, design or compilation. We can assume that a deeper inheritance tree means a more complex application and more reusable code. Composition is not affected in this terms. In fact, comparing tree depth of inheritance to composition we observe that composition always has a depth of 1.
The dependency graph is another topic that affects both equally, inheritance and composition.

As complexity grows, so does the depth of the inheritance tree or dependency graph. Complexity and depth are coupled. Complexity means, implementation of advanced OO principles and design patterns and an increased number of responsibilities (at application and class level). It means an improved reusability. Complexity means more error prone code. Deeper graphs tend to provide functionality to subclasses that they don't need. If not carefully designed, the inheritance tree can clutter the API of subclasses with unnecessary and unwanted members. A deep inheritance tree is good when inheritance is used with care and purpose and not in an excessive way. Inheritance is usually not the right solution. For most cases composition is more relevant.

As the example with the House and its has a relationship with its components Basement, Stairway and Attic and the is a relationship between a House and a Warehouse has shown, both inheritance and composition are equally important and can't be replaced by one or the other.
They are not interchangeable because they are semantically different..

What is relevant is the hierarchy itself (from an abstract perspective of software architecture).

For example,
when considering inheritance or reviewing the implemented inheritance tree (if answered with "yes" we are good):

  • Does it make semantically sense?
  • Does it not violate other principles like the LSP principle?
  • Does the level or degree of segregation makes sense in regards of the inheritance tree's depth (i.e. have we spread common functionality across too many classes)?
  • Do we really create a more specialized type and we are not just creating a new (unrelated) type? (falls into the same category like violating the LSP)

when considering composition or reviewing the implemented dependency graph (if answered with "yes" we are good):

  • Does it make semantically sense (e.g. doe we really have a has a relationship or rather an is a relationship)?
  • Are we really not creating a more specialized type of the type we use to extend the functionality?
  • Am I sure that I don't have to extend the functionality of the currently composed type?

We always can and usually have to combine inheritance with composition. A superclass or subclass can always use composition to implement functionality. Inheritance and composition are not mutual exclusive.

0

Yes, you can model it like relational tables

Result {
    Id
    ModelId
    Epoch
}

etc. which can often lead to easier programming and simple mapping to a database. but at the expense of losing the hard coding of your relationships

0

I don't really know how to solve this in Java, because Java is built around the idea that everything is an object. You can't suppress intermediate classes by replacing them by a dictionnary because the types in your data blobs are heterogenous (such as a timestamp + list, or string + list...). The alternative of replacing a container class with its field (e.g. pass along a "m_epoch" variable with a "m_modules") is simply bad, because it is making an implicit object (you can't have one without the other) with no particular benefit.

In my predilection language (Python), my rule of the thumb would be to not create an object if no particular logic (except basic getting and setting) belongs to it. But given your constraints, I think the composition hierarchy you have is the best possible design.

0

Look at your Model. It has some stuff that wasn't mentioned, and it has an array of Result. The "Law of Demeter" asks you to make sure that Result can answer all the questions that Model might have. If it does that then it doesn't matter one bit what the implementation of Result is, and whether it has a 117 level deep nested composition hierarchy. It's not Model's problem. It may be Result's problem, but Result itself only cares about one level again.

Now if you are a developer using Model, then Model should be able to answer all the questions that you have. You should not have to care about its composition hierarchy. Now maybe you have the question "give me a set of all your Result objects". If that is your question, and Model needs to answer it, then by coincidence it's not quite trivial: Model needs a method getSetOfResults, and that method can take the array of Result and create a set from it. But as the caller, you don't care how Model did that.

Let's take another example: You have a Stage and you want to know the report text for that stage. And the Module is supposed to know it. So you ask the Module "tell me, what is the report text for this stage?" Now the Model iterates through its Result array (the Model knows that there is a Result array) and asks each Result in turn "tell me, what is the report text for this stage?" So your code doesn't know about the Result array, the Model code doesn't know about the implementation of Result, and Result implements this method by looking up the ExperimentModule for the Stage, and the ExperimentModule can tell it its report text.

So there is a hierarchy, but everyone only cares about the one level of the hierarchy that is directly visible to them. The bad alternative would be that your code, which only has a "Stage" and wants a string, would have to know about all this hierarchy.

Of course you don't have to split up everything to this extreme. Maybe your code knows about ExperimentModules and is happy if the Model gives it the ExperimentModules for the Stage and goes from there. The important bit is that you reduce the amount of hierarchy you need to know to a manageable amount. If two levels is acceptable, then fine. Better than 117 levels.

PS. Your Model has an array of Result. If you wanted to inherit from, what would that be? Not Result. And certainly not Array, I hope.

0

Short answer: No. Deep composition hierarchies are not bad, as long as you otherwise follow good design like encapsulation and separation of concerns.

The warnings against deep inheritance hierarchies are because they create a tight coupling between an ancestor and all descendants. A small change to the implementation of a class might affect the behavior of all its descendants, however deep in the hierarchy. But this is a particular problem of implementation inheritance, not a general problem of any hierarchy.

Composition hierarchies (more commonly called composition trees or graphs) do not have this problem - a change in a component will at worst affect its immediate connections, but not components elsewhere in the hierarchy.

Of course, a simple object model is preferable to a complex model, all other things being equal. But the model cannot be simpler than what is necessary to solve the problem - and sometimes we need to solve complex problems. A browser DOM or a compiler AST are examples of composition trees that can have thousands of nodes in deep hierarchies, without this being inherently problematic.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.