Jump to content
Edwin Yip

Prefer composition over inheritance for code reuse?

Recommended Posts

@Stefan Glienke

Even due to the lack of interface issue historically, the RTL/VCL is ok, because:

  • It's designed by talented people so the class hierarchy is well designed, and it's well documented.
  • We usually only use them from an external perspective and don't need to think about their internal relations. In case of designing our program, we do.
  • We couldn't change that fact :)

Interface is the BEST, but the 'composition over inheritance' approach still apply even if the language has no interface support.

For example, let's say we need a class to perform FTP upload, we have two options:

  • Inheritance - inherit from TIdFTP (in case of using Indy).
  • Composition - create a new TFtpUpload class and use a TIdFTP object inside.

With composition, the second approach, we'll gain the following benefits, as concluded by Nicolò Pignatelli:

  • isolated, side effect free code units; injecting interfaces only as dependencies will remove every nasty side effect around the code you are working on; changing an interface implementation won’t affect other concrete classes, since they depend only on the interface abstraction;
  • low, manageable complexity; as everything is isolated, you won’t need to worry about rippling changes; this dramatically decreases the complexity of your code;
  • low cognitive load; with decreased complexity, your brain will be free to focus on what matters;

Thank you for all the discussion, my thoughts on this issue is clearer now :)

Share this post


Link to post
On 4/1/2019 at 6:33 AM, edwinyzh said:

Interface is the BEST,

<sigh> I keep reading that, especially from Delphites, but ISTM that many totally misunderstood "program to the interface". 

 

Interfaces (no matter if they are actual language constructs or abstract classes) are fine and have their use, but like with everything, one can overdo it. Not everything is or should be an interface. The author of the article you link to seems to be one of those dependency injection, "interface only" gurus. ISTM that all these techniques should be part of a larger toolbox, not just a toolbox with lots of nails and a few hammers. I am not even sure if the VCL would have been better with interfaces, if they had been available.

 

And the claim "composition is better than inheritance" is just as wrong. Both have their place, and which one should be preferred depends on the situation.

  • Like 3

Share this post


Link to post

I dislike abstract classes because they still force you to inherit from a common base class instead of leaving you full freedom of implementation.  

 

 

Share this post


Link to post
On 3/31/2019 at 9:33 PM, edwinyzh said:

@Stefan Glienke

Interface is the BEST, but the 'composition over inheritance' approach still apply even if the language has no interface support.

For example, let's say we need a class to perform FTP upload, we have two options:

  • Inheritance - inherit from TIdFTP (in case of using Indy).
  • Composition - create a new TFtpUpload class and use a TIdFTP object inside.

Your example highlights the fallacy of your assertion.

 

In fact, inheritance locks you into whatever resources are required by the parent class! In this case, Indy.

 

FTP is a very generic thing. Why inherit from a specific concrete implementation that locks you into it's view of the world for ever and ever?

 

I think a far better approach would be to "inherit" from an abstract base class (a.k.a. "interface") that would allow the use of virtually ANYTHING that supports generic FTP features, including the one you happen to prefer today. But at that point, you're about 50/50 whether that approach is better than using composition (HAS-A) to include an API that offers the same abstract interface, possibly as a drop-on-the-form component.

 

Unless your goal is creating a bigger, better, faster, or more specialized version of the base class, then inheritance of a non-abstract base is silly. Without multiple inheritance, you're locked into specializing or expanding a single concrete base class anyway.

 

So if you're building a class that EMPLOYS FTP, for example, but IS NOT INTENDED to BE a "better" FTP service, then inheritance is clearly a very poor choice.

 

And inheriting from an abstract class presented as an Interface is simply a way of mixing-in a single type's namespace with your component's namespace. It's effectively just "anonymous composition" because you simply refer to the methods and properties as if they're part of your class, without having to refer to the name of the object containing them.

 

Finally, you can do dependency injection just as easily without interfaces.

Share this post


Link to post
14 hours ago, David Schwartz said:

you can do dependency injection just as easily without interfaces.

Yes, you can do - however using DI with interfaces has a unique benefit in Delphi - you don't need to care for who is responsible for cleaning up.

Leaving aside circular references and assuming a topdown object graph you can simply pass down dependencies even to more than one consumer and stuff gets cleaned up properly in the end.

Without that you need to track who is the one owning a dependency and responsible for destroying it - possible but more complicated.

 

In GC languages however having only one implementation for an interface (which you might want to do in Delphi because of the aforementioned reason) is considered a code smell by some - see https://blog.ploeh.dk/2010/12/02/Interfacesarenotabstractions/

Share this post


Link to post
11 hours ago, Stefan Glienke said:

Yes, you can do - however using DI with interfaces has a unique benefit in Delphi - you don't need to care for who is responsible for cleaning up.

 

That's certainly true. What I had in mind was DI performed through Constructor Injection as well as Getter/Setter (or Property) Injection, rather than the inheritance type of Interface usage.

 

Mark Seeman talks about the inevitable tendency to turn multiple parameters passed to a class via CI into a Parameter pattern that evolves to a standalone record/struct or class, so he suggests short-circuiting the inevitable by using a Facade pattern to pass one or more parameters through a separate object for CI. This could also be managed with an Interface.

 

Getter / Setter / Property injection might not fare so easily, unless you used this approach to pass in a Facade class with lots of values. Usually, they're one-to-one, however.

Edited by David Schwartz

Share this post


Link to post
On 4/2/2019 at 2:33 PM, A.M. Hoornweg said:

I dislike abstract classes because they still force you to inherit from a common base class instead of leaving you full freedom of implementation.  

 

 

The big advantage is that they don't need special handling (implementing methods) or care (like "don't mix interface and object references to the same instance"), that they really represent an IS-A relationship and that they can contain implementation, i.e. there is no need to first define an interface and then to implement it. Classes that really only do one thing (unfortunately, this is not always the case) can often use such inheritance.

 

Every descendant of TStrings IS-A TStrings. See how they are used throughout the VCL and RTL and how useful they have been over many many years. The fact that you are forced to inherit from a certain base class has its advantages.

Also think of, heheh, TInterfacedObject, from which almost every interface-implementing class must inherit.

 

Both interfaces, which allow you to program to the interface cross-hierarchy and abstract classes, which allow you to program polymorphically without the hassle of having to implement an interface and refcounting, have their davantages and disadvantages. Do not discard one of them. Use the kind that is best for the purpose.

 

Polymorphism is not limited to inheritance:ritance

  • inheritance polymorphism  ("subtyping")
  • interface polymorphism ("subtyping")
  • polymorphism through generics and constraints ("parametric polymorphism")
  • polymorphism through overloading and operator overloading ("ad hoc" polymorphism)

... I'm sure I forgot a kind here. And then there is code re-use, which can be done by composition (delegation, aggregation) or class inheritance.

 

Each of these techniques and practices should be used (if possible, i.e. if the language allows it) where they make sense. Do not make the mistake of favouring one over the other.

Edited by Rudy Velthuis

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×