Software Engineering bits: composition vs inheritance

Alberto Sanz
adidoescode
Published in
10 min readMay 31, 2024

Inheritance is one of the most important concepts of Object-Oriented Programming (OOP). It allows us to create new classes that inherit the attributes and methods of existing classes. This way, we can reuse code, avoid duplication, and enhance the readability and maintainability of our programs. Inheritance also enables polymorphism, which means that we can use different subclasses in the same way as their parent class. It is a powerful tool that helps us design modular and scalable software systems.

Just with this information, you should be motivated to use inheritance as much as possible in your codebases. Why not, if all those benefits are undoubtable?

I’m not here to tell you not to use inheritance. I’m here to tell you not to abuse inheritance.

Note: I’m using Java here with a pseudo-Spring Web framework. You don’t need to know much about how they work as the code is pretty much self-explanatory.

Typical movie hackers like you and me. Photo by Zanyar Ibrahim on Unsplash

Abusing inheritance

Coding time!

Let’s assume we have a backend application that exposes an endpoint for retrieving our users tasks:

@RestController
public class TasksController {

...

@GetEndpoint("/tasks")
public List<Task> getTasks(String jwtToken) {
var currentUser = getUserFromToken(jwtToken);
return tasksService.tasksForUser(currentUser);
}

private User getUserFromToken(String jwtToken) {
// some code for getting the user based on the token
...
}

}

Nice and clean! We get the current user, validate some tokens, and perform some business validation regarding such user.

Let’s add a new endpoint so users can recover their lists of tasks:

@RestController
public class ListsController {

...

@GetEndpoint("/lists")
public List<List> getLists(String jwtToken) {
var currentUser = getUserFromToken(jwtToken);
return listsService.tasksForUser(currentUser);
}

private User getUserFromToken(String jwtToken) {
// SAME code for getting the user based on the token
...
}

}

mmm

Yep, nice, clean… If we just consider those classes isolated. But as soon as we have both controllers in our codebase, it starts smelling.

But hey! We’re super skilled OOP programmers! Inheritance to the rescue!

Masters of coding. Photo by Thao LEE on Unsplash

Applying inheritance to increase maintainability

Looks like both our controllers need to perform some common logic regarding the user authentication. Let’s create a BaseController class for doing such tasks:

public class BaseController {

protected User getUserFromToken(String jwtToken) {
// same logic as before
...
}

}

@RestController
public class TasksController extends BaseController {

@GetEndpoint("/tasks")
public List<Task> getTasks(String jwtToken) {
// getUserToken is available from super!
var currentUser = getUserFromToken(jwtToken);
return tasksService.tasksForUser(currentUser);
}

}

@RestController
public class ListsController extends BaseController {

@GetMapping("/lists")
public List<TaskList> getLists() {
// getUserToken is available from super!
var currentUser = getUserFromToken(jwtToken);
return listsService.listsForUser(currentUser);
}

}

Nice and clean again! Since both controllers share some common logic, we define them as children of a common BaseController with that shared logic. We can even leverage such base class for adding some more handy methods, and even feel the superior seniority by adding generics:

public class BaseController<T> {

protected User getUserFromToken(String jwtToken) { ... }

protected ServerResponse hateoasReponseFor(T response) { ... }

protected ServerResponse handleException(Exception e) {
/* please don't use advices for this */
...
}

protected void limitToPremiumUsers(User user) { ... }

}

@RestController
public class TasksController extends BaseController<Tasks> {

@GetEndpoint("/tasks")
public ServerResponse getTasks(String jwtToken) {
var currentUser = getUserFromToken(jwtToken);
try {
var tasks = tasksService.tasksForUser(currentUser);
return hateoasReponseFor(tasks);
} catch (Exception e) {
return handleException(e);
}
}

}

@RestController
public class ListsController extends BaseController<Lists> {

@GetMapping("/lists")
public List<TaskList> getLists() {
var currentUser = getUserFromToken(jwtToken);
try {
limitToPremiumUsers(currentUser);
var lists = listsService.listsForUser(currentUser);
return hateoasReponseFor(lists);
} catch (Exception e) {
return handleException(e);
}
}

}

We’re reusing code! Less maintainability! GREAT!

Trve coding skills! Seniority flowing through my fingers!

Not so great

Stop.

Think.

What is inheritance?

Pause the reading for five seconds and think about it. Then proceed.

What is not inheritance?

Pause the reading for a bit more than five seconds and think again. This is a bit more complex.

Helpful thinking music

Think in a common use case for inheritance. One that was probably explained to you when you were learning OOP. Or, for setting some common ground for everyone, let’s see an example:

public class Animal {

public void makeSound() { ... }

}

public class Cat extends Animal { ... }
public class Dog extends Animal { ... }
public class Duck extends Animal { ... }
public class Fish extends Animal { ... }

Looks good. We know animals make sounds (fishes do “blop”), and cats, dogs et al are all animals. Make sense to apply inheritance here. This is a powerful and flexible model that allows us to define true “A is a subtype of B” relationship. We can add behaviour to all our Animals e.g. move(...), play(...), eat(...). It make sense that all Animals have that behaviour, although each one of them can override it.

Let’s review our previous example. We have a new requisite for adding some management related endpoints (of course, in a different port not exposed to the public). Exceptions should be handled, so we need the BaseController:

@RestController
public class ManagementController extends BaseController<???> {

...

@GetEndpoint("/manage-stuff")
public ServerResponse manageStuff(...) {
try {
return ...
} catch (Exception e) {
return handleException(e);
}
}

}

Wait.

BaseController... Of what?

Our seniority was a lie!

We’re forced now to specify a class for the generic BaseController, even though it's not applicable to our usecase here

I’m hearing a trve senior claiming a class ManagementController extends BaseController<Object> solution in the distance...

Do you really like that solution?

No. Really. Do you like it? 🤨

Even worse

Oooopppps

Remember our getUserFromToken method above? Well... We forgot to validate the token. 🤷

No problem. We have that logic centralised in our BaseController class. We have to change it only in one place. Mini-point for our seniority! 🥹

Let's add some dependency that does that validation:


public class BaseController<T> {

private final ValidateToken validateToken;

public BaseController<T>(ValidateToken validateToken) {
this.validateToken = validateToken;
}

protected User getUserFromToken(String jwtToken) {
validateToken.doYourThing(jwtToken);
// rest of code unchanged
}

// same as before
protected ServerResponse hateoasReponseFor(T response) { ... }

protected ServerResponse handleException(Exception e) { ... }

protected void limitToPremiumUsers(User user) { ... }

}

Success!

Until… Not. It doesn’t compile.

Did you notice? You added a new argument to the constructor. Children classes need to be updated. Focusing on the one we’re interested in…

@RestController
public class ManagementController extends BaseController<Object> {

@Autowired
public ManagementController(
/* not really using it but OK */
ValidateToken validateToken
) {
super(validateToken);
}

@GetEndpoint("/manage-stuff")
public ServerResponse manageStuff(...) {
try {
return ...
} catch (Exception e) {
return handleException(e);
}
}

}

Congratulations! You now have a coupled codebase. Your controllers now have unneeded dependencies, unneeded features, and coupled lifecycle.

But hey! Reduced code 🥳

By the way, we’re introducing a handy new library that forces us to use their own BaseController, so now we have to get rid of our own 🥲

Inheritance has betrayed me. Save me, white coding wizard!

Composition to the rescue

Again, I’m not here to discourage you from using inheritance. But we’ve just seen an example of how easy we can abuse that feature, resulting in overcomplicated code. Inheritance is a strong “A is B” relationship, while composition is a “A can leverage B” relationship. Instead of giving A the same behaviour, we delegate such behaviour to specialised classes. Then we can specify which behaviour we need.

Applying it to our example:

@Service
public class GetUserFromToken {

private final ValidateToken validateToken;

public GetUserFromToken(ValidateToken validateToken) {
this.validateToken = validateToken;
}

public User from(String jwtToken) { ... }

}

@Service
public class BuildHateoasResponse<T> {

public ServerResponse for(T response) { ... }

}

@Service
public class ExceptionHandler {

public ServerResponse handle(Exception e) {
/* please don't use advices for this */
...
}

}

@Service
public class LimitToPremiumUsers {

public void check(User user) { ... }

}

Below is a small and simple way to manage easy-to-maintain classes that can be used wherever they’re needed:

@RestController
public class TasksController {

private final GetUserFromToken getUser;
private final BuildHateoasResponse buildResponse;
private final ExceptionHandler exceptionHandler;

...

@GetEndpoint("/tasks")
public ServerResponse getTasks(String jwtToken) {
var currentUser = getUser.from(jwtToken);
try {
var tasks = tasksService.tasksForUser(currentUser);
return buildResponse.for(tasks);
} catch (Exception e) {
return exceptionHandler.handle(e);
}
}

}

@RestController
public class ListsController {

private final GetUserFromToken getUser;
private final BuildHateoasResponse buildResponse;
private final ExceptionHandler exceptionHandler;
private final LimitToPremiumUsers limitToPremiumUsers;

...

@GetEndpoint("/lists")
public ServerResponse getLists(String jwtToken) {
var currentUser = getUser.from(jwtToken);
try {
limitToPremiumUsers.check(currentUser);
var tasks = tasksService.tasksForUser(currentUser);
return buildResponse.for(tasks);
} catch (Exception e) {
return exceptionHandler.handle(e);
}
}

}

@RestController
public class ManagementController {

private final ExceptionHandler exceptionHandler;


@GetEndpoint("/manage-stuff")
public ServerResponse manageStuff(...) {
try {
return ...
} catch (Exception e) {
return exceptionHandler.handle(e);
}
}

}

Yes, we’re adding a few extra attributes to our controllers. Yes, we’re adding dependencies that helps us doing what we need to do, decoupling such behaviour. Yes, we make them final so we do not reassigned by mistake and let the compiler warn us when we forget to added to the constructor. Yes, we let the IDE fix that for us.

Yes.

We’re investing 10 extra seconds now so we avoid 10 hours of work tomorrow.

Crazy idea, eh?

It’s the end of the world as we know it (and I feel fine)

As a side note, we’re applying the S from SOLID here (food for thought: why am I claiming that? 😉)

Bonus point: composition in other languages

Not gonna lie, sometimes it’s quite uncomfortable to implement composition. When you need to delegate a lot of tasks to one of your dependencies and expose the same API, it’s… far from ideal. For example, imagine you need a class that implements two interfaces and want to apply composition so you delegate each implementation to specific classes:

// For the sake of the example, let's forget about Interface Segregation :S
interface Foo {
void foo1();
void foo2();
void foo3();
}

interface Bar {
void bar1();
void bar2();
void bar3();
}

This is how you’d do it in Java:

class FooBar implements Foo, Bar {
private final Foo foo;
private final Bar bar;

FooBar(Foo foo, Bar bar) {
this.foo = foo;
this.bar = bar;
}


@Override
public void foo1() {
foo.foo1();
}

@Override
public void foo2() {
foo.foo2();
}

@Override
public void foo3() {
foo.foo3();
}

@Override
public void bar1() {
bar.bar1();
}

@Override
public void bar2() {
bar.bar2();
}

@Override
public void bar3() {
bar.bar3();
}
}
No maintainability and no conciseness make Homer go brum brum

Personal point of view here: perfect example of famous Java verbosity. Perfect example as well of discouraging people from using composition. Note, however, that we can’t inherit from multiple classes, so applying inheritance here comes with its own drawbacks!

I wish Java would integrate some features so it’s easier to do. For example, this is the equivalent code in Kotlin:

interface Foo {
fun foo1()
fun foo2()
fun foo3()
}

interface Bar {
fun bar1()
fun bar2()
fun bar3()
}

class FooBar(
foo: Foo,
bar: Bar
) : Foo by foo, // implement Foo interface, delegate invocation of such methods to foo object
Bar by bar { // implement Bar interface, delegate invocation of such methods to bar object
// no need even to store a reference to foo or bar
// this.foo is an unresolved reference
// less things to manage!

// Since we have no body, we could even remove the { }
}

As a “pure Java” alternative, you can use @Delegate annotation from Project Lombok. However, I recommend you researching on your own about how Lombok does what it does and then thinking: do you really want such kind of behaviour in your project? Stay tuned if you want to know more about Lombok!

Concluding…

At some point during the reading, you may have thought “Wow, this guys really hates inheritance”. No, I don’t. But I see close to no value in applying it to classes that are just behaviour (Behaviour? State? Remember those pillars of OOP?) Inheritance should only be applied in those scenarios in which there really is an inheritance relationship, not just as a way of reusing code. This article showcases a hand-picked example in which going for what we could think is a good approach (DRY, that’s where inheritance shines) leads to painful maintainability. Experienced readers may claim that I’m not saying anything new and all of this is widely known in the industry (they’re right!) but we all have written “not-so-great” solutions, that makes us wish to delete and rewrite them from scratch. Not every reader has fought with their own codebase, trying to tame the beasts they created long ago (personal definition of “long ago” = “yesterday”). So next time you’re about to write some inheritance ask yourself these questions: does this class really inherits this other class? Or am I doing it just so it can use its code?

Of course, share your new knowledge! Make those same questions whenever you review your peers code and you have doubts about it!

Derek says it’s always good to end a paper with a quote. He says someone else has already said it best. So if you can’t top it, steal from them and go out strong. So I picked a woman I thought you’d like:

L in SOLID

Or, translated to us, vulgar displays of ignorance:

If it looks like a duck and quacks like a duck but it needs batteries, you probably have the wrong abstraction

Stay tuned for more Software Engineering Bits! Let us know what do you think of the topic (or even if you hate me, it’s fine to shout to the screen from time to time 🌚)

The views, thoughts, and opinions expressed in the text belong solely to the author, and do not represent the opinion, strategy or goals of the author’s employer, organization, committee or any other group or individual.

--

--