Technical Practice: No Inheritance

I've written before about the technical practice of composition over inheritance but what about the idea that we have NO inheritance?

I use inheritance VERY infrequently.
I have two cases where I use inheritance.

  1. Only when there is clear duplication between classes. I'd have to copy and paste the same code, and then go change it in all the classes.
  2. When there is raw data with C#. I avoid Primitive Obsession which is helped by my Fluent Types. Any classes that extend the fluent functionality uses inheritance. Though I'm starting to better represent objects instead of passing WrappedPrimitives around. C# is called out because implicit cast operators can't be on interfaces (though... C# 8 might allow something...) so I must have inheritance for that.

These two places are where I find inheritance. Which I think is a more maintainable way to work with the code. It makes the code vastly easier to refactor when there's not class dependencies that also need to change to stay green. Test coverage is the only way to safely refactor.

Even with the limited places I use inheritance... can I do better? How could I change the code so that the common behavior of the base class is instead a new object that acts as a collaborator?

One of the things that got me thinking about this was Yegor's Elegant Object Programming Language. Yegor and I seem to be on the same page for a lot of things, not sure I'd ever use the language, but I'm going to explore the ideas and write my own thoughts about them.

Remove Inheritance

I got the opportunity to play with replacing inheritance with collaborators at the end of December. It was amazing. It exposed high coupling that I didn't see before. It enabled breaking apart tightly coupled behaviors into their own objects. A lot of what bothered me about the abstract class was removable when it stopped being abstract.

Abstract classes exist to share behavior while adding customized behavior - Behavior belongs in a collaborator. Take the shared, make it an object, interact with that object.

In a project for work, I had some abstract classes. And I was undertaking a MASSIVE refactor. Not rewrite; I was step by step refactoring.
The abstract classes (and derived) made it REALLY hard to refactor that module. I did one, which required a lot of constructors and temp values to stay green and able to run the tests.

I decided to rewrite one of the usages of an abstract class as a class that took collaborators, composition over inheritance.

It was pretty quick to isolate the one abstract method into a class and pass it into the now sealed (C#) class.
Clearly the number of collaborators my former base class had would grow; and it did... by one. Which put it to 4(ish). This is too many collaborators. Three is the max collaborators I'll accept in a class. I have classes with four, that means I haven't extracted the cohesive collaborators yet. I don't HAVE to extract, but the only place I still make excuses is the edge of the system.
I've yet to write a class that requires more than three collaborators that doesn't have high cohesion between some pair. Or have multiple behaviors that can be refactored out.
That many collaborators in a class means it's doing too much.

I did the rewrite of the classes to see what it looked like with no inheritance. Once I got things re-implemented; I looked at it. Looked at these four collaborators; and could CLEARLY see refactorings and abstractions that want to exist. When I used an abstract class, it was hidden from me. The "collaborator" of a derived class doesn't show how things are actually interacting. It PREVENTS you from refactoring high cohesion when some of that cohesion is in the derived class.

Inerhitance hides code complexity.

Very literally, using inheritance was stopping me from seeing refactor opportunities. It hides behavior and high cohesion. It forces the base class to accumulate things that should be refactored into a new object to be used as a collaborator. It's hard to see though because they interact with the derived class, so they are "needed".

This is a minor thing for refactoring - being able to identify the things to refactor. :)

If you can't see cohesive collaborators then you can't refactor them out. It was there, just hidden by inheritance.
If the refactoring is hard because you have to modify multiple classes at once - you'll skip that refactor. It's harder and in my experience, it's not easy to see what could/should be refactored.
It took me hours to get rid of the abstract class using safe refactorings. That's after I did it quickly via a re-write first. What should be changed wasn't obvious from just the code. I took a couple hours to rewrite and refactor with a single derived class so I could see where the code wanted to go.

For quite a while now, I haven't been a fan of inheritance, the refactorings I went through at the end of December have given me a little more push.

Inheritance shouldn't be used.

Except... If all I'm doing is copy & pasting different collaborators; I'll use a base class as all that class is really doing is knowledge based.

Knowledge Class

public sealed Foo : IFoo{
    private readonly IFoo _foo;
    public Foo():this(new PreviouslyAbstractClass(new FooCollaboratorOne(), new FooAbstractMethodBehavior()));
    private Foo(IFoo previouslyAbstract) => _foo = previouslyAbstract;
    
    public void Bar() => _foo.Bar();
}

If I need to do the same thing for a Fizz and a Buzz it's

  1. Copy
  2. Paste
  3. Replace Foo w/Fizz
  4. Repeat for Buzz

Then, I'll create a base class. It does nothing but act as storage and delegation. I also do this because the tests for these three classes followe the same sequence of steps. There's nothing unique to making sure it delegates.

If new behavior arises; that changes the collaborators; not the storage and delegation components.

public abstreact class FooBar : IFoo{
    private readonly Foo _foo;
    protected FooBar(IFoo foo) => _foo = foo;
    public void Bar() = _foo.Bar();
}
public sealed Foo : FooBar{
    public Foo():base(new PreviouslyAbstractClass(new FooCollaboratorOne(), new FooAbstractMethodBehavior()));
}

It might still be copy and paste; but it's clearly a knowledge class, not a behavior class.
This distinction helps refactor in the future when it's clear it's not expected to do something different than any other implementor of FooBar.

When to inherit?

This brings me back to my list of times to inherit... It changed when I did my experimentation.
At the start of this post I said I inherit when

  1. Only when there is clear duplication between classes.
  2. When there is raw data with C#.

Because of my experimentation, that's no longer true. The first point is gone. We create a collaborator for that.
What my little sample above shows is that inheritance works for a knowledge class. Now I'll inherit only along these lines

  1. When a refactored collaborator becomes a passthrough.
  2. When implict casting to raw data

These are much more explicit situations. If I'm copying around some class fields and a shared delegation, toss that into the base class. All those things are tested for each class.
A HUGE red flag about duplication is when you can copy and paste unit tests. That means there's some shared behavior. This duplication can be extracted into a collaborator. That's what we should do when we find the same behavior across classes. If the collaborator becomes a pass through, then it should be an abstract class. Our FooBar above, if a collaborator would be a pass through. We have it abstract, which is what it should be. The field and delegation are shared, there's no pass through class.

The other time is for converting from a type to raw data. I use implicit casting, but any converstion to a raw type at the boundary of the system is likely to be acceptable. The boundary is key to acceptance - don't convert before the boundary (except dealing with bools... still working on how to avoid those w/o adding complexity).
Inheritance makes it the simplest way in C# to enable shared implicit casting.

This is why I started using the Fluent Type Text everywhere. It implicitly converted for me, and I didn't want to have to implement the operator string in a lot of classes. I've since changed my view on that.
I'm happy to have an abstract class SubscriptionName base and have a sealed class CurrentHourMonitorSubscriptionName : SubscriptionName implementation when the SubscriptionName has the implicit operator string. It's behavior in a lot of places, but it ensures Objects are interacted with, not data manipulated. This gets into why we wrap it in an object in the first place; we want an object, not a piece of data.

As should be clear by this tangent, I'm still working out some of the details on what I want to see in my code around this. :) Always exploring, always looking for better ways.

Show Comments