Fl4m3Ph03n1x

Fl4m3Ph03n1x

How would PragDave separate this code?

Background

After reading Elixir in Action I came with a (slightly modified for my evil purposes) module that I very originally call the ProcessRegistry. Now this module works and is fine, it also has a behaviour defined, but everything is in the same file and it feels tangled to me, specially after reading this post from pragmatic Dave Splitting APIs, Servers, and Implementations in Elixir.

Code

So, following is the file called ProcessRegistry:


defmodule Clint.IProcessRegistry do
  @type via_tuple_type  ::
    {:via, module, {module, {module, any}}} |
    {:via, module, {module, {module, any}, any}}

  @callback lookup(tuple)                  :: [{pid, any}]
  @callback update_with_value(tuple, any)  :: {any, any} | :error
  @callback via_tuple(tuple)               :: via_tuple_type
  @callback via_tuple(tuple, any)          :: via_tuple_type
end

defmodule Clint.ProcessRegistry do
  @behaviour Clint.IProcessRegistry

  alias Clint.IProcessRegistry

  ###############
  # Public API  #
  ###############

  @impl IProcessRegistry
  def lookup(key), do: Registry.lookup(__MODULE__, key)

  @impl IProcessRegistry
  def update_with_value(key, new_value), do:
    Registry.update_value(__MODULE__, key, fn _old_value -> new_value end )

  @impl IProcessRegistry
  def via_tuple(key), do:
    {:via, Registry, {__MODULE__, key}}

  @impl IProcessRegistry
  def via_tuple(key, value), do:
    {:via, Registry, {__MODULE__, key, value}}

  @spec start_link() :: {:ok, pid} | {:error, any}
  def start_link, do:
    Registry.start_link(keys: :unique, name: __MODULE__)

  #############
  # Callbacks #
  #############

  @spec child_spec(any) :: Supervisor.child_spec
  def child_spec(_) do
    Supervisor.child_spec(
      Registry,
      id: __MODULE__,
      start: {__MODULE__, :start_link, []}
    )
  end
end

It’s a fairly small file with 53 lines, but it has a couple of things that bother me:

  1. The Contract and the Implementation are defined in the same file. I really don’t like this.
  2. According to pragmatic Dave, the API and the implementation are mixed together.

For the first point, I don’t really think there will ever be another implementation of ProcessRegistry that doesn’t use Elixir’s Registry. Yes, I could go on an adventure and re-invent the wheel, but what for?

How many of you had to re-implement registry’s functionality for your own projects after Elixir 1.4? What is so bad about the current implementation of Registry (or so lacking) that would make you re-implement it? Short summary, I don’t really think I need a Contract for this, because I don’t see a future where ProcessRegistry will have (or need) an implementation that doesn’t use Elixir’s Registry.

The second point is even more contentious. From what I understand, even without the behaviour, Dave would not approve of my “mixing of concerns” between the API and the Registry module. He would define a module called ProcessRegistry with the API, and then inside a folder with the same name he would create a module with a completely useless name like impl and there I would have the calls to Registry.

I attack this idea based on the fact Dave likes to complain about useless folders and boilerplaty code, but I find that in my specific case, this is what I would be creating - more boilerplate and indirection.

Opinions

  1. Have you read Dave’s small article? What do you think?
  2. How can I improve this module and this code? Let me know !

Marked As Solved

LostKobrakai

LostKobrakai

A behaviour is still useful even if there’s only one implementation. It’s not only a contract which makes switching out implementations easier, but it’s also a contract between caller and implementation, which is more explicit than “oh there’s a public function on the implementation module”. So if you already have a useful behaviour keep it around.

For the one file issue: Just put them in two files and the issue is gone.

The “indirection” of behaviours you mentioned is not in the fact that you have a behaviour, but you get indirection only if you no longer call MyOnlyImpl.via_tuple(key), but you need a more flexible system, where switching out the actual implementation is possible. That part you can skip if you don’t need it. As long as the caller only uses callbacks defined in the behaviour you can add a more flexible system later at any time.

Your module hardly does stuff anyways, so spliting it up is surely overkill. Even if you like the ideas of Dave you’ll always need to make the decision if the tradeoff of more files is worth the greater abstraction. Creating more stuff for the sake of having more stuff is never a good idea. To turn the idea around. If you build a genserver and each callback has multiple hundred lines of state modification, task spawning and return value calculation in it you might be in a place where spliting up state modification, runtime message passing and worker spawning into concrete modules will be way more useful.

In my opinion “separation of concern” is a bad metric. What even is a concern? E.g. in my mind your example module is a single concern, because it bundles stuff needed to interact with a registry. Nobody else needs to know details about it but this module. You seem to not trust your instinct in that it actually is one concern.

Also Liked

pragdave

pragdave

Author of Programming Elixir

I wouldn’t particularly argue against the code you have. I’m not sure I’d have the interface module unless I knew there’d be more implementations, and I’d do the types a little differently.

  @type via_tuple_without_value   :: {:via, module, {module, {module, any}}}
  @type via_tuple_with_value(val) :: {:via, module, {module, {module, any}, val}}
  @type via_tuple                 :: via_tuple_without_value | via_tuple_with_value(any)

  @spec via_tuple(tuple)      :: via_tuple_without_value
  @spec via_tuple(tuple, any) :: via_tuple_with_value(any)

If the main module has just the API, then adding the extra behaviour seems to obscure things to my eye,

To your point about splitting APIs, what you have here is exactly what I describe. The external Registry is the implementation (only without the useless name :slight_smile: ) and this file is the API that delegates to it. Yes, it adds a little along the way, but not so much that it obscures its nature.

What I’m less keen on is the start_link/child_spec magic in here. As far as I can tell, the API code would still run fine if these two were moved into a separate module: in fact the API would run fine any time there is a Registry process called Clint.ProcessRegistry running. So I might do a little refactoring:

defmodule Clint.ProcessRegistry do

  @registry_name __MODULE__

  @type via_tuple_without_value   :: {:via, module, {module, {module, any}}}
  @type via_tuple_with_value(val) :: {:via, module, {module, {module, any}, val}}
  @type via_tuple                 :: via_tuple_without_value | via_tuple_with_value(any)

  @spec lookup(tuple) :: [{pid, any}]
  def lookup(key),
  do: Registry.lookup(@registry_name, key)


  @spec update_with_value(tuple, any) :: {any, any} | :error
  def update_with_value(key, new_value),
  do: Registry.update_value(@registry_name, key, fn _old_value -> new_value end )


  @spec via_tuple(tuple) :: via_tuple_without_value
  def via_tuple(key),
  do: {:via, Registry, {@registry_name, key}}


  @spec via_tuple(tuple, any) :: via_tuple_with_value(any)
  def via_tuple(key, value),
  do: {:via, Registry, {@registry_name, key, value}}

  
  #############################################################################
  
  defmodule Server do
    
    @registry_name Clint.ProcessRegistry
    
    def start_link,
    do: Registry.start_link(keys: :unique, name: @registry_name)

    def child_spec(_) do
      Supervisor.child_spec(
        Registry,
        id: @registry_name,
        start: {__MODULE__, :start_link, []}
      )
    end
  end
end

I haven’t run this, so it might be total BS.

I’m not keen in the duplication of the registry name between the two modules, but I couldn’t quickly come up with an alternative which wasn’t even worse :slight_smile:

The two things I prefer about this solution are:

  1. The API and the server stuff are separate, so there’s no need for a behaviour module, and

  2. The use of __MODULE__ is disambiguated: before it was used both as a process name and as the module passed to the supervisor, which made it harder for my few remaining neurons to work out what was going on.

But…

Honestly, what’s important here isn’t whether a particular structure is right or wrong. The most important thing is that we’re discussing it, and thinking about it.

Nicely done.

Dave

sasajuric

sasajuric

Author of Elixir In Action

I don’t really understand the purpose of this module. In a real project, I’d likely delete this code and use Registry directly.

If you really insist on having this module, the second best simplification would IMO be to remove the IProcessRegistry contract, and use ProcessRegistry directly.

I definitely wouldn’t split this particular code into more files. That would just lead to more indirections which would needlessly degrade the reading experience.

sasajuric

sasajuric

Author of Elixir In Action

IMO, starting required processes is the part of the API of an abstraction, just like constructor is the part of the API of a class, or new is the part of the API of some data abstraction (e.g. Map).

With the proposed code split, in order to use Clint.ProcessRegistry, I need to first start Clint.ProcessRegistry.Server. In my view this makes the usage and the subsequent reading experience more complicated. Having to start Foo in order to use Foo seems more intuitive to me.

Of course, there are some cases where it’s worth splitting the abstraction API, for example if the API becomes too large and covers a bunch of semi-related concerns. In such scenarios, I’d prefer to have the top-level module, e.g. Foo providing the childspec, and then have other API concerns exposed via “submodules”, such as Foo.ApiConcern1, Foo.ApiConcern2, … That way, if I want to use different aspects of Foo, I have to start Foo, which, again, seems more intuitive than having to start Foo.Server.

Either way, the exact approach I’d prefer is IMO highly dependent on the context. In this simple case, given the code size, I’d definitely keep the start API (i.e. the childspec) together with the rest of the API.

Last Post!

sasajuric

sasajuric

Author of Elixir In Action

Roughly speaking yeah. My position is that lifecycle functions (childspec and optional start_link) are a part of the component/service API. I’m not saying this should always be in the same file though. I do concede that lifecycle can be thought of as a separate API concern, so in the case of a larger API, we might decide to move lifecycle and some other API concerns into separate modules (in which case I’d try to keep lifecycle in the top-level module). In this particular example, I feel that the module is too small, and IMO splitting it causes more confusion with little to no tangible benefits.

I feel that the mainstream microservices approach (i.e. the approach based on one OS process per microservice) is an improvisation around deficiencies in the first chosen technology (the language of choice + runtime). I’m happy to discuss this in further details, but it’s probably better done in another thread.

Where Next?

Popular in Questions Top

lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New

Other popular topics Top

JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
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
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 44778 311
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New

We're in Beta

About us Mission Statement