Zerg Overmind
Zerg Overmind

Reputation: 1055

How to specify module implementation at compile time in Elixir?

I have an application that should support multiple types of storages. At the moment, I want it to support both S3 and swift.

Now for the question: How do I allow for my application to choose which backend it will use at the load time (as in, the config will specify whether it will be S3 or swift)?

In OOP I would have interface, and could inject a dependency. In this case, the obvious answer was GenServer. But I don't actually need a whole dedicated process (which should also act as a chokepoint for my code).

I considered simply passing the reference to a module as a parameter, but it feels kinda iffy, since technically an incorrect implementation could be passed.

Thus to further specify: How do I go about injecting a specific backend (S3 or swift) into my code based on config (without Genserver)?

defmodule App.Filestorage do
    @callback store(String.t) :: :ok | :error
end

defmodule App.S3 do
    @behaviour App.Filestorage
    @impl
    def store(path), do: :ok 
end

defmodule App.Swift do
    @behaviour App.Filestorage
    @impl
    def store(path), do: :ok
end

defmodule App.Foo do
    def do_stuff()
        # doing some stuff
        App.Filestorage.store(result_file) # replace this with S3 or Swift
    end
end

Upvotes: 1

Views: 403

Answers (1)

apelsinka223
apelsinka223

Reputation: 533

You are on the right way! To use injections by configs, you could specify name of the selected executable module and get its name from config to call it:

config:
config :app, :filestorage, App.S3


defmodule App.Filestorage.Behaviour do
    @callback store(String.t) :: :ok | :error
end

defmodule App.Filestorage do
  def adapter(), do: Application.get_env(:app, :filestorage)
  def store(string), to: adapter().store
end

defmodule App.S3 do
    @behaviour App.Filestorage.Behaviour
    @impl true
    def store(path), do: :ok 
end

defmodule App.Swift do
    @behaviour App.Filestorage.Behaviour
    @impl true
    def store(path), do: :ok
end

defmodule App.Foo do
    def do_stuff()
        # doing some stuff
        App.Filestorage.store(result_file) # replace this with S3 or Swift
    end
end

NOTE 1: You could merge behaviour(App.Filestorage.Behaviour) and "implemention"(App.Filestorage) modules if you want

NOTE 2: You could use a module attribute to specify the adapter from config, but be aware of side effects during deploy, because it will save the exact config that would be during compile time

NOTE 3: If you use adapter specification by function, same as is it in the example, you could even change the selected implementation during runtime, by changing config

More details at the posts: http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/ https://blog.carbonfive.com/lightweight-dependency-injection-in-elixir-without-the-tears/

Upvotes: 2

Related Questions