Angular Design Patterns: Factory Pattern
Intro
The Factory pattern is creational design pattern that provides a mechanism to outsource object creation to either subclass or separate class called factory class.
Key Benefits
Purpose: Centralizes object creation, providing a flexible/dynamic way to create objects without exposing the details of their concrete classes to the rest of your application. The objects can refer to classes/components/services.
Flexibility: It enables you to dynamically decide, at runtime, which objects to instantiate.
Encapsulation: Hides object instantiation details from the client code. Clients interact with a factory to obtain objects, rather than directly instantiating concrete classes. This promotes loose coupling and makes code more adaptable to changes.
Useful Scenarios π
- You don't know in advance the exact objects/services your application will need at runtime
- When you want to provide a mechanism for users to add their own object types.
An important advantage of the Factory Pattern is its ability to instantiate objects dynamically at runtime.β¨
Glossary π
Glossary is going to make more sense when we visit our example.
1. Concrete Factory Class:
The factory class is responsible for creating objects. The objects can refer to classes/components/services.
Consists of factory method.
2. Concrete Factory Method :
Factory Method is a method within a factory class actually returns objects based on conditional statements.
3 .Concrete Product:
The objects/components/services that are created/returned by factory method.
The type of Concrete Product should be same or extended type of Product returned by Concrete Factory Method.
You can also add Abstract Factor class, Abstract Factory Method and Product interface for each of those but it is generally preferred to use interfaces so I did not add them here separately.
Examples:
Example 1: Online/Offline note app:
Assume you have note taking application. Currently, application uses online APIs to save and update data.
Now you would like to add offline capabilities for user to save notes when internet is unavailable.
When it is online the notes would be saved via API, if offline the notes would be saved on local storage.
As you noticed, we want dynamically switch the way the client updates notes.
Problem:
Let's examine naive/brute force approach: β¬οΈ
@Injectable({
providedIn: 'root',
})
export class NoteService {
constructor(
private readonly onlineNoteApi: OnlineNoteApiService,
private readonly offlineNoteApi: OfflineNoteApiService
) {}
public saveNote(data: INote): void {
if (this.isOnline) {
this.onlineNoteApi.save(data);
} else {
this.offlineNoteApi.save(data);
}
}
private get isOnline(): boolean {
return window.navigator.onLine;
}
As you see on saveNote
method we already have undesired condition. This can get more complicated. What if we introduce new API such as Fast API and so on? We are going to have more complexity. π
Solution: π
Step 1: Create Interface for concrete products:
export interface INoteApiService {
save: (note: INote) => void;
}
Step 2: Implement this INoteApiService
on Concrete Products:
@Injectable({
providedIn: 'root',
})
export class OnlineNoteApiService implements INoteApiService {
public save(data: INote): void {
console.log('Save Online');
}
}
@Injectable({
providedIn: 'root',
})
export class OfflineNoteApiService implements INoteApiService {
public save(data: INote): void {
console.log('Save Offline');
}
}
Step 3: Create a Concrete Factory Class:
@Injectable({
providedIn: 'root',
})
class NoteServiceFactory {
constructor(private readonly injector: Injector) {}
public getNoteService(): INoteApiService {
if (window.navigator.onLine) {
return this.injector.get(OnlineNoteApiService);
} else {
return this.injector.get(OfflineNoteApiService);
}
}
}
Step 4: Use NoteServiceFactory
on NoteService
:
Our refactored NoteService:
@Injectable({
providedIn: 'root',
})
export class NoteService {
constructor(private readonly noteServiceFactory: NoteServiceFactory) {}
public saveNote(data: INote): void {
const noteApiService = this.createNoteService();
noteApiService.save(data);
}
private createNoteService(): INoteApiService {
return this.noteServiceFactory.getNoteService();
}
}
That's it π
Do you notice the issue here? On every call we call createNoteService
which creates and returns new reference necessarily. This can cause memory leak. To improve it we can create service on demand when there is a change in network. This solution below resembles strategy pattern.
Updated solution
@Injectable({
providedIn: 'root',
})
class NoteServiceFactoryV2 {
private currentService!: INoteApiService;
constructor(private readonly injector: Injector) {
this.setApiService(window.navigator.onLine);
this.listenToConnection();
}
public getNoteService(): INoteApiService {
return this.currentService;
}
private listenToConnection(): void {
merge(
fromEvent(window, 'online').pipe(map(() => true)),
fromEvent(window, 'offline').pipe(map(() => false)),
fromEvent(document, 'DOMContentLoaded').pipe(map(() => navigator.onLine)) // current status
)
.pipe(startWith(window.navigator.onLine))
.subscribe((isOnline: boolean) => {
console.log('isOnline', isOnline);
this.setApiService(isOnline);
});
}
private setApiService(isOnline: boolean): void {
if (isOnline) {
this.currentService = this.injector.get(OnlineNoteApiService);
} else {
this.currentService = this.injector.get(OfflineNoteApiService);
}
}
}
So instead of creating new reference of service, we listen to internet connection and change the currentService
based on online status.
Example 2: Dynamic Notification provider
Problem:
Imagine you're developing a UI library that needs to support multiple versions of notification components, such as Material Design V1 and Material Design V2. You want users to be able to choose between these versions dynamically, possibly to compare them within the same application environment.
Solution: π
Similarly,in this scenario, we can create Factory Class NotificationServiceFactory
and concrete products NotificationServiceV1
and NotificationServiceV2
which implement INotificationService
.
Since it follows same steps as in Example 1, I skip implementation of it.
Why not use Angular's useFactory
for services?
You might be curious why we don't just use Angular's built-in factory service provider, specifically the useFactory
option, for creating our notification services? In that case it would inject service statically but in our scenario we are interested in dynamic injection of services.
If you are interested in more flexible injection of services, I would recommended to use Strategy Pattern rather than Factory Pattern.
That's all hope you enjoyed it and found it useful. Thanks for reading.π
References:
https://refactoring.guru/design-patterns/factory-method
https://medium.com/@baranoffei/angulars-gof-patterns-factory-method-0c740fbd3b2e