Introduction
Dependency Inversion is a well-known software development principle, described by Robert C. Martin as the responsible of systems to become flexible by making source code dependencies refer only to abstractions and not to concrete items.
Dependency Inversion demonstration taken from Clean Architecture book.
Deciding which implementation will be used in a given piece of code can be very useful and is commonly used to develop mocks on tests that involves external dependencies, such as HTTP calls or event publications.
How it is used
On Java and other object oriented programming languages the principle can be achieved using an Interface to expose desired functions and then develop a Class to implement these signatures. On the other hand, Elixir makes it possible to apply this principle by defining a set of callbacks on a Behavior and implement them on another module following the defined specification.
We can see this pattern replicated on different open source libraries of Elixir community on many use cases:
Implementation example
By deriving the Dependency Inversion principle on Elixir it is possible to implement Ports and Adapters design pattern. It consists of having a Port defining the Behaviour callbacks and the functions calling the configured Adapter implementation.
Check out this quick demonstration:
defmodule MySystem.SmsNotificationPort do
@moduledoc """
Sends SMS to users using configured SMS adapter.
"""
@callback notify(user :: User.t(), message :: String.t()) :: :ok | :error
def notify(user, message), do: impl().notify(user, message)
defp impl, do: Application.get_env(:my_system, __MODULE__)
end
defmodule MySystem.TwillioSmsAdapter do
@moduledoc """
Twillio SMS adapter implementation.
"""
@behaviour MySystem.SmsNotificationPort
@impl true
def notify(user, message) do
case MySystem.HTTP.post(“http://twillio-sms.com/" <> user.phone, %{message: message}) do
{:ok, _} -> :ok
_ -> :error
end
end
end
And on config.exs we would setup:
# …
config :my_system, MySystem.SmsNotificationPort,
MySystem.TwillioSmsAdapter
Conclusion
The same principle is used on Dependency Injection design pattern, given that you can use different implementations on a piece of your software. The difference is that Dependency Injection implementation is given on compile-time whilst Ports and Adapters is on run-time.
This means you could execute the command below on a running application and change the SMS API used on your system.
Application.put_env(:my_system, MySystem.SmsNotificationPort, MySystem.ZenviaSmsAdapter)
This is very powerful because now we are able to switch running implementations on the fly. For instance, if Twillio went through an outage we could change the SMS implementation Adapter to Zenvia resulting in our users having zero impact on their SMS messages. In the future, we could automatically detect outage events on a specific SMS adapter and switch to the other one, a pattern known as Circuit Breaker.
Top comments (11)
Hey man, great article!! I always try to implement this pattern on my code. One thing that a make different, is that I use defdelegate on the port, I think that make the code more easy to understand. But thanks for the excellent article
Doesn't assigning it to
@adapter
make it a compiled variable, breaking theApplication.put_env
trick?Yes @03juan, didn't notice that
I think you're right, in this case we need to move to a private function instead
defp adapter, do: Application.get_env(:my_system, __MODULE__)
Doesn't it?
Yep! You right! I already change this implementation, and now I use Application.get_env instead
Great tip, Bruno! Thanks for the feedback
Hi Bruno,
I don't quite understand why the delegate or function call of the behavior should be made in the port, what's the porpuse?
It's because all the application use the
MySystem.SmsNotificationPort.notify/2
function?Hey Diego!! How are you?
Well, I use the delegate on the port, because the port's idea is to be an interface. The only responsibility of the port is to call the correct adapter.
Awesome content!
Paring this with mox for testing makes testing so much simpler
Yep! I like to use with Hammox! It's a really good abstraction for the mox