Using dry-container to implement Inversion Of Control for Hanami::Events

Subscribe to receive new articles. No spam. Quality content.

Hi guys, today we will go through the idea of Inversion Of Control (IoC) by really interesting example: Hanami::Events gem.

Before going too deep into details of implementation, I wanted to tell you the story of my last couple weeks. I've noticed that Luca Guidi wanted to add Events to version 2 of Hanami.

I'm a big fan of event-driven systems so I decided to contribute to Hanami and offered implementation with wisper gem. I've opened PR which started a good discussion regarding details of implementation. Now we have separate experimental code base for Hanami Events.

I've been working on this project with Anton Davydov, core dev of Hanami and it's been great experience so far. I wanted to share with you what I've learned working with Anton.

We wanted to have many different adapters (backends) for Events, for example: memory, redis, kinesis or basically anything that developers might want to use for event-driven systems.

I'll simplify the actual code to show the idea. At the beginning we had something like this:

class Events
  attr_reader :adapter

  def initialize(adapter_name)
    case adapter_name
    when :memory
      @adapter = Memory.new
    when :redis
      @adapter = Redis.new(**options)
    else
      @adapter = Memory.new
    end
  end

  def broadcast(event, **payload)
    adapter.broadcast(event, payload)
  end

  def subscribe(event_name, &block)
    adapter.subscribe(event_name, block)
  end
end

I want to mention that this was just a really early stage of Hanami::Events. With this basic implementation we had initialize method that allowed us to instantiate events with selected adapter, for example:

events = Events.new(:memory)
events.subscribe('user.created') { |payload| puts payload }

In this case we instantiated Events with :memory adapter. Adapter would respond to two methods: subscribe and broadcast.

But we had a problem with this implementation. Developers should be able to add custom adapters. Gem should be flexible enough to support them.

With this implementation Events has to know about all adapters upfront:

Hanami Events IoC

We needed to change direction of those dependencies. That's when Anton opened really good PR which allowed developers to register any adapter they want and Hanami::Events would support it automatically.

To create more flexible implementation we should use Inversion Of Control, as Martin Fowler described it:

A framework embodies some abstract design, with more behavior built in. In order to use it you need to insert your behavior into various places in the framework either by subclassing or by plugging in your own classes. The framework's code then calls your code at these points.

To let developers "insert behavior" by plugging their own classes Anton suggested to use dry-container.

I'll show code that we can have with dry-container and then we will go through details to understand how it works:

require 'dry/container'

class Events
  extend Dry::Container::Mixin

  register(:memory, memoize: true) do
    require_relative 'memory'
    Memory.new
  end
end

# Outside of gem code, in application, developer can inject adapters like this:
Events.register(:redis) { Redis.new }

Look at this code. It looks much better now. We extended our class by Dry::Container::Mixin which allowed us to register basic in memory adapter that we will ship with gem, but also, now developers can inject their own adapters into framework using register function:

Events.register(:redis) { Redis.new }

Of course I've omitted couple levels of abstractions from real implementation just to show the main idea. Now gem dependencies go into other direction. Adapters depend on framework. Framework doesn't know anything about the possible adapters. All we care about is that adapters should respond to two methods: broadcast and subscribe.

Another huge benefit of dry-container is "lazy loading". If we use just one adapter, code for other adapters will never be loaded. Code inside register method for :memory adapter will be executed only at the moment when we call Events[:memory]. Also there is a way to memoize result of execution with: memoize: true param.

Dry-container provides a really good documentation and I'm really happy to see such good use case for this gem.

I wanted to mention another gem from dry set, called dry-auto_inject:

dry-auto_inject provides low-impact dependency injection and resolution support for your classes.

Probably in next articles I'll describe how I use these two gems in my project now.

Learning such things and working with really smart developers is a huge benefit of contributing to Open Source.

I'll keep you posted on anything new I learn from it. Thanks for reading!

Subscribe to receive new articles. No spam. Quality content.

Comments