3
votes

This is mostly a Functional Programming question rather than an Elixir one, but since I'm learning Elixir it would be nice if someone can answer it using that language. Even so, if someone wants to give a more general answer it'll be appreciated.

I'm an OO programmer myself and I can't wrap my head around how to change the behavior of a component based on a configuration file (for example).

Example: I have an application that loads/saves users from a database. In a production environment, I want my users to be saved and retrieved from a MongoDB database, while in development and testing I want to use an in-memory map. If I was programming given system in an OO language (Lets say Java), I would simply make an Interface named "UserRepository" with 2 implementations: "MemoryUserRepository" and "MongoDBUserRepository". I would then instantiate the corresponding Repository based on a configuration file (or hardcoding it, it doesn't matter) at startup and right after it, all the objects that interact with the Repository will never know its implementation (they will use a repository, but will never care if it's in memory or in mongo). That gives me the ability to create as many implementations as I want and the only thing I need to do to change the behavior of the system is instantiate the implementation that I want to use.

I want the same behavior but in Elixir (let's use the same example). Since it's not an Object Oriented language I can't use the above approach. Obviously I want it to be extensible (I could easily pass a String with the type of repository I want to use in each call and use pattern matching to determine what behavior to use, but that doesn't scale well because every time I'll want to add an implementation I will have to look in every piece of code I'm pattern matching the type and add the new implementation). What would be the best approach to achieve this?

Thanks in advance!

1
One correction. OO and Functional are not orthogonal to each other. True Elixir doesn’t have classes but that doesn't mean it couldn't.Onorio Catenacci

1 Answers

7
votes

Suppose you have these two (or more) repository implementations, which implement the same interface:

defmodule MyApp.Repository.Memory do
  def get(key) do
    # ...
  end

  def put(key, value) do
    # ...
  end
end

defmodule MyApp.Repository.Disk do
  def get(key) do
    # ...
  end

  def put(key, value) do
    # ...
  end
end

Then you can write a general repository module that will just forward the function calls to one of the repository backends, based on a configuration value in your config/config.exs file:

defmodule MyApp.Repository do
  @backend Application.get_env(:my_app, :repository_backend)

  defdelegate [get(key), put(key, value)], to: @backend
end

The configuration can be made so that it is environment specific (just look at the default config.exs in a mix project freshly created with mix new my_app):

# config/config.exs
import_config "#{Mix.env}.exs"

# config/dev.exs
config :my_app, repository_backend: MyApp.Repository.Memory

# config/prod.exs
config :my_app, repository_backend: MyApp.Repository.Disk

Throughout your entire code, you can then just use the MyApp.Repository module without explicitly referencing one of the specific implementations:

MyApp.Repository.put(:foo, "Hello world!")
value = MyApp.Repository.get(:foo)