Config Driven Reactive Programming in Ruby
Recently, I was examining a legacy application I’ve been working on which was based on Ruby on Rails and I was wondering if there could be any ways in which I could modify certain workflows to improve performance and configurability. Though the approach I’m following is already available through existing Ruby gems, I found it exciting to write it in a configurable manner.
Problem statement: The order management system had an order model and an order instance can have a state attached to it. We do not use any state machines to moderate the change in order states hence I thought of developing my own customizable state machine.
Approach: I will be making use of the observer pattern in Ruby to structure my classes. An observer pattern is used when there is a one-to-many relationship between objects such as if one object is modified, its dependent objects are to be notified automatically. Observer pattern falls under the behavioral pattern category.
Initially, I set up a hash constant that maps the publishers to the subscribers. For simplicity, I’ve shown this to be residing in a ruby file. In production, this can be stored in a centralized config manager or a database so the values can be configured and the change can be seen in real-time.
Next, I have shown a Delivery Service, this is the service which calls our publisher service.
The OrderStateService is the most important class and this acts as our publisher. We require Ruby’s observer module and in line 8, we filter the OBSERVER_CONFIG by our class name -> this will return all the subscribers registered for the OrderStateService publisher.
In line 9, we iterate through each subscriber and register them as an observer to the OrderStateService publisher. This action is only done if the observer has an update method implemented. This is because it’s specified in the documentation to listen through the update method.
In line 17, we indicate that a change has been made and in line 18, the event is passed to all our subscribers. We also run this asynchronously by using Ruby’s multithreading functionality.
The NotificationService which acts as a subscriber has an update method implemented and will perform any actions when a change event is registered in OrderStateService.
If a new subscriber needs to be created, we just have to create the class with an update method and register the subscriber in our OBSERVER_MAPPING config.
This is just a prototype and the solution for the problem statement can be further improved where an event such as an order state transition can have several subscribers performing multiple actions asynchronously. As this is completely config driven, the admin can easily add/remove subscribers during runtime which can help in isolating errors, performance monitoring, etc.
Advantages of this approach
- Open/Closed Principle. You can introduce new subscriber classes without having to change the publisher’s code (and vice versa if there’s a publisher interface).
- You can establish relations between objects at runtime.
- You can easily add or remove subscribers without making any code changes or redeploying.
Disadvantages of this approach
- Subscribers are notified in random order.
- Could be difficult to debug if a proper logging mechanism is not set up.
- The code is fully dependent on configs. Separate validation and security mechanisms should be set up to make sure the configs are not misused.