Skip to content

Instantly share code, notes, and snippets.

@mindreframer
Forked from timruffles/dyanmic_or_di_elixir.md
Created September 5, 2016 16:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mindreframer/5af5b24c922b0df1a232d11c1268eba7 to your computer and use it in GitHub Desktop.
Save mindreframer/5af5b24c922b0df1a232d11c1268eba7 to your computer and use it in GitHub Desktop.
Approaches to dependency-injection/dynamic dispatch in elixir

In many production systems you'll want to have one module capable of talking to many potential implementations of a collaborator module (e.g a in memory cache, a redis-based cache etc). While testing it's useful to control which module the module under test is talking to.

Here are the approaches I can see. The two points that seem to divide the approaches are their tool-ability (dialyzer) and their ability to handle stateful implementations (which need a pid).

Passing modules

Modules are first class, so you can pass them in. Used in EEx, where passed module must implement a behaviour.

defmodule Cache do
  use Behaviour
  defcallback cached?(any,any) :: boolean
  defcallback put(any,any) :: nil
end

defmodule Cache.Memory do
  def put(set,x) do: Set.add set, x
  def cached?(set,x) do: Set.member? map, x
end

defmodule Cache.Redis do
  def put(redis_pid,x) do 
    {:ok,1} = Redis.set redis_pid, x, 1
  end
  def cached?(redis_pid,x) do
    {:ok,x} = Redis.get(redis_pid,x)
    x != nil
  end
end

# usage
defmodule UsesCache do
  def start(cache,cache_pid) do
    cache.put(cache_pid,:hello)
    true = cache.cached?(cache_pid,:hello)
  end
end

UsesCache.start(Cache.Memory,HashSet.new)

Similar idea to duck-typing.

  • simple
  • dializer(?)
  • modules with state - you'd have to pass a pid too, e.g {module,pid} (eugh)

Protocols + values

Write a Protocol for the functionality. You can then pass in an opaque value to collaborators, and the implementation will be decided at runtime.

  • handles stateful and stateless implementations easily
  • dialyze-able
  • requires stub implementations for testing
defprotocol Cache do
  def cached?(id,item)
  def put(id,item)
end

defmodule Cache.Memory do
  defstruct set: nil
  alias __MODULE__, as: Mod
  defimpl Cache, for: Mod do
    def put(%Mod{set: set},x) do: Set.add set, x
    def cached?(%Mod{set: set},x) do: Set.member? map, x
  end
end

defmodule Cache.Redis do
  defstruct redis: nil
  alias __MODULE__, as: Mod
  defimpl Cache, for: Mod do
    def put(%Mod{redis: redis},x) do
      {:ok,1} = Redis.set redis, x, 1
    end
    def cached?(%Mod{redis: redis},x) do
      {:ok,x} = Redis.get(redis,x)
      x != nil
    end
  end
end

# usage
defmodule UsesCache do
  def start(cache) do
    Cache.put(cache,:hello)
    true = Cache.cached?(cache,:hello)
  end
end

UsesCache.start(%CacheMemory{set:HashSet.new})

Passing callbacks

For a single method, you could just pass a function. Then in tests you pass a stub method, and in production you can wrap up the real module behind it.

def start_link({some_callback}) do
  :gen_server.start_link(@name,SomeModule,{some_callback},[])
end
    
def init({some_callback}) do
  {:ok,%State{callback: some_callback})
end

Now the callback field of state can be used by functions of this gen_server module.

# usage
defmodule UsesCache do
  def start(put,cached) do
    put.(:hello)
    true = cached.(:hello)
  end
end

# can create callbacks from anything: stateful, stateless etc
  • works with any implementation, even ad-hoc
  • dialyze-able
  • pass many values for large APIs

Stateful

Create a stateful module that holds the module, refer to that.

  • doesn't work for use-cases with many instances (pass-the-pid)
  • dialyzer - need a Behaviour for the return type of the getter

Module generation

You could generate a module based on a run-time config.

  • moving parts
  • doesn't work for use-cases with many instances (pass-the-pid)
  • dialyzer - seems likely to throw it off
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment