The main reason for this is due to the concepts of Inheritance or Polymorphism, but clarity can be a challenge, particularly in frontend development.
Angular adds complexity with the forwardRef
function, which can lead to confusion.
To give a more thorough explanation, it's important to note that:
Angular separates components from services to enhance modularity and reusability.
Why emphasize this (from the Angular Doc)? Because it is the primary reason for utilizing Inheritance or Polymorphism. These concepts are often more suitable for Services rather than Components.
Does this mean they cannot be used in Components? Not at all. There are valid use cases, but following the principle that components should focus on presentation while logic is handled by Services, Inheritance or Polymorphism may be more commonly applied in Services.
Let's illustrate a scenario where Inheritance
and abstract
classes are well-suited.
Imagine our app needs to calculate areas for different geometrical shapes like rectangles and triangles:
@Injectable()
abstract class ShapeService<T = any> {
protected abstract calcArea(measures: T): number;
logArea(measures: T): void {
console.log("The area of our shape is: ", this.calcArea(measures));
}
}
interface Rectangle {
width: number;
height: number;
}
@Injectable()
class RectangleService extends ShapeService<Rectangle> {
protected calcArea(measures: Rectangle): number {
return measures.height * measures.width;
}
}
interface Triangle {
width: number;
height: number;
}
@Injectable()
class TriangleService extends ShapeService<Triangle> {
protected calcArea(measures: Triangle): number {
return measures.height * measures.width / 2;
}
}
In this example, both classes extend ShapeService
and implement the calcArea
method. By abstracting common logic into the base class, we avoid duplication when different services utilize the same message logging functionality.
This scenario exemplifies the typical use case for implementing abstract classes. It is especially prevalent when employing the strategy design pattern, where shared logic exists across multiple classes.
If you wanted to incorporate these classes into a component, you could do so as follows:
@Component([
...,
standalone: true,
providers: [
{ provide: ShapeService, useClass: RectangleService }
]
])
export class RectangleComponent {
constructor(private readonly shapeService: ShapeService<Rectangle>) {}
}
What if the service's selection depends on the current route? Using the useFactory
provider offers a solution without requiring dual injections in your component:
@Component([
...,
standalone: true,
providers: [
{
provide: ShapeService,
useFactory: (route: ActivatedRouteSnapshot) => {
if (route.paramMap.has('rectangle')) {
return new RectangleService();
} else if (route.paramMap.has('triangle')) {
return new TriangleService();
}
throw new Error('An error')
}
}
]
])
export class ShapeComponent {
constructor(private readonly shapeService: ShapeService) {}
}
Now, using the same component, we can calculate and log different shapes based on the route. The component handles UI interaction, while the services manage calculation logic independently.
In response to your question:
What is the purpose of both providing and extending a class in Angular?
It allows us to divide our code into modular components, sharing common logic, and ensuring appropriate usage of specific classes in their intended contexts.