Inheritance vs Composition
Object-oriented programming (OOP) is a popular paradigm that encourages modular, reusable, and maintainable code. Two fundamental concepts within OOP are inheritance and composition. While both play crucial roles in designing software systems, they offer distinct approaches to code organization and reuse. In this article, we'll delve into the key differences between inheritance and composition in TypeScript, providing illustrative examples and real-world use cases for each.
Understanding Inheritance
Inheritance is a fundamental concept in OOP that enables one class (the child or subclass) to inherit properties and methods from another class (the parent or superclass). This mechanism promotes code reuse by allowing the child class to extend the behaviour of the parent class. In TypeScript, inheritance is achieved using the extends
keyword.
Example: Inheriting Properties and Methods
Let's consider a classic example involving shapes. We have a base class Shape
and a subclass Circle
that inherits from it.
class Shape {
constructor(public color: string) {}
draw(): void {
console.log(`Drawing a ${this.color} shape`);
}
}
class Circle extends Shape {
constructor(color: string, private radius: number) {
super(color);
}
calculateArea(): number {
return Math.PI * this.radius ** 2;
}
}
In this example, Circle
inherits the color
property and the draw
method from Shape
and it also defines its method calculateArea
.
Embracing Composition
Composition is an alternative approach to code reuse that involves constructing complex objects by combining smaller, more specialized objects. Unlike inheritance, which creates a tight coupling between parent and child classes, composition promotes greater flexibility and modularity. TypeScript facilitates composition through the use of class properties and interfaces.
Example: Composing Complex Objects
Let's consider a scenario where we need to model a Person
class that has an associated Address
object.
class Address {
constructor(public street: string, public city: string, public zip: string) {}
}
class Person {
constructor(public name: string, public age: number, public address: Address) {}
displayInfo(): void {
console.log(`${this.name} is ${this.age} years old and lives in ${this.address.city}`);
}
}
In this example, the Person
class is composed of an Address
object, promoting a more modular and loosely coupled design.
Choosing the Right Approach: Use Cases
Inheritance Use Cases
Inheritance is suitable when you want to:
Extend Behavior: You want to create a specialized version of an existing class, adding or modifying its behaviour. For instance, creating different types of shapes based on a common shape class.
Promote Code Reuse: You intend to reuse a significant portion of code from a base class across multiple subclasses.
Composition Use Cases
Composition shines when:
Flexibility is Key: You need to build complex objects with varying parts that can be easily swapped out or modified without affecting the entire structure.
Avoid Tight Coupling: You want to avoid creating deep hierarchies and tight dependencies between classes, leading to more maintainable and modular code.
Conclusion and Further Readings
Inheritance and composition are two powerful techniques in TypeScript's object-oriented toolbox. Each approach has its strengths and use cases, allowing developers to craft software systems that balance code reuse, modularity, and flexibility. By understanding the differences between inheritance and composition, you can make informed design decisions that lead to robust and adaptable software architectures.
https://www.geeksforgeeks.org/difference-between-composition-and-inheritance-in-javascript/
https://www.newline.co/books/beginners-guide-to-typescript/inheritance-and-composition
https://softchris.github.io/pages/javascript-composition.html