Background
I have recently finished @pragdave 's online course and I was rather happy with all the architectural insight I got from it. Dave specifies in his course that his approach differs from the one used by the community (no surprises here for me) but I didn’t think this would impact me that much … until I started using process trees.
Here is a small example on how Dave would organize an app:
- Interface file. It delegates to a server in this case.
defmodule FootbalEngine.Populator do
@moduledoc """
Interface for the populator that fills up the memory table (populates it) with
data.
"""
alias FootbalEngine.Populator.Server
@spec new(String.t) :: GenServer.on_start
def new(path), do: Server.start_link(path)
end
- Server file. It has all the OTP logic and GenServer behaviours and callbacks:
defmodule FootbalEngine.Populator.Server do
@moduledoc """
Server for the Cache. Tries to populate it with data and if it gets anything
other than a complete success for the indexation, it will keep trying to
repopulate the memory tables.
"""
use GenServer
alias FootbalEngine.Populator.Cache
###############
# Public API #
###############
@spec start_link(String.t) :: GenServer.on_start
def start_link(path), do:
GenServer.start_link(__MODULE__, path)
###############
# Callbacks #
###############
@impl GenServer
@spec init(String.t) :: {:ok, String.t} | {:stop, any}
def init(file_path) do
:persistent_term.put(:indexation_status, :initializing)
check_file_with_msg(file_path, {:ok, file_path})
end
@impl GenServer
def handle_info({:check_status}, file_path), do:
check_file_with_msg(file_path, {:noreply, file_path})
###############
# Aux Functs #
###############
@spec check_file_with_msg(String.t, any) :: any
defp check_file_with_msg(file_path, msg) do
case Cache.populate(file_path) do
status = {:ok, :indexation_successful} ->
:persistent_term.put(:indexation_status, status)
bad_status ->
:persistent_term.put(:indexation_status, bad_status)
{:ok, _ref} = :timer.send_after(15_000, {:check_status})
end
msg
end
end
- Logic file. Contains the logic used by the GenServer.
defmodule FootbalEngine.Populator.Cache do
@moduledoc """
Reads the CSV file, validates and parses its data and then populates the
memory tables (the DB) with it's information.
"""
#logic code here
# def populate .....
end
Here we have a really good separation of concerns:
- one file for the interface
- one file for OTP and GenServer behaviours
- one file for the program’s logic
The challenge
So, now that we have this neat interface it’s time to use it. Let’s say I have an OTP app, and I want to add populator
to my supervision tree, as is normal in Elixir apps.
How would I do it?
The ideal solution would be to use the Interface file, namely using FootbalEngine.new/1
.
children = [
{FootbalEngine, file_path}
]
opts = [strategy: :one_for_one, name: FootbalInterface.Supervisor]
Supervisor.start_link(children, opts)
But if you try it, you will soon realize it fails. It fails because FootbalEngine
is not an OTP compatible behaviour, it doesn’t even have a childspec
function. It is simply an interface that delegates to another module.
The obvious solution here is to fix it the following way:
children = [
{FootbalEngine.Populator.Server, file_path}
]
opts = [strategy: :one_for_one, name: FootbalInterface.Supervisor]
Supervisor.start_link(children, opts)
But this breaks the encapsulation principle Dave has tried to create by placing the Server module behind an interface. We, the dummy users, are not supposed to know Populator uses a GenServer behind the scenes. We are only supposed to know about it’s interface.
Paradox
So now I have a paradox. I want to have an interface that hides implementation details (such as, does this use a GenServer, or GenStage or does this even use processes?) but at the same time, if I want to make an OTP supervision tree, I need to expose these details and break the encapsulation of my interface.
Questions
- Is this a signal my interface is poorly designed?
- How can I hide implementation details while still making supervision trees possible?
- Are these 2 approached incompatible in nature? (should I just quit trying to separate concerns like Dave does in his courses?)
Your opinions and ideas are welcome !