I store data in both browser's Local and Session storage. What is a good design practice for implementing Local and Session Storage services? I have a generic service for handling json.
@Injectable()
export class StorageService {
private storage: any;
constructor() {
this.storage = sessionStorage;
}
public retrieve(key: string): any {
const item = this.storage.getItem(key);
if (item && item !== 'undefined') {
return JSON.parse(this.storage.getItem(key));
}
return;
}
public store(key: string, value: any) {
this.storage.setItem(key, JSON.stringify(value));
}
public remove(key: string) {
this.storage.removeItem(key);
}
}
As you can see, currently its working with Session. I need to handle also Local.
- I can make an abstract parent class with these functions and in children provide the Local or Session instance: LocalService, SessionService -> StorageService.
- I can add a parameter to these json functions in order to know which storage I use.
public removeLocal() { .. }
public removeSession() { .. }
private remove(key: string, storage: Storage) {
storage.removeItem(key);
}
- If you have any other better idea, please let me know. At this moment, I prefer variant 1 but I'm open to everything.
CodePudding user response:
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')
CodePudding user response:
I would not use a service for this. It can be a simple class suited to every usage.
class MyStorage {
constructor(
private storage: Storage,
private readonly prefix = '',
) {}
private createKey(key: string): string {
return this.prefix ? `${this.prefix}-${key}` : key;
}
public retrieve<T = any>(key: string): T {
const item = this.storage.getItem(this.createKey(key));
try {
if (item && item !== 'undefined') {
return JSON.parse(item);
}
} catch { }
return;
}
public store<T = any>(key: string, value: T): void {
this.storage.setItem(this.createKey(key), JSON.stringify(value));
}
public remove(key: string): void {
this.storage.removeItem(this.createKey(key));
}
}
The main selling points of this are:
prefix
- later, when you would use this multiple times at different places, the prefix will make sure you don't have name collisions.- The generics are a sort of safeguard as you can use them to validate the assigned value.
export const userSettingsSotrage = new MyStorage(localStorage, '[USER]');
userSettingsSotrage.retrieve<User>('user'); // Just a shorthand for "as User"
userSettingsSotrage.store<User>('user', userOrUndefined); // Error
userSettingsSotrage.store<User>('user', user); // OK
If you wanted more type safety, you could give the whole MyStorage
generics to define keys that exist and their types. You could even do so in a manner that would parse the value into a specific class that you want on a key basis.