6

One of the basic precepts of object oriented programming is that subtyping represents the "is-a" relationship. That is, the child is always a specific form of the parent. A common example is that a square is a specific form of rectangle, so it seems to make sense to make a Square class inherit from a Rectangle class. (read here why that doesn't actually make sense - thanks @KevinKrumwiede for pointing this out)

Occasionally, one encounters a situation where subtyping is a convenient way to write a different relationship.

The one I currently face is relatively simple. I'm controlling a simple mechanical system B, which contains subsystem A. Thus, in meatspace, we see a very literal "has-a" relationship: System B has a System A.

Writing the code as a close analogy to reality, I wound up with something like the following: (Shown in Python for brevity. Actual code is in C++.)

class A:
    def m1(self): print "A.m1"
    def m2(self): print "A.m2"
    def m3(self, arg): print "A.m3(" + arg + ")"

class B:
    def __init__(self): self.a = A()
    def m1(self): a.m1()
    def m2(self): a.m2()
    def m3(self): print "B.m3"
    def m4(self): a.m3("B.m4")

While class B contributes useful functionality (in the real life version), a bunch of its methods are simple pass-throughs. Class B just doesn't have anything useful to add to m1() or m2() - it just needs to expose them to the owners of B objects.

If we throw out the precept that inheritance should represent an "is-a" relationship, we could simplify things:

class A:
    def m1(self): print "A.m1"
    def m2(self): print "A.m2"
    def m3(self, arg): print "A.m3(" + arg + ")"

class B(A):
    def __init__(self): pass
    def m3(self): print "B.m3"
    def m4(self): a.m3("B.m4")

Now all methods of A are exposed as if they belong to B. B can (and does) override methods of A as needed. The maintainer of B doesn't need to update B every time that A adds a new method - passthroughs are free.

This feels wrong somehow, but I can't fully figure out why. Is this an abuse of the technicalities of the inheritance mechanism? Is it ever appropriate to represent a "has-a" relationship this way? B is not a more specific form of A, but we've subclassed it as if it was.

The only practical drawback I can think of is that all of A's methods are now available for calling, and B has little to no way to prevent this. In a language like Python, there's no concept of private methods, so that doesn't matter. In a language like C++ it might. Do other drawbacks exist?


Edit: To all who find this question in the future, be sure to read this answer as well as the accepted answer. Both list noteworthy drawbacks to using inheritance to accomplish composition.

6
  • 1
    tree has-a subtree but sub-tree in turn is-a tree. Totally appropriate, composite pattern
    – gnat
    Commented Apr 27, 2016 at 13:11
  • 1
    Squares and rectangles are a classic bad example. See When a square is not a rectangle. Commented Apr 27, 2016 at 13:49
  • @KevinKrumwiede, I stand enlightened Commented Apr 27, 2016 at 13:53
  • @gnat: That makes sense; in the case of trees and subtrees, it's conceptually recursive. Do you feel this still applies if the mechanical analog isn't quite so recursive? In my situation, the subclass is very much not the parent class. It's more like a desklamp has a lightbulb, but when you turn off the lamp, you're turning off the bulb. Commented Apr 27, 2016 at 13:56
  • 1
    A link in lieu of an answer because I'm lazy: en.wikipedia.org/wiki/Composition_over_inheritance Commented Apr 27, 2016 at 14:01

3 Answers 3

6

If your composition situation is simple enough, you can use inheritance instead.

Not recommended, because it would be confusing to others, and also will require refactoring as soon as the composition situation becomes less simple.

Composition allows a number of things. Because the classes are separate, each can have their own inheritance hierarchy. So using composition, a B can have a has-a relationship with any A instance, including those whose class is a subclass from A.

Composition allows an existing A to be referred to by a new B. It allows multiple B's to refer to a single A. Further, composition potentially also allows changing which A that B has-a at runtime, whereas inheritance means you get a new A with a new B (period).

Also, for languages that differentiate between classes and interfaces, composition allows a has-a relationship with an interface whereas inheritance requires a known, fixed class to be specified as the base. For these reasons (perhaps and more), composition is looser coupling than inheritance, and being able to use interfaces further reinforces looser coupling, which reduces maintenance burdens.

If you don't use any of those features, you could use inheritance to bundle an A with a B. But you will never be able to pick a subclass of A to relate to B, so if A is involved in subclassing you won't be able to choose among them for B. You'll have to duplicate B into new class B' that inherits from A'. If you did that kind of duplication, it would be a code smell.

You also wouldn't be able to choose an existing A for B's has-a. You'd always be creating a new A along with a new B.

Inheritance allows virtual methods and overrides by subclasses, and passing subclass around when a base class or beyond is expected. If you inherit and don't do any virtual methods or overrides, or polymorphism, that seems like it would also be a code smell.

Note that C++ allows embedding of an instances as another mechanism to bundle an A within a B, without using inheritance; you'll have the same restrictions on not being able to choose a subclass of A without using a B', but at least you're not confusing other maintainers with inheritance otherwise unused. Other languages (Java,C#) use references, so embedding is not an option: it's either inheritance or composition.

3

The convenience of inheritance in this example is reducing the amount of boilerplate code you have to write, but this falls under code reuse rather than behavioral reuse. By itself, that isn't an argument against using inheritance but rather that code reuse isn't a sufficient argument for inheritance.

Inheritance has significance and consequences beyond reducing code repetition. When the consequences aren't intended, issues can arise. Issues with using inheritance for a "has-a" relationship include:

  • it violates encapsulation (as you mention in the question)
  • it couples the classes tighter than necessary, which has far reaching consequences
  • coupling can unintentionally create overridden methods, which can produce different bugs:
    • in the child, the derived method doesn't call the base when it should (can be caught in unit tests & fixed)
    • in parent methods, calls to the overriden method may instead call the derived method when they shouldn't
  • coupling can also unintentionally create overridden fields, which, combined with encapsulation violation, can cause bugs
  • it allows child instances to be used wherever the parent can be used. This may be desirable behavior or it may be a source of bugs. Does it make sense for a function taking a subsystem to instead receive the mechanical system?
  • it affects how the type hierarchy can be altered and the classes extended

If you don't want to explicitly write forwarding methods, you can sometimes use language features to forward undefined method calls to another object (the functional part of the (Object) Proxy Pattern). This may still violate access restrictions, but largely avoids other issues.

In Python, you can use __getattr__, __setattr__ and __delattr__ (there are other special methods that may be useful or necessary); these also give you the opportunity to implement access restriction, as unpythonic as that is. Implement the Proxy Pattern in its own class, and you can add the functionality to most any other class you want with less than 2 lines of code.

In C++, you can overload operator->, but at the cost of unrestricted access and you must use the arrow operator rather than the dot operator to call methods (which is less coupling than inheritance but slightly more than explicitly writing each forwarding method).

1

Your goal in both cases is the same: extending B's behviour. This could either be done if B uses A or if B inherits from A.

Usage could be done in three ways:

a) Aggregation

b) Composition

c) B makes temporary use of A for doing stuff (think of A as part of a message sent to B).

Both of your implementations differ semantically. The second implementation makes only sense in polymorphic context. Think of decorators, where you want to substitute B for A with an alternative behaviour. B extends As behaviour. But besides from that, either implementation is fine.

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.