0

I've created a class EventBus. This class provides an interface for attaching on/off/once methods to a class, the class extending it can in turn provide an array of possible events that can be listened to.

Currently, to make it work I need to extend the class with EventBus. I feel this isn't the right approach;

class Something extends EventBus {
   private events: Array<string> = [
    'ready',
  ];

  constructor() {
    this.registerEventBus(this.events);
  }
}

EventBus is initialised using a method, not a constructor, though a constructor can also be used;

class EventBus {
  private eventListeners: EventListeners = {};

  registerEventBus(eventListenersArray: Array<string> = []) {
    eventListenersArray.forEach((listener) => {
      this.eventListeners[listener] = [];
    });
  }
  ...on, off, supporting private methods etc

I'm wondering what the right approach for this is. I feel extending something with EventBus isn't correct inheritance, EventBus is a dependency that's used. My issue is that events should be chainable, returning the correct context after each call;

const someInstance = new Something();

someInstance
  .on('ready', () => {})
  .on('destroyed', () => {});

Currently I struggle to find a solution to this that works with typescript, I know it should be injected but I fail to do so. If anyone know the correct term for the approach I should take, or a sample of code that I can use to circumvent the extends, I would be very grateful!

Thieu
  • 168
  • 6

1 Answers1

0

yeah, you are right that publish subscriber pattern should not be inherited to be used through application. You can use ioc container or create a single instance of your event bus.

An example of singleton pattern in TypeScript:

class MyEventBus
{
    private static _instance: MyEventBus;

    private constructor()
    {
        //...
    }

    public static get Instance()
    {
        // Do you need arguments? Make it a regular static method instead.
        return this._instance || (this._instance = new this());
    }
}

and use it like this:

const MyEventBusInstance = MyEventBus.Instance;

An example of creating single instance of event bus can be seen here

UPDATE:

If you want to have an instance of event bus per instance of class, then you can create instance of event bus in your class:

class Something  {
    eventBus: EventBus

    constructor() {
        this.eventBus = new EventBus;
    }
}

UPDATE 1:

If you want EventBus to be available through hierarchy of classes, then you can create some hierarchy of your classes. E.g. "Animal", "Dog" and just declare event bus in base class "Animal". By doing this, methods will be available in all derived types.

class EventBus {
    runMe() {
        console.log("EventBus is run!");
    }
}

class Animal {
  eventBus: EventBus = new EventBus();

  move(distanceInMeters: number = 0) {
    console.log(`Animal moved ${distanceInMeters}m.`);
  }
}
 
class Dog extends Animal {
  bark() {
    console.log("Wooof! Wooof! Wooof!");
  }
}

and run it like this:

const dog = new Dog();
dog.bark(); // "Wooof! Wooof! Wooof!" 
dog.move(11); // "Animal moved 11m."
dog.eventBus.runMe() // "EventBus is run!"

UPDATE 2:

If you want to have chainable methods, then you can use Fluent interface pattern. Let me show an example of code:

class EventBus {
    onFoo(eventName: string) {
        console.log("EventName is: ", eventName);
        return this;
    }
}

class Animal extends EventBus {
  eventBus: EventBus = new EventBus();

  move(distanceInMeters: number = 0) {
    console.log(`Animal moved ${distanceInMeters}m.`);
    this.onFoo("Hello").onFoo("World").onFoo("!"); // fluent interface pattern
  }
}
 
const dog = new Animal();
dog.move(10);
StepUp
  • 36,391
  • 15
  • 88
  • 148
  • Thank you for the response, however, I'm not looking for a global or singleton event bus. Each instance of a class which has the eventbus dependency should have its own eventbus. So if I create 2 instances of "Something" each has their event queue and instance. Would the IOC container be the solution in that case? Do you happen to have an example on how I could set this up? – Thieu Jun 07 '22 at 16:46
  • @Thieu Please, see my updated reply. I've shown the way to create new instance per class. Thanks! – StepUp Jun 08 '22 at 05:06
  • @Thieu Feel free to ask any question. If you feel that my reply is helpful, then you can upvote or mark my reply as an answer. [How does accepting an answer work?](https://meta.stackexchange.com/a/5235/309682) – StepUp Jun 08 '22 at 11:40
  • This is getting closer to what I'm looking for, but I'm looking for something more akin to PHP traits, if you're familiar with that. Where the methods of EventBus would become available on Something's top-level. I realise it's basically boiling down to the exact functionality "extends" provides, but just presented differently, and not restricted to a single "extend". In your example, a new instance on Something would require someone to listen to events by doing somethingInstance.eventBus.on('event') instead of somethingInstance.on('event') – Thieu Jun 09 '22 at 05:07
  • @Thieu please, see my updated answer – StepUp Jun 10 '22 at 07:27
  • Sorry I'm afraid this either isn't feasibly possible or you aren't able to help me.. I just want to merge EventBus into another class, EXACTLY like how "extends" works, but not using extends as it's canonically invalid, and make sure typescript is aware of what that means. I'm very surprised this isn't a common use case, there's multiple classes I want to add a basic event bus structure to, maybe I should just use nested extends, everything originating from a base class containing functionality many of my classes need. As I mentioned, I'm looking for "somethingInstance.on('event')" – Thieu Jun 11 '22 at 22:03
  • @Thieu I don’t think it is really good approach like in your post, however, you can try to use it. If you want to chain your calls of your event bus, then you can use “Fluent interface” pattern. It can be implemented by returning “this” from your chainable methods – StepUp Jun 12 '22 at 08:01
  • @Thieu you can use any approach you want, just wanted to show eligible ways in my opinion – StepUp Jun 12 '22 at 08:02
  • @Thieu please, see my updated answer. I've added an example with fluent interface pattern. This is how you can make methods to be chained. – StepUp Jun 14 '22 at 05:16