creation date:
--------
I am creating a small application that acts as a broker that connects to N different services.
Any client that connects to this broker can send data to any service. This is done by sending some JSON data which contains the payload along with the name of the service that should receive it.
My code has one module per service. Each one of those modules knows how to do its thing.
Initially, I had a case where I chose what module I should call based on what the user had sent. Something along these lines:
case service_name do
"foo" ->
MyBroker.Services.Alpha
"bar" ->
MyBroker.Services.Beta
"baz" ->
MyBroker.Services.Gamma
"pum" ->
MyBroker.Services.Gamma
service_name ->
raise "Invalid service name '#{service_name}'."
end
This works fine for a handful of services. But when I added a new one I had to remember to edit this file.
Also, I added ecto. And it validating that the service name was valid was also something that I should be doing.
So, I needed a way to:
- have a list of the valid service names
- dynamically find the module for a given service name
Turns out this is something that you can already introspect with one simple erlang call to :code.all_loaded
. This is in part what IEx
does with its autocompletion.
So, after calling :code.all_loaded
what’s left is to find all the module names that match what I am looking for.
In my case, all the Services implement a specific callback and all are named Bleh.Whatever.XXXXX
.
This will return all modules which start with Bleh.Whatever.
:
defmodule Bleh do
def relevant_sub_modules do
:code.all_loaded
|> Enum.map(& elem(&1, 0))
|> Enum.filter(& String.starts_with?(Atom.to_string(&1), "#{__MODULE__}.Whatever."))
end
end
Then it’s just a matter of finding which module ‘listens’ or ‘accepts’ a specific service name. For that, I each module has to have a function called accepted_names
which returns a list of service names it can process. This is enforced by a behaviour.
Here’s the relevant code.
defmodule Bleh do
@callback accepted_names() :: List.t
@callback run :: Anything
def valid_names do
relevant_sub_modules
|> Enum.map(fn(mod) -> mod.accepted_names end)
|> List.flatten
end
def relevant_sub_modules do
:code.all_loaded
|> Enum.map(& elem(&1, 0))
|> Enum.filter(& String.starts_with?(Atom.to_string(&1), "#{__MODULE__}.Whatever."))
end
def dynamic_dispatch(name) do
relevant_sub_modules
|> Enum.find(fn(mod) -> has_name?(mod.accepted_names, name) end)
|> call_for_module
end
def call_for_module(module) do
module.run
end
def has_name?(accepted_names, name) do
Enum.find(accepted_names, fn(x) -> x == name end)
end
end
defmodule Bleh.Whatever.Alpha do
def accepted_names, do: ["foo"]
def run, do: IO.puts "I am alpha"
end
defmodule Bleh.Whatever.Beta do
def accepted_names, do: ["bar"]
def run, do: IO.puts "I am beta"
end
defmodule Bleh.Whatever.Gamma do
def accepted_names, do: ["baz", "pum"]
def run, do: IO.puts "yeeess gamma"
end
Now, in my ecto schema I can have the following validation:
def changeset(model, :create, params) do
model
|> cast(params, @required_fields, @optional_fields)
|> validate_inclusion(:service_name, Bleh.valid_names)
end
Also, I can make a function call just by knowing the service name:
Bleh.dynamic_dispatch("foo")
# would print "I am alpha"
Notes
I don’t claim this is THE way to do this. It works for me.
If anyone knows a better way to do this or a better name for it, please hit me up on twitter.
EOF