Atom-based proxy to modules that adhere to a behaviour

Hi :wave:
I came across the need for implementing the proxy pattern in Elixir. In particular, I have the concept of content source in my codebase, which represents a place where content can be obtained from and that’s represented by a behaviour ContentSource, and multiple modules that adhere to the behaviour and that represent multiple content sources: GitHubContentSource, ShopifyContentSource

I’d like to have a module that acts as a proxy and is able to proxy the calls to the right module at runtime based on an identifier (represented by an atom) that represents the content source. For example:

defmodule ContentSource do
  @callback get_content(id :: String.t() :: { :ok, String.t() } | { :error, any() }
end

defmodule ContentSources do
  def get_content(content_source, id) do
    case content_source do
      :github -> GitHubContentSource.get_content(id)
      # ...
    end
  end
end

My question is, is there an eloquent way in Elixir to achieve the above without having to manually every single function of the behaviour with a case in it? I thought about writing a macro that does that automatically given a behaviour and a list of content sources (a pair atom & module name), but I wanted to check if there’s a different Elixir feature at our disposal that I should be using instead.

Thanks in advance,
Pedro

I see absolutely nothing wrong with that, I say go for it.

I’m sure there is a more elegant way, but this worked for me.

defmodule App do
  @callback list_actions(id :: atom(), param :: String.t()) ::
              {:ok, list(Action.t())} | {:error, any()}
end

defmodule Apps.GitHub do
  @behaviour App

  def list_actions(_id, param) do
    # ...
  end
end

defmodule Apps.GitLab do
  @behaviour App

  def list_actions(_id, param) do
    # ...
  end
end

defmodule Apps do
  @behaviour App

  defp get_app(id) do
    case id do
      :github ->
        Apps.GitHub

      :gitlab ->
        Apps.GitLab
        # ...
    end
  end

  def list_actions(id, param) do
    get_app(id).list_actions(id, param)
  end
end

defmodule SomeModule do
  def run do
    Apps.list_actions(:gitlab, "something")
  end
end
1 Like

The example posted by @puemos is the one I’ve seen implemented many times.

In Elixir, being a dynamic language, I’d just use content_source.get_content(id) without doing “dynamic dispatch” like in your example because you’re not doing anything to the result of or the arguments to the ContentSource implementer.

If I’m writing a library and want to provide a shortcut for a module included in the library, like with Plug.Parsers, I might consider using this so called pattern.

Thank you folks! I’ll find your suggestion and if I find myself writing a lot of code manually I’ll adventure through the macros path.