Programming with Functions #10 — Composition over inheritance

Maciek Gorywoda
Nerd For Tech
Published in
13 min readFeb 8, 2022

The same article published on my blog

This topic is a bit on the verge of what we consider the scope of Functional Programming. You might have used both in writing OO code before. You might have also written functional code and used composition without even considering it. The reason why I put this topic here, as the last entry of my video series about functional programming, is that I associate its rise in popularity in recent years to the disillusionment in OOP and the general rise in popularity of FP concepts. I may be wrong in this. But… anyway.

Both inheritance and composition address the problem of how to extend existing code with new functionality. We already have some code, possibly from another module, and we want to write something that re-uses some parts of that code so that we don’t have to repeat ourselves or the other way around: we want to write something that can be used by that code, and only under the hood works a bit differently from the original implementation. Or maybe there was never an original implementation, to begin with, but only some abstractions that tell us how the implementation should be written so that it can be used by the rest of the code.

In practice, I’m talking here about abstract classes on the inheritance side versus interfaces or traits on the composition side. And as you can already notice in many programming languages you can have both: Java and Kotlin have classes and interfaces, Scala has classes and traits. A long time ago we could say that a trait is an interface which can implement some default functionality for its methods, and an interface can’t, but since then interfaces in Java and Kotlin also acquired the possibility to have default methods. In C++ they are even one and the same: an interface is simply an abstract class with no fields and all its methods abstract. On the other hand, Rust is completely on the composition side: we have data structures and traits that can be implemented for those structures. No classes at all, abstract or not. So, again, just like in the chapter about expressions vs statements, the difference comes down to how we decide to write our code and how that decision — hopefully made in the early stages of the development — affects how the code looks.

And, to be honest, it’s not even so much about which one we use, but rather how we use them. Even in the code written from scratch with composition in mind, we can sometimes find an abstract class, while interfaces or traits in inheritance-based code are pretty common. The difference, in my opinion at least, is mostly about whether we prefer deep hierarchies of classes where with each layer we have a new type which is built upon upon other types, or are we rather fans of more loosely linked, pretty much independent types that implement a bunch of interfaces so they can communicate with each other.

Ok, but what am I talking about? And where are the examples? Well… It won’t be easy to explain this topic with cats but I’ll try.

Inheritance: what the type is

There are still places in programming where deep hierarchies of inheritance are alive and kicking. One is GUI toolkits of widgets, like Android SDK, Swing or JavaFX in Java, or Qt in C++. Let’s take a look at a simple RadioButton from Android SDK.

Quoting the documentation: “A radio button is a two-states button that can be either checked or unchecked. When the radio button is unchecked, the user can press or click it to check it” [link]. RadioButton is a special case of CompoundButton which is a common parent class also for Checkbox, Toggle, and Switch — all of the buttons that mark somehow that they were clicked on. In turn, CompoundButton inherits from a regular Button that does not know how to do that and is in fact very similar to TextView from which it inherits. A TextView can display any kind of text (and can be clicked too — therefore in Android I rarely use Button, TextView is usually enough). In turn, it inherits from View which can be virtually anything that Android can display.

In short, every descendant is a more specialised version of its direct ancestor. With each level down we get some new functionality, but also the scope of the class gets more restricted. With each level up, we get something more generic; something that can be used in more ways. (Technically, it is possible to write hierarchies in the other direction: more specialised at the top and more generic at the bottom, but I wouldn’t recommend such design).

collections library. It’s more mixed here — you can see traits and classes together — and it’s generally more complex. If you’re interested in Scala I highly recommend you go through the documentation of Scala 2.13 collections. You don’t have to read and remember everything, of course, but it will give you a good idea of how to design class hierarchies and you will probably learn about methods you didn’t know existed.

the best way to go. It is used too much and too often in end-user applications, and the main reason why it is used is that we are used to using it. I remember my first contact with it in around 2001 when I was introduced to C++ at the university. After a semester of writing data structures in C, I was out of sudden creating those pyramids of abstract and concrete classes, re-using code and overwriting it. It was like adding new species to the evolutionary tree of my more and more complex code. I thought that, yes, this is advanced programming. I’m a professional now. So, for some time I used it everywhere, even if it was not necessary or if finding a way to connect two pieces of code by inheritance was very unintuitive.

Imagine, for example, a Kitten… and a Duckling. They are both Adorable.

trait Adorable
class Cat
class Kitten extends Cat with Adorable
class Duck
class Duckling extends Duck with Adorable

As you can see, something is not right here. Even if they are adorable, it doesn’t mean that they both have a common ancestor that was the adorableness itself. The last common ancestor of cats and ducks was most probably a small, definitely not adorable reptile, living some 350 million years ago. Them being adorable is not something they inherited. It’s a trait (a-ha!) they developed independently from each other. And on top of that, sometimes they lose it when they turn into adult animals. Being adorable is then more like something they can do (i.e. affect people with their cuteness) than something they are. More likely than having a class Kitten extending something adorable, we will have class Kitten extending Cat with the trait Adorable, and similarly we will have the class Duckling extending Duck with the trait Adorable.

trait Adorable
class Cat
class Kitten extends Cat with Adorable
class Duck
class Duckling extends Duck with Adorable

But “a-ha!” you may say back to me. “Nevertheless, there’s obviously a deep hierarchy of evolutionary ancestors of kittens and ducklings, as well as a big phylogenetic tree on both sides! Kittens are cats, cats are felines, felines are mammals, and mammals are animals. The same thing for ducklings, and — by the way — if we look into the duck evolutionary tree, then there’s got to be a t-rex somewhere there…”

both come from a long line of extinct species and/or they are categorised into more and more generic groups of animals that are connected at some point? No! You go “aww” because they are adorable! It’s as simple as that! There’s no inheritance involved!

Translating this quite risky metaphor to programming, I’d argue that the majority of programs we write are nowhere near so complex that they actually require deep inheritance hierarchies. The code that uses our data models is usually much more interested in what those data models can do, than what they are in their essence.

Composition: what the type can do

In a real end-user app, much more likely than cats and ducks, we will encounter storages, services, and either some kind of UI views or another piece of code that helps us communicate with our program. It’s quite possible that for many of those classes we actually may find their common parts and put them in superclasses. But in most cases, this hierarchy will be flat: one type of abstract storage, one type of abstract service, etc. Instead, if we spot that some parts of code behave similarly and we want to use that, we will do it with composition.

With inheritance, we tend to have this mental model that the superclass of several subclasses belongs close to those subclasses. The whole hierarchy — or at least the core part of it — is going to be in the same module. And the subclasses are heavy: there’s almost always some code we don’t care about but we still inherit it.

With composition, it’s the opposite. Interfaces and traits tend to be small, light, and in comparison more independent from classes that implement them. Imagine we build an application with services and storages, and at some point, we want a service that will handle only Adorable data through any storage that can fetch it. To do that, we will create a trait AdorableFetcher and put it somewhere near the new service, together with the method fetchAdorable:

trait Adorable
trait AdorableFetcher[T <: Adorable] {
def fetchAllAdorable: List[T]
}

Only after that, almost as if it was an afterthought, we will go to the storages and make some of them implement the AdorableFetcher trait so that in the future the service will know that it can use them to fetch adorable data.

abstract class AbstractStorage[T] { 

}

So here we have an abstract class AbstractStorage, and a KittenStorage which extends AbstractStorage[Cat] with AdorableFetcher of kittens, and we will have an overridden method fetchAllAdorable that fetches all adorable kittens.

class KittenStorage extends AbstractStorage[Cat] 
with AdorableFetcher[Kitten] {

override def fetchAllAdorable: List[Kitten] = …

}

Then we have a class DucklingStorage that extends AbstractStorage, but of ducks this time, and it also implements an AdorableFetcher of a Duckling. Here we override fetchAllAdorable to fetch all ducklings.

class DucklingStorage extends AbstractStorage[Duck]
with AdorableFetcher[Duckling] {

override def fetchAllAdorable: List[Duckling] = …

}

In the end, we will have the AwwService which is created with the AdorableFetcher of kittens, and the AdorableFetcher of ducklings. There we will have a method allAdorable that will fetch all adorable kittens and all adorable ducklings and it will return them all together. And also here is our method aww that will call the method cutenessOverload which comes from the trait Adorable and so we can call it on every adorable creature we can find.

class AwwService(
kittensFetcher: AdorableFetcher[Kitten],
ducklingsFetcher: AdorableFetcher[Duckling]
) {
def allAdorable: Set[Adorable] =
List(kittensFetcher, ducklingsFetcher)
.flatMap(_.fetchAllAdorable)
.toSet

def aww(): Unit = allAdorable.map(_.cutenessOverload())
}

By using traits our service is no longer limited to work only with class hierarchies. Literally, anything that implements Adorable can be handled by AwwService. And the same goes for AdorableFetcher — there’s a reason why I didn’t call it a storage. Anything that implements the trait AdorableFetcher is now a valid helper for AwwService. It doesn’t have to be a storage. On the other hand, not every storage may fulfil the criteria posed by AwwService — some of them cannot fetch anything adorable. The trait AdorableFetcher becomes a contract between the service and whatever there is on the other side. In other words, if it walks like a duck and quacks like a duck, then it can be a t-rex for all we care, because we are interested only in that it fulfils the contract of a duck.nly in that it fulfils the contract of a duck.

Mocking frameworks

In a more serious example, this ability is the basis for mocking classes in unit tests. A mock is an implementation of an interface that is… well, I would want to say “fake”, but it really is an implementation. It just doesn’t do what a real implementation is supposed to do. Instead, the mocking framework lets us build a simplified implementation in a way that we don’t even feel that we do it. And then we can create an instance of our tested service with all those storages substituted with their mocked counterparts. Every time the service will access a mocked storage and call one of its methods, it will not run the actual production code, but the implementation provided by the mocking framework. For example when we run a unit test with the following code …

val mockedKittens = mock[AdorableFetcher[Kitten]]
when(mockedKittens.fetchAllAdorable)
.thenReturn(List(simba, nala))

… then mockedKittens will be transformed into an instance of an anonymous class implementing AdorableFetcher[Kitten] and its implementation of the method fetchAllAdorable will always return List(simba, nala).

Similarly, mockedDucklings will be a mocked version of an AdorableFetcher of ducklings, and when we call fetchAllAdorable on mockedDucklings then a generated method will be used that will return a list of three ducklings: Huey, Dewey, and Louie.

val mockedDucklings = mock[AdorableFetcher[Duckling]]
when(mockedDucklings.fetchAllAdorable)
.thenReturn(List(huey, dewey, louie))

(To be honest, I never liked those three in DuckTales).

And now we can create our very real AwwService which will never know that both fetchers we pass to it are not real storages of kittens and ducklings, but their mocked counterparts:

val awwService = new AwwService(mockedKittens, mockedDucklings)

Thanks to this, we can test the AwwService’s method aww. The code of aww is the same as in production (that’s the whole point; that’s what we want to test) but in turn, it will access our mocked implementations of fetchAllAdorable. In the end, we will get a list composed of our kittens and ducklings from the mocking code. Which will prove that the aww method works as it should. method works as it should.

awwService.allAdorable 
shouldEqual List(simba, nala, huey, dewey, louie)

Code architecture with traits

Another advantage of this approach is that when you first sit down and design your data structures and methods working on them, you’re free. You can focus on making them exactly as you want and only then think if you can make them implement a given trait. If doing that is not trivial, you can decide — either you can change something in the code you have just written or maybe there is another way. For example, maybe you can provide a method that will create an instance of another class from the one you have, and that new class will be able to implement the trait (think of a collection and an iterator).

The second solution provides better separation of concerns, but, on the other hand, there is a disadvantage in that it can lead to code duplication. In many cases, an interface is going to be implemented in the same or a very similar way in several classes. But since those implementations are independent of each other, the code has to be repeated. One partial solution to this are default implementations of traits methods. The default implementations may then be overridden in the class which implements the trait, or they may be left as they are.

trait MyIterator[T] {
def next(): Option[T] // the only abstract method in the trait
def map(f: T => Z): Option[Z] = …
// a method with default implementation
// that at some point calls next()
def foreach(f: T => Z): Unit = …
// and another one
}

For example, the trait MyIterator will have only one abstract method, next, but it will have the default implementations of map and foreach which both use that next method. And then we can have a class, for example MyComplexCollection, that extends MyIterator, and it defines the next method:

class MyComplexCollection[T] extends MyIterator[T] {
override def next(): Option[T] = …
}

And then we can have MyOtherComplexCollection that creates an iterator...

class MyOtherComplexCollection[T] {
def iterator: Iterator[T] = new MyIterator[T] {
override def next(): Option[T] = …
}
}

.. and another that creates another iterator…

class OhGodsNotAgainComplexCollection[T] {
def iterator: Iterator[T] = new MyIterator[T] {
override def next(): Option[T] = …
}
}

A class that implements MyIterator needs to provide implementation only for the next method — and in return, it gets map, and foreach for free. You can see it working very well in Rust where you just have to implement a few key traits on your data structure and suddenly you get a buttload of additional functionality.

Final words

And this is how we finally reach the end of the series. I think I’m going to take a small break now from making videos and focus on some other stuff… but not for long. For sure I would like to make a follow-up video about Scala futures which, in the Scala functional programming community, are viewed with some disdain. I would like to defend them a bit, talk about both their advantages and disadvantages, and look into how their internals work.

I really hope you enjoyed the series and if you learned something new then, yay, that’s great. Let me know, here in the comments, or on Twitter. Also, please look through the series and share with friends anything that you find interesting. All I do is published under the Creative Commons licence so if it can help you in any way, please just take it and use it.

Go back to the first article in the “Programming with Functions” series

Sign up to discover human stories that deepen your understanding of the world.

Nerd For Tech
Nerd For Tech

Published in Nerd For Tech

NFT is an Educational Media House. Our mission is to bring the invaluable knowledge and experiences of experts from all over the world to the novice. To know more about us, visit https://www.nerdfortech.org/.

Maciek Gorywoda
Maciek Gorywoda

Written by Maciek Gorywoda

Scala. Rust. Bicycles. Trying to mix kickboxing with aikido. Trying to be a better person too. Similar results in both cases. 🇪🇺 🇵🇱

No responses yet

Write a response