Why should I prefer composition over inheritance?

I always read that composition is to be preferred over inheritance. A blog post on unlike kinds, for example, advocates using composition over inheritance, but I can’t see how polymorphism is achieved.

But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation. How are you going to get polymorphism without inheritance?

Here is a concrete example where I use inheritance. How would this be changed to use composition, and what would I gain?

Class Shape
{
    string name;
  public:
    void getName();
    virtual void draw()=0;
}

Class Circle: public Shape
{
    void draw(/*draw circle*/);
}

17

Polymorphism does not necessarily imply inheritance. Often inheritance is used as an easy means to implement Polymorphic behaviour, because it is convenient to classify similar behaving objects as having entirely common root structure and behaviour. Think of all those car and dog code examples you’ve seen over the years.

But what about objects that aren’t the same. Modelling a car and a planet would be very different, and yet both might want to implement Move() behaviour.

Actually, you basically answered your own question when you said "But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation.". Common behaviour can be provided through interfaces and a behavioural composite.

As to which is better, the answer is somewhat subjective, and really comes down to how you want your system to work, what makes sense both contextually and architecturally, and how easy it will be to test and maintain.

5

Preferring composition isn’t just about polymorphism. Although that is part of it, and
you are right that (at least in nominally typed languages) what people really mean is “prefer a combination of composition and interface implementation.” But, the reasons to prefer composition (in many circumstances) are profound.

Polymorphism is about one thing behaving multiple ways. So, generics/templates are a “polymorphic” feature in so far as they allow a single piece of code to vary its behavior with types. In-fact, this type of polymorphism is really the best behaved and is generally referred to as parametric polymorphism because the variation is defined by a parameter.

Many languages provide a form of polymorphism called “overloading” or ad hoc polymorphism where multiple procedures with the same name are defined in an ad hoc manner, and where one is chosen by the language (perhaps the most specific). This is the least well behaved kind of polymorphism, since nothing connects the behavior of the two procedures except developed convention.

A third kind of polymorphism is subtype polymorphism. Here a procedure defined on a given type, can also work on a whole family of “subtypes” of that type. When you implement an interface or extend a class you are generally declaring your intention to create a subtype. True subtypes are governed by Liskov’s Substitution Principle, which says that if you can prove something about all objects in a supertype you can prove it about all instances in a subtype. Life gets dangerous though, since in languages like C++ and Java, people generally have unenforced, and often undocumented assumptions about classes which may or may not be true about their subclasses. That is, code is written as if more is provable than it really is, which produces a whole host of issues when you subtype carelessly.

Inheritance is actually independent of polymorphism. Given some thing “T” which has a reference to itself, inheritance happens when you create a new thing “S” from “T” replacing “T”s reference to itself with a reference to “S”. That definition is intentionally vague, since inheritance can happen in many situations, but the most common is subclassing an object which has the effect of replacing the this pointer called by virtual functions with the this pointer to the subtype.

Inheritance is a dangerous like all very powerful things inheritance has the power to cause havoc. For example, suppose you override a method when inheriting from some class: all is well and good until some other method of that class assumes the method you inherit to behave a certain way, after all that is how the author of the original class designed it. You can partially protect against this by declaring all methods called by another of your methods private or non-virtual (final), unless they are designed to be overridden. Even this though isn’t always good enough. Sometimes you might see something like this (in pseudo Java, hopefully readable to C++ and C# users)

interface UsefulThingsInterface {
    void doThings();
    void doMoreThings();
}

...

class WayOfDoingUsefulThings implements UsefulThingsInterface{
     private foo stuff;
     public final int getStuff();
     void doThings(){
       //modifies stuff, such that ...
       ...
     }
     ...
     void doMoreThings(){
       //ignores stuff
       ...
     }
 }

you think this is lovely, and have your own way of doing “things”, but you use inheritance to acquire the ability to do “moreThings”,

class MyUsefulThings extends WayOfDoingUsefulThings{
     void doThings {
        //my way
     }
}

And all is well and good. WayOfDoingUsefulThings was designed in such a way that replacing one method doesn’t change the semantics of any other… except wait, no it wasn’t. It just looks like it was, but doThings changed mutable state that mattered. So, even though it didn’t call any override-able functions,

 void dealWithStuff(WayOfDoingUsefulThings bar){
     bar.doThings()
     use(bar.getStuff());
 }

now does something different than expected when you pass it a MyUsefulThings. Whats worse, you might not even know that WayOfDoingUsefulThings made those promises. Maybe dealWithStuff comes from the same library as WayOfDoingUsefulThings and getStuff() isn’t even exported by the library (think of friend classes in C++). Worse still, you have defeated the static checks of the language without realizing it: dealWithStuff took a WayOfDoingUsefulThings just to make sure that it would have a getStuff() function that behaved a certain way.

Using composition

class MyUsefulThings implements UsefulThingsInterface{
     private way = new WayOfDoingUsefulThings()
     void doThings() {
        //my way
     }
     void doMoreThings() {
        this.way.doMoreThings();
     }
}

brings back static type safety. In general composition is easier to use and safer than inheritance when implementing subtyping. It also lets you override final methods which means that you should feel free to declare everything final/non-virtual except in interfaces the vast majority of the time.

In a better world languages would automatically insert the boilerplate with a delegation keyword. Most don’t, so a downside is bigger classes. Although, you can get your IDE to write the delegating instance for you.

Now, life isn’t just about polymorphism. You can don’t need to subtype all the time. The goal of polymorphism is generally code reuse but it isn’t the only way to achieve that goal. Often time, it makes sense to use composition, without subtype polymorphism, as a way of managing functionality.

Also, behavioral inheritance does have its uses. It is one of the most powerful ideas in computer science. Its just that, most of the time, good OOP applications can be written using only using interface inheritance and compositions. The two principles

  1. Ban inheritance or design for it
  2. Prefer composition

are a good guide for the reasons above, and don’t incur any substantial costs.

3

The reason people say this is that beginning OOP programmers, fresh out of their polymorphism-through-inheritance lectures, tend to write large classes with lots of polymorphic methods, and then somewhere down the road, they end up with an unmaintainable mess.

A typical example comes from the world of game development. Suppose you have a base class for all your game entities – the player’s spaceship, monsters, bullets, etc.; each entity type has its own subclass. The inheritance approach would use a few polymorphic methods, e.g. update_controls(), update_physics(), draw(), etc., and implement them for each subclass. However, this means you’re coupling unrelated functionality: it is irrelevant what an object looks like for moving it, and you don’t need to know anything about its AI to draw it. The compositional approach instead defines several base classes (or interfaces), e.g. EntityBrain (subclasses implement AI or player input), EntityPhysics (subclasses implement movement physics) and EntityPainter (subclasses take care of drawing), and a non-polymorphic class Entity that holds one instance of each. This way, you can combine any appearance with any physics model and any AI, and since you keep them separate, your code will be much cleaner too. Also, problems such as “I want a monster that looks like the balloon monster in level 1, but behaves like the crazy clown in level 15” go away: you simply take the suitable components and glue them together.

Note that the compositional approach still uses inheritance within each component; although ideally you’d use just interfaces and their implementations here.

“Separation of concerns” is the key phrase here: representing physics, implementing an AI, and drawing an entity, are three concerns, combining them into an entity is a fourth. With the compositional approach, each concern is modelled as one class.

4

You will see this cycle a lot in software development discourse:

  1. Some feature or pattern (let’s call it “Pattern X”) is discovered
    to be useful for some particular purpose. Blog posts are written extolling the virtues about Pattern X.

  2. The hype leads some people to think you should use Pattern X whenever possible.

  3. Other people get annoyed to see Pattern X used in contexts where
    it is not appropriate, and they write blog posts stating that you should
    not always use Pattern X and that it is harmful in some contexts.

  4. The backlash causes some people to believe Pattern X is always
    harmful and should never be used.

You will see this hype/backlash cycle happen with almost any feature from GOTO to patterns to SQL to NoSQL and to, yes, inheritance. The antidote is to always consider the context.

Having Circle descend from Shape is exactly how inheritance is supposed to be used in OO languages supporting inheritance.

The rule-of-thumb “prefer composition over inheritance” is really misleading without context. You should prefer inheritance when inheritance is more appropriate, but prefer composition when composition is more appropriate. The sentence is directed towards people at stage 2 in the hype cycle, who think inheritance should be used everywhere. But the cycle has moved on, and today it seems to cause some people to think inheritance is somehow bad in itself.

Think of it like a hammer versus a screwdriver. Should you prefer a screwdriver over a hammer? The question does not make sense. You should use the tool appropriate for the job, and it all depends on what task you need to get done.

The example you’ve given is one where inheritance is the natural choice. I don’t think anyone would claim that composition is always a better choice than inheritance — it’s just a guideline that means that it’s often better to assemble several relatively simple objects than to create lots of highly specialized objects.

Delegation is one example of a way to use composition instead of inheritance. Delegation lets you modify the behavior of a class without subclassing. Consider a class that provides a network connection, NetStream. It might be natural to subclass NetStream to implement a common network protocol, so you might come up with FTPStream and HTTPStream. But instead of creating a very specific HTTPStream subclass for a single purpose, say, UpdateMyWebServiceHTTPStream, it’s often better to use a plain old instance of HTTPStream along with a delegate that knows what to do with the data it receives from that object. One reason it’s better is that it avoids a proliferation of classes that have to be maintained but which you’ll never be able to reuse. Another reason is that the object that serves as the delegate can also be responsible for other things, such as managing the data received from the web service.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *