Event bus with configurable encapsulation and permission levels (subscribe-only/emit+subscribe)

26 Views Asked by At

Suppose we have a CompanyService which is our (singleton) event bus used for communication between classes.

class CompanyService {
  EventEmitter<Schedule> workScheduleChanged;
  // (plus many others)
}

We also have the classes Manager and Employee that have this CompanyService injected.

class Manager {
  @Injected
  CompanyService companyService;

  void changeWorkSchedule(Schedule newSchedule) {
    this.companyService.workScheduleChanged.emit(newSchedule);
  }
}
class Employee {
  @Injected
  CompanyService companyService;

  Schedule workSchedule;

  void setup() {
    this.workScheduleChanged.subscribe(newSchedule -> this.workSchedule = newSchedule);
  }

  void illegallyChangeWorkSchedule(Schedule newSchedule) {
    this.companyService.workScheduleChanged.emit(newSchedule);
  }
}

I'd like to prevent classes like Employee from directly calling emit on events inside CompanyService. But I would still like for them to be able to subscribe to said events.

I wish to somehow inject a subscribe-only version of the CompanyService class. The constraints are:

  1. No repetition
  2. No bypassing this encapsulation in an underhanded manner
  3. Type safety

The simplest solution is creating another class that wraps CompanyService's fields in a way that prevents access to their emit method:

class CompanyServiceSubscribeOnly {
  @Injected
  CompanyService companyService;

  EventSubscriber<Schedule> getWorkScheduleChanged() {
    return companyService.workScheduleChanged.asEventSubscriber();
  }

  // (plus many others)
}

(In this context, EventSubscriber can only subscribe to events, not emit them also)

This solution breaks constraint #1, as I need to create a wrapper method for each emitter of CompanyService.

Another solution would be to wrap the fields selectively, like so:

class CompanyServiceSubscribeOnly {
  @Injected
  CompanyService companyService;

  EventSubscriber<T> getAsEventSubscriber<T>((CompanyService -> EventEmitter<T>) selector) {
    return selector(companyService).asEventSubscriber();
  }
}

But instead of calling it normally like so:

companyServiceSubscribeOnly.getAsEventSubscriber(companyService -> companyService.workScheduleChanged).subscribe(...)

...nothing would prevent me from underhandedly calling emit:

companyServiceSubscribeOnly.getAsEventSubscriber(companyService -> companyService.emit(...

...which breaks constraint #2.

A last solution that I thought about involves creating just one event emitter where events are distinguished by the name under which they emit, and their names would be kept in an enum. While this complies with the first 2 constraints, it would make my events typeless i.e., there is no way of knowing at compile-time that my event called "work-schedule-changed" returns a Schedule, which breaks constraint #3.

I tried to make the examples language-agnostic as I initially bumped my head on this problem in Angular and didn't want to mess up the examples with framework-specific clutter.

I would like a solution that checks all 3 constrains in any statically-typed language or even pseudocode.

0

There are 0 best solutions below