Fl4m3Ph03n1x
Separating concerns with Supervision trees
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 !
Most Liked
rvirding
I personally think that separating a server into 3 files is actually a bit too much. I use file and just be careful to keep the different types of functions in in separate sections of the file. So there will one section with the user/admin interface functions, one section with the behaviour callbacks, and a final section with any extra internal logic functions needed. This has one benefit in the it limits the exports to only those functions which actually need to be exported.
pragdave
I think the issue you’re seeing is a common one among Elixir developers: people tend to conflate the supervision structure with the design of the code. In reality, the two are distinct: supervision is about starting and stopping things, and the code is about doing things.
So the interface to your server; the API; does not include the code that knits it into the application when it starts.
I’d just put the server module in the supervisor parameters.
Dave
rvirding
I find that the interface part is generally quite small as all the functions generally do is just send of a request most of the code is in the callbacks. Separating them doesn’t give you that much. Also the interface calls and the code in the callbacks are very closely linked so they fir well together in the same module.
Now, of course, sometimes the implementation of the callbacks can result in quite a lot of code so breaking some of it out into a separate library module is a reasonable thing to do but doing that as a rule does not seem right. And finding a reasonable divide is often not that easy.
Popular in Discussions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #javascript
- #code-sync
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








