I have a medium-sized Angular-based web application that I'm currently implementing some permission components for. Overall, the areas where the permissions components will be used are virtually identical, but the specific endpoints (and the parameters that need to be provided to build those endpoints) are slightly different.
As it stands now, there are 4 separate branches to the logic (and those will likely only grow over time). The 4 branches basically work out to:
- Organization
- Group
- User
- Project
- Group
- User
I've created a few basic interfaces and classes to try to facilitate this design.
First, we have my actual service interface:
export interface PermissionService {
getPermissionsList(): Observable<PermissionGroup[]>;
getAppliedPermissions(objectId: number): Observable<AppliedPermission[]>;
setObjectPermission(objectId: number, permissionId: number, allow: boolean): Observable<any>;
removeObjectPermission(objectId: number, permissionId: number): Observable<any>;
}
Next, we have the service factory interface:
export interface PermissionFactory {
applies(params: PermissionServiceParams): boolean;
create(params: PermissionServiceParams): PermissionService;
}
The factory, uses another interface PermissionServiceParams
which contains the base parameters that all requests will share, along with an enum that provides what the service's type is.
export interface PermissionServiceParams {
type: PermissionServiceType;
organization: string;
}
Finally, we have the actual implementation of my abstract factory, which is responsible for selecting the appropriate service factory to create the necessary service. It looks something like:
@Injectable({
providedIn: 'root'
})
export class PermissionServiceFactoryService {
private _factories: PermissionFactory[] = [];
constructor(organizationUserPermissionFactoryService: OrganizationUserPermissionFactoryService,
organizationGroupPermissionFactoryService: OrganizationGroupPermissionFactoryService,
projectUserPermissionFactoryService: ProjectUserPermissionFactoryService,
projectGroupPermissionFactoryService: ProjectGroupPermissionFactoryService) {
this._factories.push(organizationUserPermissionFactoryService);
this._factories.push(organizationGroupPermissionFactoryService);
this._factories.push(projectUserPermissionFactoryService);
this._factories.push(projectGroupPermissionFactoryService);
}
getFactory(params: PermissionServiceParams): PermissionFactory {
const factories = this._factories.filter(fac => fac.applies(params));
if (!factories || factories.length === 0) {
throw new Error('No matching factories found!');
} else if (factories && factories.length > 1) {
throw new Error('Ambiguous Invocation! Multiple factories apply to the provided params.');
}
return factories[0];
}
}
If it's not immediately apparent from the code above, the algorithm works by housing a list of every PermissionFactory
in the application within my concrete implementation of my abstract factory, PermissionServiceFactoryService
. Whenever getFactory
is called, the abstract factory iterates over each of the defined permission factories, and finds the one that applies to the parameters passed into the method.
Once the appropriate factory is found, the developer is then free to call the create method, like so:
const params = {organization: data.organization, type: PermissionServiceType.OrganizationUserPermissions};
this._permissionsService = permissionServiceFactoryService.getFactory(params).create(params);
The factory is responsible for passing the proper static parameters into the constructor of the corresponding permission service. So if additional parameters aside from those that are present on the base PermissionServiceParams
are required, the factory handles that.
Is this the most simple approach? It seems rather verbose, and it may be a bit of overkill for what I'm trying to accomplish?