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 !

First Post!

LostKobrakai

LostKobrakai

The paradox is not in the architectual part of your design. It’s a technical issue, because the public interface you support doesn’t match the public interface expected by the supervisor. For your public interface to support being used in supervision trees like you showed it needs to implement child_spec/1, which is what’s used by the supervisor.

You can implement it in FootbalEngine.Populator like so:

defdelegate child_spec(args), to: FootbalEngine.Populator.Server

Now your public interface supports that usage, but still doesn’t do anything by itself. It’s still just a plain wrapper module.

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 #4
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.

Last Post!

mindriot

mindriot

I think the C# feature you are thinking of is #region. While I wouldn’t advocate cramming all kinds of things in the same file, it might be worth also considering why this is actually a problem. I think there is a bit of a difference between doing this in an OO world vs a functional world. Some reasons why you would work hard to extract anything you possibly can include:
• Reducing the size of the things you have to reason about as a whole (which is a class mainly due to the amount of mutation that is or can be happening)
• increasing reuse opportunity by being able to use the extracted code in a different context
• increasing composition options by being able to configure more easily how extracted components are combined via IOC or similar.

Having regions in a class is a strong indicator that you can improve any/all of the above factors and due to those reasons we often find ourselves taking any opportunity to separate anything even if it might make more sense not to in the absence of reasons created by the language constructs.

I’m not sure what the answer to your original question really is or where that should really land (especially since you are looking for a particular persons approach and there are good answers here already), however I would probably rethink the relevance of the region story and reconsider how many of the issues that drive that notion in C# are real problems that should force us to move things into another file when already our units of reasoning, reuse and composition are smaller.

Where Next?

Popular in Discussions Top

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
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 55125 245
New
New
AstonJ
If so I (and hopefully others!) might have some tips for you :slight_smile: But first, please say which area you’re finding most challen...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
lucaong
Hello Elixir and Nerves community, I have been working for a while on an open-source embedded key-value database for Elixir, that I call...
230 14403 124
New
marciol
Please, let me know if this kind of discussion already took place in another topic . Hi all, how do you consider if is better to build ...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 131117 1222
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

We're in Beta

About us Mission Statement