19

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?

  • Don't understand the external context part. – Tulains Córdova Nov 4 '16 at 19:43
  • @TulainsCórdova what I'm trying to ask here is "are there situations for which the deep composition is a good solution"? – AwesomeSauce Nov 4 '16 at 19:46
  • I added an answer. – Tulains Córdova Nov 4 '16 at 19:55
  • 5
    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. – Mason Wheeler Nov 4 '16 at 19:57
  • 3
    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 '16 at 21:29
17

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.

  • 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. – AwesomeSauce Nov 4 '16 at 20:27
7

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.

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

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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