When to Choose Composition Over Inheritance
Code Reuse
Both inheritance and composition are tools that can be used for code reuse. If you don't need a class hierarchy, then inheritance should be avoided. Having a class hierarchy that isn't used polymorphically is a sign that you should be using composition over inheritance.
This article is going to take the same example and implement it using inheritance first and then implement it using composition to show why composition is better.
Code Reuse Using Inheritance
When implementing a Data Access Object (DAO) layer, you are going to have helper methods to simplify the communication to the database. For this example, it is going to be implemented using inheritance. The helper methods are going to be defined in a base class called SqlDao.
// Java code
public abstract class SqlDao {
public void delete(final String sql) {
// ...
}
public void insert(final String sql) {
// ...
}
}
Next, a UserDao will be added. It will inherit from SqlDao so the helper methods can be used inside UserDao.
// Java code
public class UserDao extends SqlDao {
public void delete(final Object object) {
delete("{sql statement}");
}
public void insert(final Object object) {
insert("{sql statement}");
}
}
This is a really simple example, but you can already see things wrong with it. The first thing is, the methods in the SqlDao are public. This breaks encapsulation, and these methods can be called in any DAO that implements SqlDao. This makes it possible for developers to have SQL outside of the DAO layer, breaking that layer.
// Java code
userDao.delete("{sql statement}");
This could be fixed by making the methods in SqlDao protected. This fixes the encapsulation problem, but you still have the possibility of a naming conflict. In the UserDao class, there are the same methods that are in the SqlDao. Since they take different parameter types, this is okay, but in other scenarios it may cause a conflict.
In a more complex inheritance structure, you could have another base class that inherits from SqlDao. You could have a base class for an Object-Relational Mapping (ORM), or you might have a base class for temp tables.
If you are using meta data in your code, inheritance introduces additional complexity with this as well. Using inheritance in this situation offers code reuse but adds too much complexity.
Code Reuse Using Composition
The above example is much simpler using composition instead of inheritance. It also eliminates all of the problems outlined above. Using composition, the SqlDao class doesn't change other than it is no longer abstract.
// Java code
public class SqlDao {
public void delete(final String sql) {
// ...
}
public void insert(final String sql) {
// ...
}
}
The UserDao contains an instance of SqlDao instead of inheriting from it.
// Java code
public class UserDao {
private SqlDao sqlDao = new SqlDao();
public void delete(final Object object) {
sqlDao.delete("{sql statement}");
}
public void insert(final Object object) {
sqlDao.insert("{sql statement}");
}
}
This example is very simple and doesn't break encapsulation. This doesn't define a hierarchy. Defining a class hierarchy implies that it is to be used with polymorphism. Composition avoids this confusion and greatly simplifies the code without the complexity of inheritance.
Dependency Injection
When using composition, it will commonly be paired with dependency injection. Dependency injection is where you use composition, but the instance is provided to the class either through a constructor or a setter method. This allows you to change what dependencies the class uses depending on the environment. Production code could use one implementation, and test code could use a different implementation, such as a mock.
Using the UserDao above, the SqlDao will be provided through constructor injection.
// Java code
public class UserDao {
private SqlDao sqlDao;
public UserDao(final SqlDao sqlDao) {
this.sqlDao = sqlDao;
}
public void delete(final Object object) {
sqlDao.delete("{sql statement}");
}
public void insert(final Object object) {
sqlDao.insert("{sql statement}");
}
}
When creating a UserDao, you can now provide a different SqlDao depending on the environment. Creating an instance in a production environment will use the same SqlDao class.
// Java code
final var userDao = new UserDao(new SqlDao());
In a test environment, you can now provide a mock SqlDao.
// Java code
final var userDao = new UserDao(mock(SqlDao.class));
Conclusion
Composition should be used when you need code reuse. Defining a class hierarchy when you don't need one complicates the code and tells developers that it is intended to be used with polymorphism. Composition also allows you to inject the dependency into the class, making it very flexible. This isn't something that can be done with inheritance.