Composition over inheritance
Introduction
These days, one of the most common software engineering principle did dawn on me. Personally I do have a good grasp of understanding this principle, as well as the concepts around it.
In my opinion this principle and concepts always appear in interviews commonly. In addition, at work either we are instinctively implementing them, or emphasizing from the earlier code that was written by a fellow engineer.
In this article, I’ll describe all the concepts and topics around the principle, so that you’ll have a better understanding of the details. Then I’ll explain why we should prefer Composition or Inheritance. I’ll demonstrate UML diagrams which are kind of outdated in fast-paced development environments, and some code examples with definitions.
Association
The fundamental starts with the Association, when we approach the topic top to bottom approach as illustrated above, we apprehend the simple definition:
Association sets a relationship between classes which gives them ultimate freedom to extend their design.
The earlier design assumptions will improve the simplicity and re-usability in the overall system. This root topic dwells in two particular sub topics, namely “Inheritance” and “Associations”
Inheritance
The most fundamental piece in all Object Oriented Programming Languages. Every single OOP language in the market has its own ways of implementing it.
When we say Inheritance, we point that two related classes is one another and it is a form in which a class can be derived from another class for a hierarchy of commonly-shared set of attributes and methods.
In essence, a participant is called “Parent/Super Class” the other participant is “Child/Sub/Derived Class”. With that being said, the term “IS-A” is derived, when we look at the nature and we observe similarities.
Animal.java
public class Animal {}
Dog.java
public class Dog extends Animal{}
The implementation of Inheritance relies on the language we use. In Java, we are able to implement it using the following keywords “extends” and “implements” on the class definitions. Let’s dive into some of the basic remarks in Java:
- An Interface can extend another interface or multiple interfaces,
- A Class can extend only one class/abstract class at most but implement many interfaces,
- Multiple inheritance is not allowed.
On the other hand, it would be beneficial to note a vital topic “Polymorphism” while we talk about the Inheritance
Polymorphism means “many forms” by dictionary. By using Polymorphism, the child classes can implement different features other than their parents.
In the topic of the Polymorphism, we will be exposed to the following concepts:
1. Method Overriding: Child classes can change the parent-provided method, and implement its own version for an implementation detail,
2. Method overloading: Child classes can change the signature of the parent-provided method method and define its own custom overloaded version of additional method.
By now, we shall have good understanding of the Polymorphism. Now, I’d like to demonstrate an example and a basic use case how to implement Polymorphism in Java.
In this example, we will design a system that calculates Interest rates. The parent will have blueprint of the Interest Rate and each specific country will be able to implement its own methods depending on their laws and regulations or stick with the default implementations.
BaseInterestRate,java
public class BaseInterestRate {
public int calculate() {
return 2 * 2;
}
public void callCentralBank(){
System.out.println("Calling the central bank");
}
}
EuroInterestRate.java
public class EuroInterestRate extends BaseInterestRate {
@Override
public int calculate() {
return 4 * 4;
}
@Override
public void callCentralBank() {
System.out.println("Calling European Central Bank");
}
public void callCentralBank(String message) {
System.out.println("Calling European Central Bank with the message: " + message);
}
}
UsInterestRate.java
public class UsInterestRate extends BaseInterestRate {
@Override
public int calculate() {
return 6 * 4;
}
}
Bank.java
public class Bank {
public static void main(String[] args) {
BaseInterestRate baseInterestRate = new BaseInterestRate();
System.out.println("Calculated Base Interest Rate: " + baseInterestRate.calculate());
baseInterestRate.callCentralBank();
EuroInterestRate euroInterestRate = new EuroInterestRate();
System.out.println("Calculated European Rate: " + euroInterestRate.calculate());
euroInterestRate.callCentralBank();
euroInterestRate.callCentralBank("Give discount for developers");
UsInterestRate usInterestRate = new UsInterestRate();
System.out.println("Calculated US Rate: " + usInterestRate.calculate());
usInterestRate.callCentralBank();
}
}
Console Output
Calculated Base Interest Rate: 4
Calling the central bank
Calculated European Rate: 16
Calling European Central Bank
Calling European Central Bank with the message: Give discount for developers
Calculated US Rate: 24
Calling the central bank
In this example, we observe the following design patterns:
- Base Interest Rate is the parent class which has the common functionality for the Child Classes,
- Euro Interest Rate is a Child which offers polymorphic behaviors that overrides all the parent methods including the “callCentralBank()”. In addition, it does overload the same method, with its own way of implementation with a different method signature that accepts a String variable,
- Us Interest Rate is yet another Child Class which just chooses to override the interest rate calculation and for the rest of the methods, it relies on the Parent’s methods.
Associations
Associations come as the root of concept, and it offers different types. The common sub topics that lies under Associations are “ Aggregation” and “Composition”. If we start with the definition:
The Association/has-a relationship signifies that a class has a relationship with another class. For instance, Class A holds Class B’s reference and can access all properties of class B.
The association can be any type of:
- One-to-one: points to a degree in which participants are unique to each other, such as a Person and a National Identity Card,
- One-to-many: points to a degree in which a single entity can be associated with many other entities, for instance A Company has many Employees,
- Many-to-one: points to a degree in which many entities are associated with single entity, say that many of cities are associated to a single country,
- Many-to-many: points to a degree in which many entities are associated with each other, for example many lecturers can be associated with many courses.
We are embarking on topic “Directions” last but not least it important to note following definition:
Defines a degree in which it allows the participants how they should know the weak connection of their relationship. Either one or both of them will be aware of it
The definition “weak” will be elaborated more in the Aggregation which is the target topic defining it. Furthermore, the directions come in one of two forms:
- Uni-direction: Only one class knows about the relationship. In the following particular example, only the Course class knows about the Students that are enrolled to it, not the other way around. The course will access the students via the Set data structure.
Student.java
public class Student {
String name;
}
Course.java
public class Course {
Set<Student> students = new HashSet<>();
public Set<Student> getStudents() {
return students;
}
public void setStudents(Set<Student> students) {
this.students = students;
}
}
2. Bi-direction: Both classes are care aware of their relationship. Let us alter the existing example and introduce the bi-directional relationship between the Course and the Students.
Student.java
public class Student {
String name;
Course course;
public Course getCourse() {
return course;
}
public void setCourse(Course course) {
this.course = course;
}
}
Aggregation
Aggregation is a weak ‘has-a’ and uni-directional association type in which classes can survive independently, when their relationship is removed. Let’s look at a contemplating example between Car and Engine Classes:
Car.java
public class Car {
Engine engine;
public Engine getEngine() {
return engine;
}
public void setEngine(Engine engine) {
this.engine = engine;
}
}
Engine.java
public class Engine {
int cylinder;
}
We see observe the weak relationship on the Car side via the dependency which is set by the setter method. We can construct and instantiate a Car object and is optional to pass the Engine dependency.
Composition
Composition is yet another strong ‘has-a’ and uni-directional association type in which entities CANNOT survive independently. In addition, it is considered is ‘part-of relationship’. Let’s look at a another example.
Citizen.java
public class Citizen {
private final IdentityCard identityCard;
public Citizen(IdentityCard identityCard) {
this.identityCard = identityCard;
}
public IdentityCard getIdentityCard() {
return identityCard;
}
}
IdentityCard.java
public class IdentityCard {
String uniqueId;
}
In this example we see that each Identity card is bounded to each respective citizen, and one cannot use other’s identity, that would be called as “Criminal Impersonation” same as in real life, your identity card is unique to you. The strong relation is provided by marking the dependency “IdentityCard” as final and enforcing the injection via constructor.
Last of all, composition has another concept called “Composite Aggregation”
Composite Aggregation signifies a one-to-many binary association in which items on the many side cannot remain on their own.
The most appropriate example can be applied in IT systems. A Folder and the Files lie underneath it. Files cannot exist without an existing Folder that are associated with it.
Final Remarks and Preference Composition over Inheritance
I believe so far we have grasped a good understanding of the fundamentals and the concepts around the principle. As a last course, we can note a few points:
- Inheritance should be implemented, when we make a conscious decision that a designated Child class is a Parent Class. However, on the other hand, the Is-a Association can cause multiple hierarchical issues,
- We can notice an important disadvantage, which is that Inheritance breaks the encapsulation. This circumstance occurs when a Child Class relies on the behavior of the Parent Class. When Parent Class’ behavior changes, it will have a ripple effect on the Child Class,
- Finally, we can point out one of the uttermost disadvantages in Inheritance as it goes; when Multiple Inheritance is implemented among many Child and Parent classes that is known as “Diamond Problem”. In simple terms the Multiple Inheritances leads to the ambiguity in which when the Child Class invokes an Overloaded Method from Multiple Parents it inherits and doesn’t know which one to call. You can read the detailed description at Wikipedia.
- The code re-use, and duplication should not be solely based on Inheritance, but composition. Otherwise, the system design will be purely based on each class is another class,
- Compositions gives us leverage to replace the required dependency in runtime, while following the Dependency Inversion Principle,
- Composition offers better test-ability, than Inheritance. We can easily mock the dependency during unit testing.