Fl4m3Ph03n1x

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:

  1. 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
  1. 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
  1. 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

  1. Is this a signal my interface is poorly designed?
  2. How can I hide implementation details while still making supervision trees possible?
  3. 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

rvirding

Creator of Erlang

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.

13
Post #5
pragdave

pragdave

Author of Programming Elixir

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

rvirding

Creator of Erlang

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.

Where Next?

Popular in Discussions Top

jswny
I would like to better understand what the advantages/disadvantages of umbrella applications are compared to structuring your app as as s...
New
Jayshua
I recently came across the javascript library htmx. It reminded me a lot of liveview so I thought the community here might be interested....
New
vans163
So useless benchmarks aside, Its possible to write a webserver that can serve 300k requests per second (perhaps more with optimizations)....
New
cvkmohan
The upcoming Phoenix 1.6 release looks very interesting. Became a habit to watch the commits - and - what they are bringing in. phx.gen...
New
Qqwy
Looking at the stacks that existing large companies have used, WhatsApp internally uses Mnesia to store the messages, while Discord uses ...
New
Fl4m3Ph03n1x
Background This question comes mainly from my ignorance. Today is Black Friday, one of my favorite days of the year to buy books. One boo...
New
ricklove
I was just introduced to Elixir and Phoenix. I was told about the 2 million websocket test that was done 2 years ago. From my research, t...
New
nunobernardes99
Hi there Elixir friends :vulcan_salute: In a recent task I was on, I needed to check in two dates which of them is the maximum and which...
New
hazardfn
I suppose this question is effectively hackney vs. ibrowse but we are at a point in our project where we have to make a choice between th...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New

Other popular topics Top

marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement