In this blog, we'll explore how to implement a generic factory design pattern in Angular 18, using a transportation service booking system as an example with a styled flow diagram with Mermaid.
The Factory Design Pattern is one of the most popular and widely used design patterns in software development. It provides a way to create objects without specifying their concrete classes.
What Is the Factory Design Pattern?
The Factory Design Pattern defines an interface for creating objects, but lets the subclasses alter the type of objects that will be created. This approach increases code flexibility and reduces dependencies on specific implementations.
Use Case: Transportation Service Booking
Imagine an application where users can book transportation services via different modes such as car, bike, bus, or train. Instead of directly instantiating the transportation services in components, we can use a factory design pattern to dynamically create and manage these service objects.
Implementation in Angular 18
1. Define an Interface for Transportation Services
This interface will serve as the blueprint for all transportation modes.
export interface TransportationService {
book(from: string, to: string): string;
calculateCost(from: string, to: string, rate: number): number;
getDistance(from: string, to: string): number;
}
2. Create Specific Transportation Services
Create standalone components for each transportation mode and implement the TransportationService interface.
Car Service Component
import { Injectable } from '@angular/core';
import { TransportationService } from './transportation.service';
@Injectable({ providedIn: 'root' })
export class CarService implements TransportationService {
rates =10;
book(from: string, to: string): string {
const cost = this.calculateCost(from, to);
return `Car booked successfully! From: ${from}, To: ${to}, Cost: $${cost}`;
}
calculateCost(from: string, to: string): number {
const distance = this.getDistance(from, to);
return distance * this.rate;
}
getDistance(from: string, to: string): number {
// Mock distance calculation
return Math.floor(Math.random() * 201);
}
}
Bike Service Component
import { Injectable } from '@angular/core';
import { TransportationService } from './transportation.service';
@Injectable({ providedIn: 'root' })
export class BikeService implements TransportationService {
rates =10;
book(from: string, to: string): string {
const cost = this.calculateCost(from, to);
return `Bike booked successfully! From: ${from}, To: ${to}, Cost: $${cost}`;
}
calculateCost(from: string, to: string): number {
const distance = this.getDistance(from, to);
return distance * this.rate;
}
getDistance(from: string, to: string): number {
// Mock distance calculation
return Math.floor(Math.random() * 201);
}
}
Bus Service Component
import { Injectable } from '@angular/core';
import { TransportationService } from './transportation.service';
@Injectable({ providedIn: 'root' })
export class BusService implements TransportationService {
rates =10;
book(from: string, to: string): string {
const cost = this.calculateCost(from, to);
return `Bus booked successfully! From: ${from}, To: ${to}, Cost: $${cost}`;
}
calculateCost(from: string, to: string): number {
const distance = this.getDistance(from, to);
return distance * this.rate;
}
getDistance(from: string, to: string): number {
// Mock distance calculation
return Math.floor(Math.random() * 201);
}
}
Train Service Component
import { Injectable } from '@angular/core';
import { TransportationService } from './transportation.service';
@Injectable({ providedIn: 'root' })
export class TrainService implements TransportationService {
rates =10;
book(from: string, to: string): string {
const cost = this.calculateCost(from, to);
return `Train booked successfully! From: ${from}, To: ${to}, Cost: $${cost}`;
}
calculateCost(from: string, to: string): number {
const distance = this.getDistance(from, to);
return distance * this.rate;
}
getDistance(from: string, to: string): number {
// Mock distance calculation
return Math.floor(Math.random() * 201);
}
}
3. Create a Generic Factory Service
The factory service dynamically resolves the transportation service based on the type, with added support for generics.
import { Injectable, inject, Injector } from '@angular/core';
import { TransportationService } from './transportation.service';
import { CarService } from './car.service';
import { BikeService } from './bike.service';
import { BusService } from './bus.service';
import { TrainService } from './train.service';
@Injectable({ providedIn: 'root' })
export class TransportationFactory {
private injector = inject(Injector);
public getTransportationService(mode: string): TransportationService {
const serviceMapping: Record<string, { new (...args: any[]): TransportationService }> = {
car: CarService,
bike: BikeService,
bus: BusService,
train: TrainService,
};
const serviceClass = serviceMapping[mode];
if (!serviceClass) {
throw new Error(`Invalid transportation mode: ${mode}`);
}
return this.getInstance(serviceClass);
}
private getInstance<T extends TransportationService>(serviceClass: { new (...args: any[]): T }): T {
return this.injector.get(serviceClass);
}
}
4. Integrate the Factory in a Component
Let's create a component where the user selects the transportation mode, and the appropriate service is booked dynamically.
Transportation Booking Component
import { Component, inject } from '@angular/core';
import { TransportationFactory } from './transportation-factory.service';
import { FormsModule } from '@angular/forms';
@Component({
standalone: true,
selector: 'app-transportation-booking',
imports: [FormsModule],
template: `
<div class="container">
<h1>Book Your Transportation</h1>
<div>
<label for="from">From:</label>
<input id="from" [(ngModel)]="from" placeholder="Enter starting location" />
</div>
<div>
<label for="to">To:</label>
<input id="to" [(ngModel)]="to" placeholder="Enter destination" />
</div>
<div>
<label for="mode">Mode:</label>
<select id="mode" [(ngModel)]="selectedMode">
<option value="car">Car</option>
<option value="bike">Bike</option>
<option value="bus">Bus</option>
<option value="train">Train</option>
</select>
</div>
<button (click)="bookTransportation()">Book</button>
<p>{{ bookingMessage }}</p>
</div>
`,
styles: [
`
.container {
text-align: center;
margin: 20px;
}
input,
select {
margin: 10px;
padding: 5px;
}
button {
margin-top: 15px;
}
`,
],
providers: [
CarService,
BikeService,
BusService,
TrainService,
TransportationFactory,
],
})
export class TransportationBookingComponent {
from: string = '';
to: string = '';
selectedMode: string = 'car';
bookingMessage: string = '';
private transportationFactory = inject(TransportationFactory);
bookTransportation(): void {
if (!this.from || !this.to) {
this.bookingMessage = 'Please enter both From and To locations';
return;
}
const service = this.transportationFactory.getTransportationService(this.selectedMode);
this.bookingMessage = service.book(this.from, this.to);
}
}
Benefits:
Extensibility: Adding new transportation modes is easy; just create a new service and register it with the factory.
Maintainability: Changes to a specific mode's logic are isolated.
Testability: Easier to mock and test individual services and the factory.
Decoupling: The booking component doesn't depend on concrete service implementations.
Conclusion
The Factory Design Pattern is a powerful way to create objects dynamically in Angular applications. By implementing it in our transportation service booking example, we've demonstrated how this pattern promotes flexibility, maintainability, and scalability.
This implementation also highlights the use of Angular 18 features, such as standalone components, making it a modern and efficient solution. With a structured approach and proper separation of concerns, this design pattern can be applied to a wide range of use cases.
Top comments (0)