Let’s define a base class Person that takes in a parameter name, and assigns it to a protected property.
1 2 3 4 5 6 7 8 9 10 11 |
class Person { constructor(protected name: string) { this.name = name; } display() { console.log(`name is: ${this.name}`); } work() { console.log(`${this.name} is not doing anything`); } } |
We then create Employee that extends from Person. It has a super reference to its parent class Person. Since it extends from Person, it can access property name. In our constructor, we simply take in a name and append a Emp stamp on it. We also override the work function from parent class.
1 2 3 4 5 6 7 8 9 10 11 12 |
class Employee extends Person { constructor(param: string) { super(`${param} [Emp]`); } work() { console.log(`${this.name} is sitting in a Cubicle`); } } function getToWork(person: Person) { person.work(); } |
When we declare functions, we can use the base class as parameters and pass in any kind of children classes. This is called Sharing common behavior
1 2 3 4 |
const a : Person = new Person("ricky"); const b: Employee = new Employee("ricky"); getToWork(a); getToWork(b); |
Abstract classes
Abstract classes requires children classes to implement abstract interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
abstract class Person { constructor(protected name: string) { this.name = name; } // No implementations abstract work (): void; abstract display(): void; } class Employee extends Person { constructor(param: string) { super(`${param} [Emp]`); } work() { // implemented abstract method work √ console.log(`${this.name} is sitting in a Cubicle`); } // uh oh, missing display() X } |
If you extend an abstraction and do not implement it, you’ll get compiler errors:
Unlike concrete classes, abstract classes cannot be instantiated.
Hence, in order to use it
1 2 3 4 5 |
function getToWork(person: Person) { person.work(); } const b: Employee = new Employee("ricky"); getToWork(b); |
The thing about extends from either concrete/abstract classes is that it satisfies several SOLID principles:
Liskov Substitution Principle
“Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.”
Since we define valid subtypes, each implementation should work and be interchangeable as long as it implements the contract. In our case, each implementation (Employee, Manager..etc) should work and be interchangeable with parameter Person because we implement the contract.
As long as our implementation extends from our base class, any pointers or reference to base class can use our derived classes.
1 |
getToWork(extendedFromBase); |
Dependency Inversion Principle (DIP) – Depends upon abstraction, not concretion
instead of doing:
1 2 3 4 5 6 |
const a: Employee = new Employee("Joy"); a.makeCoffee(); const b: Employee = new Employee("Ricky"); b.pushPapers(); const c: Employee = new Employee("David"); c.chatAtWaterCooler(); |
We can make call its abstraction instead:
1 |
employees.forEach(employee => employee.work()); |
- Dependency upon abstraction – Don’t depend on the concrete implementation. Rather, depend on abstraction.
- Liskov Principle – functions with base class reference shall be able to use extended classes
plays into Open Closed Principle.
Let’s say we want to create a function that calculates the area of an array of shapes. With our current design, it might look like that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function calculateAreasOfMultipleShapes( shapes: Array<Rectangle | Circle> ) { return shapes.reduce( (calculatedArea, shape) => { if (shape instanceof Rectangle) { return calculatedArea + shape.width * shape.height; } if (shape instanceof Circle) { return calculatedArea + shape.radius * Math.PI; } }, 0 ); } |
The issue with the above approach is that when we introduce a new shape, we need to modify our calculateAreasOfMultipleShapes function. This makes it open for modification and breaks the open-closed principle.
1 2 3 |
interface Shape { getArea(): number; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Rectangle implements Shape { ... public getArea() { return this.width * this.height; } } class Circle implements Shape { ... public getArea() { return this.radius * Math.PI; } } |
Now that we are sure that all of our shapes have the getArea function, we can use it further.
1 2 3 4 5 6 7 8 9 10 |
function calculateAreasOfMultipleShapes( shapes: Shape[] ) { return shapes.reduce( (calculatedArea, shape) => { return calculatedArea + shape.getArea(); }, 0 ); } |
Now when we introduce a new shape, we don’t need to modify our calculateAreasOfMultipleShapes function. We make it open for extension but closed for modification.