3 min read

When to Choose Inheritance Over Composition

Inheritance is best used for code reuse when modeling data.
When to Choose Inheritance Over Composition

Favoring Composition Over Inheritance

Favoring composition over inheritance is a really great principle to follow. The problem I find with this though is that new developers lack understanding between the different use cases for inheritance. Inheritance is used mainly for code reuse and polymorphism, but what are the different use cases for code reuse? If you can do this same thing but better with composition, why is inheritance still a thing?

Modeling Data

Modeling data is probably the most powerful use case for inheritance. You can do this with interfaces and composition, but inheritance is better to use. When you are modeling data, you are more often than not going to need some code reuse. This code reuse is more about defining a structure. If you have a deep hierarchy of classes, they can still stay simple when using inheritance. An example of modeling data would be the classes used in your Data Access Objects (DAO) layer.

The example to show this will have two types of objects: objects that are updated and objects that are never updated. The top of the hierarchy will be used for classes that are never updated.

// Java Code
public abstract class ReadonlyDaoObject {

    private UUID id = UUID.randomUUID();
    private Timestamp createdOn = 
            new Timestamp(System.currentTimeMillis());

    public UUID id() {
        return id;
    }

    public Timestamp createdOn() {
        return createdOn;
    }
}

This has both an ID that each database table requires as well as a timestamp of when it was created. This class can be extended to create a class for modeling objects that can be updated.

The next class will extend ReadonlyDaoObject and will be used for classes that can be updated.

// Java code
public abstract class ReadWriteDaoObject extends ReadonlyDaoObject {

    private int verion;
    private Timestamp modifiedOn;

    // ...
}

This class contains a version variable that can be used for optimistic locking and a timestamp of when it was last modified.

Using this class hierarchy, there is a structure that can be used to represent both objects that can be updated and not updated.

Next, a Log class that isn't updateable and a User class that is updateable will be added. The log class would extend ReadonlyDaoObject since it wouldn't need to be updated.

// Java code
public class Log extends ReadonlyDaoObject {

    private String content;
    private int level;

    // ...
}

The User class would extend ReadWriteDaoObject and would look something like the following:

// Java code
public class User extends ReadWriteDaoObject {

    private String firstName;
    private String lastName;
    private Image thumbnail;

    // ...
}

When modeling data, inheritance is simpler when your code reuse is more about structure and less about behavior.

Composition for Modeling Data

If the previous example was implemented using composition instead, this would be far more complex and with no benefit. Both ReadonlyDaoObject and ReadWriteDaoObject would be interfaces.

// Java code
public interface ReadonlyDaoObject {

    UUID id();

    Timestamp createdOn();
}


public interface ReadWriteDaoObject extends ReadonlyDaoObject {

    int version();

    Timestamp modifiedOn();
}

You would then need an implementation for both these interfaces that could be used in each class.

// Java code
public class ReadonlyDaoObjectImpl implements ReadonlyDaoObject {

    private UUID id = UUID.randomUUID();
    private Timestamp createdOn = 
            new Timestamp(System.currentTimeMillis());

    @Override
    public UUID id() {
        return id;
    }

    @Override
    public Timestamp createdOn() {
        return createdOn;
    }
}


public class ReadWriteDaoObjectImpl implements ReadWriteDaoObject {

    private ReadonlyDaoObjectImpl readonlyDaoObject;
    private int version;
    private Timestamp modifiedOn;

    @Override
    public UUID id() {
        return readonlyDaoObject.id();
    }

    @Override
    public Timestamp createdOn() {
        return readonlyDaoObject.createdOn();
    }

    @Override
    public int version() {
        return version;
    }

    @Override
    public Timestamp modifiedOn() {
        return modifiedOn;
    }
}

Using composition introduces a naming problem. What do you name the implementation classes? A lot of times you will see bad naming because of this structure, such as putting a suffix of Impl like the previous example shows.

The User and Log examples would look like the following:

// Java code
public class Log implements ReadonlyDaoObject {

    private ReadonlyDaoObjectImpl readonlyDaoObject;
    private String content;
    private int level;

    @Override
    public UUID id() {
        return readonlyDaoObject.id();
    }

    @Override
    public Timestamp createdOn() {
        return readonlyDaoObject.createdOn();
    }
}


public class User implements ReadWriteDaoObject {

    private ReadWriteDaoObjectImpl readWriteDaoObject;
    private String firstName;
    private String lastName;
    private Image thumbnail;

    @Override
    public UUID id() {
        return readWriteDaoObject.id();
    }

    @Override
    public Timestamp createdOn() {
        return readWriteDaoObject.createdOn();
    }

    @Override
    public int version() {
        return version;
    }

    @Override
    public Timestamp modifiedOn() {
        return readWriteDaoObject.modifiedOn();
    }
}

There is a lot of duplication of the structure of the object since there isn't a hierarchy. Every object has to duplicate all the methods and variables. This is a clear example of where inheritance is a far better use than composition.

Conclusion

Favor composition over inheritance with the exception of modeling data structures. Even when you have a complex class hierarchy, it can be much simpler than what you can do with interfaces and composition. Don't have the mindset that one is better than the other; instead, understand which is the best tool for the job.