Introducing Template Method with Inheritance in Angular
Using the design pattern Template Method to handle URL parameters of similar components with the same logic
Reusing code and reducing the maintenance effort of code are more and more popular principles of modern programming because the code base of software applications increases very quickly within a short time. Feature pressure is one of many plagues of a software engineer these days.
Besides Object Composition, Inheritance is one of many principles for reusing code. The design pattern Template Method uses this to define the structure of an algorithm in a superclass. Still, it allows subclasses to override specific steps of the algorithm without changing the structure.
Briefly explained: Routing in Angular
In Angular the method for reading URL parameters using the ActivedRoute injection is one of the most common methods.
public id: string;
constructor(private activatedRoute: ActivatedRoute) { }
ngOnInit(): void {
this.id = this.activatedRoute.snapshot.paramMap.get('id');
}
To make the id attribute immutable, we can change this behavior by implementing a get id() method.
public get id(): string {
return this.activatedRoute.snapshot.paramMap.get('id');
}
constructor(private activatedRoute: ActivatedRoute) { }
For more information about the implementation of routing and parameter handling, we can visit the Angular Docs.
Preparing an example: Two similar components
In many cases, we use the same base URL with a parameter and specialize the suffix of an URL to give access to different components.
const routes: Routes = [
{ path: 'device/:id/masterdata', loadChildren: () => ... },
{ path: 'device/:id/monitoring', loadChildren: () => ... }
];
In this example, we have two components that need to read the URL parameter id to show the user specific information. With the id, the components receive master data through a masterdata-service.
The implementation of both components is nearly the same. Two getter methods for id and title, a constructor and an onInit() method for getting master data information by a masterdata-service.
export class MasterDataComponent implements OnInit {
private device: Device;
public get id(): string {
return this.activatedRoute.snapshot.paramMap.get('id');
}
public get title(): string {
return this.device.id + ' | ' + this.device.name;
}
constructor(private activatedRoute: ActivatedRoute, private masterdataService: MasterdataService) { }
ngOnInit(): void {
this.device = this.masterdataService.getById(this.id);
}
}
Only the get title() method varies from one component to the other.
export class MonitoringComponent implements OnInit {
// ...
public get title(): string {
return this.device.name + ' | ' + this.device.manufacturer;
}
// ...
}
It may be helpful to encapsulate the duplicated code into a superclass and outsource the get title() method. But what happens if we need the get title() method in new methods that are equal in both components? We will generate more duplicated code because the initial method get title() is different.
The get id() methods have a dependency, too: ActivatedRoute. So if we put this into the superclass, we need to add a library dependency here, too. If we implement tests for the superclass or want to reuse the class, we have to take along the dependency. Annoying and not maintainable. For this example, there is only one dependency, but if we have a more complex code base, there could be many more dependencies.
Reusing URL Parameter Acces with Template Method
Template Method is a behavioral design pattern that defines the skeleton of an algorithm in the superclass but lets subclasses override specific steps of the algorithm without changing its structure. [https://refactoring.guru/design-patterns/template-method]
We can find a detailed description of the Template Method pattern at refactoring.guru .
We must implement an abstract base class toi comply with the pattern. With this, we will implement an abstract method to get access to the id for the masterdata-service.
export abstract class DeviceTemplate {
protected _device: Device;
public get device(): Device {
return this._device;
}
public abstract get id(): string;
protected constructor(protected masterdataService: MasterdataService) { }
protected initTemplate(): void {
this._device = this.masterdataService.getById(this.id);
}
}
The value of this.id is known now because the class DeviceTemplate forces its subclasses to implement the method get id() because of the keyword abstract. And this method is implemented with the route-reading-condition in the Monitoring- and Masterdata-Component.
export class MonitoringComponent extends DeviceTemplate implements OnInit {
public get id(): string {
return this.activatedRoute.snapshot.paramMap.get('id');
}
// ...
}
We can do the same with the title to be more consistent and to use this value in the superclass for upcoming features.
We can extend the superclass code with more features by using the id and the title and complying with the DRY principle. Thanks to abstract.
Using Null Object Pattern for Unknown Objects
If we look at the final code below, we can see that we have implemented only three devices for this example. So only three ids are supported to get a device: 1, 2, 3. But what happens if we access a route with an id that is not implemented?
To prevent returning a null value, we create an Unknown-Device class that writes alternative values to the attributes as constants. To handle the polymorphism of the service, the new class implements the interface.
export class UnknownDevice implements Device {
private readonly _id: string = 'unknown';
private readonly _name: string = 'Unknown device';
private readonly _manufacturer: string = 'Unknown manufacturer';
private readonly _costs: number = 0.00;
public get id(): string {
return this._id;
}
public get name(): string {
return this._name;
}
public get manufacturer(): string {
return this._manufacturer;
}
public get costs(): number {
return this._costs;
}
}
To handle undefined ids, the service will check if there is an available device with a simple if-condition. If there is no data, we will return the defined UnknownDevice object.
export class MasterdataService {
getById(id: string): Device {
const result: Device = listDevice.find(x => x.id === id);
if (result == null) {
return new UnknownDevice();
}
return result;
}
}
We can read the following article to learn more about Special Cases: How to Refactor a Null-Checking-Condition With Object Composition in TypeScript.
Conclusion
Abstract classes and abstract methods, especially the Template Method pattern, can help us to prevent writing duplicated code and comply with the DRY principle. With that, we can write more efficient code and implement scalable features faster without breaking the Open-Closed-Principle.
Thanks for reading! Follow me on Medium, Twitter, or Instagram, or subscribe here on Medium to read more about DevOps, Agile & Development Principles, Angular, and other useful stuff. Happy Coding! :)