3

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?

1
  • I took the freedom to remove some meaningless buzzwords from your questions which often lead to downvotes and close votes on this site. Please double check if I got your intentions right.
    – Doc Brown
    Feb 28, 2020 at 7:00

1 Answer 1

3

In short: No.

You have already simplified the interface, and provided an implementation agnostic means of "selecting"/"connecting" a back-end.

You are using the Pattern exactly as its intended to be used. There is one nitpick below, but that is more a mater of taste rather than anything else.


There is however an alternative of comparable complexity:

You could remove the factory, and make the PermissionService itself (or an implementation of it) responsible for selecting an appropriate strategy (or set of strategies) to full-fill each request.

This sacrifices contextual optimisation as each request has to be considered on its own merits as to which strategy/strategies should be tried and in what order.

  • On the downside this might cause several strategies to be applied slowing down overall execution, or alternately force requests to be more detailed.
  • On the upside this offers a higher degree of flexibility as you would be able to trivially mix and match users/groups at the application/organsiation levels without forcing each module to understand what kind of PermissionService it needs first.

Nit Pick: No Reason why the PermissionServiceFactoryService can't also just be a PermissionFactory...

This way you can pass any specific subset/implementation down that makes sense in the greater context.

export class PermissionServiceFactoryService extends PermissionFactory {
  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];
  }

  applies(params: PermissionServiceParams): boolean {
    const factories = this._factories.filter(fac => fac.applies(params));
    return !!factories && factories.length === 1;
  }

  create(params: PermissionServiceParams): PermissionService {
    return getFactory(PermissionServiceParams).create(PermissionServiceParams);
  }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.