This is a place where Strategy pattern can be used:
Strategy pattern is a behavioral software design pattern that enables
selecting an algorithm at runtime. Instead of implementing a single
algorithm directly, code receives run-time instructions as to which in
a family of algorithms to use.
Let me show an example.
We need to have some common behaviour that will be shared across all strategies. In our case, it would be CRUD methods of session or local storages:
export interface Storage {
retrieve(key: string): string | null ;
store(key: string, value: string): void;
remove(key: string): void;
}
And its concrete implementations. These are exchangeable strategies:
export class LocalStorage implements Storage {
retrieve(key: string): string | null {
return localStorage.getItem(key)
}
store(key: string, value: string): void {
localStorage.setItem(key, value);
}
remove(key: string): void {
localStorage.removeItem(key);
}
}
export class SessionStorage implements Storage {
retrieve(key: string): string | null {
return sessionStorage.getItem(key)
}
store(key: string, value: string): void {
sessionStorage.setItem(key, value);
}
remove(key: string): void {
sessionStorage.removeItem(key);
}
}
This is a class which will execute strategies:
export class StorageService {
public storage: Storage;
constructor(storage: Storage) {
this.storage = storage;
}
retrieve(key: string): string | null {
return this.storage.retrieve(key)
}
store(key: string, value: string): void {
this.storage.store(key, value);
}
remove(key: string): void {
this.storage.remove(key);
}
}
And then we can call our strategies like this:
const storage = new StorageService(new LocalStorage())
storage.store('some key', 'some value')
This design is compliant with the open/closed principle. So if you would need to add other storages, then:
- you would add new class with new strategy
- you will not edit
StorageService class
And it is compliant with open closed principle.
UPDATE:
Thank for comment to Wiktor Zychla:
The client still has to decide directly which storage is passed to the
storage service. Everytime the client needs the storage service, it
has to pass a specific implementation: new StorageService(new
LocalStorage()). A step forward would be to hide the new
LocalStorage() behind a factory new LocalStorageFactory().Create() so
that the API call is fixed but the factory can be reconfigured
somewhere, e.g. depending on the configuration.
Yeah, it is really true. So we need a place where all strategies can be stored. And we should be able to get necessary strategy from this store. So this is a place where simple factory can be used. Simple factory is not Factory method pattern and not Abstract factory.
export class StorageFactory {
#storagesByKey : Record<string, Storage> = {
'local': new LocalStorage(),
'session': new SessionStorage(),
}
getInstanceByKey(key: string) {
return this.#storagesByKey[key];
}
}
and then you can get instance of desired storage easier:
const storageFactory = new StorageFactory();
const storage = new StorageService(storageFactory.getInstanceByKey('local'))
storage.store('some key', 'some value')
UPDATE 1
Can you show me how would this be implemented in Angular? Which one is
Injectable(), and how do I use one service in a component ngOnInit for
example?
It looks like strategies should be injectable. There is a good post about how you can apply strategy pattern in Angluar 2+.
And its stackblitz example can be seen here.