Reputation: 695
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.
public removeLocal() { .. }
public removeSession() { .. }
private remove(key: string, storage: Storage) {
storage.removeItem(key);
}
Upvotes: 1
Views: 668
Reputation: 695
Expanding the answer of @StepUp after trial and error research. https://stackoverflow.com/a/73643938/9442199
Use case: I want Local Storage Service in one component.ts
and in another my-service.ts
the Sesssion Storage Service.
Also, rename export interface Storage to IStorage. Because there is confusion between library and our own.
Please check this Stackblitz example to see the concrete implementation.
service or component.ts
constructor() {
const storageFactory = new StorageFactory();
const storage = new StorageService(storageFactory.getInstanceByKey('local'))
storage.store('some key', 'some value')
}
I have to instantiate these classes in all the constructors where I need a browser storage. This should not be the case since Angular has DI which can give me only one instance. If I have 10 components I need to write those lines in each of them.
We will start from the bottom.
So, all web tutorials use an obsolete version of private injector: Injector
without InjectionTokens. I have found a way.
This is how Strategy Pattern is implemented in Angular.
shared.module.ts
{
provide: LOCAL, // the injection token (~string identifier)
useClass: LocalStorage
},
{
provide: SESSION,
useClass: SessionStorage
},
storage-token.ts
// Tokens work as an abstract factory provider. Specific services linked to a string key in SharedModule.
export const LOCAL = new InjectionToken<string>('LOCAL');
export const SESSION = new InjectionToken<string>('SESSION');
your-class.service.ts
constructor(
@Inject(LOCAL) private localStorageService: IStorage, //The IStrategy
OR
@Inject(SESSION) private sessionStorageService: IStorage,
)
Where needed, import SharedModule
and in the component/service import storage-token.ts
and istorage.ts
.
Maybe we want to implement some customization before giving the LocalStorage class. The factory is created by the providers:[] of Angular with token identifiers. See factory in @StepUp answer.
shared.module.ts
{
provide: LOCAL,
useFactory: () => {
return new StorageFactory().getInstanceByKey('local');
}
},
{
provide: SESSION,
useFactory: () => {
return new StorageFactory().getInstanceByKey('session');
}
},
The next problem is that we have duplicate code in local and session ~ some json stringify upon retrieve and set item. Using @StepUp's service.
shared.module.ts
{
provide: LOCAL,
useFactory: () => {
return new StorageService(new StorageFactory().getInstanceByKey('local'));
}
},
{
provide: SESSION,
useFactory: () => {
return new StorageService(new StorageFactory().getInstanceByKey('session'));
}
},
@Inject(LOCAL) private localStorageService: StorageService,
@Inject(SESSION) private sessionStorageService: StorageService,
Instead of new StorageService(IStorage)
you can use Template Design Pattern and have them inherit
from an abstract class: LocalStorage extends StorageService where you put the repetitive code.
StorageService.ts
abstract getItem();
retrieve() {
const res = this.getItem();
// json stringify or additional manipulation
return res;
}
And return to having only: module.ts
useFactory: () => { return new StorageFactory().getInstanceByKey('local'); }
This guide is offering a very well structured solution for Strategy Pattern: Local, Session, Cookies storage. But he is choosing which service at the module level. If you have two components in the same module how do you choose Local for one and Session for the other? I don't see the use of this method. I paste it, because the classes are very nicely packed and respect a good design.
Strategy Pattern with Angular and Typescript
Upvotes: 0
Reputation: 38164
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:
StorageService
classAnd it is compliant with open closed principle.
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')
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.
Upvotes: 4
Reputation: 11555
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.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.
Upvotes: 1