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:
- The Contract and the Implementation are defined in the same file. I really don’t like this.
- 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
- Have you read Dave’s small article? What do you think?
- How can I improve this module and this code? Let me know !
Marked As Solved
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
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
) 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 ![]()
The two things I prefer about this solution are:
-
The API and the server stuff are separate, so there’s no need for a behaviour module, and
-
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
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
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.
sasajuric
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.
sasajuric
The style might not have changed much within BEAM languages (it actually has, but more on that later), but I feel that this style is mostly unknown to non-BEAM languages. Since in those languages people typically don’t have lightweight processes, they usually resort to running multiple OS processes to split runtime activities. So for example, to start an external process registry, we would have a registry as an external OS process (e.g. etcd). To start/stop it, we would use some CLI API, probably in combination with an external lifecycle manager, such as systemd. Then we’d use some internal API (e.g. REST, possibly wrapped with some library functions) to interact with the running component. Such approach is in my view close to what you seem to be suggesting.
After using that approach myself for many years, and then also using the “BEAM way” for quite some time too, I empirically came to conclusion that I prefer the latter. I find it simpler, and at the same time more powerful and flexible. YMMV of course ![]()
In addition, it’s worth remembering that child_spec/1 thing is Elixir specific, and it’s in fact a fairly new addition to Elixir. FWIW, I think it’s one of the more interesting changes introduced by Elixir, because it makes things closer to the way I personally think about code. With child_spec & support by Elixir Supervisor, a component/service can fully encapsulate the details of how its started, which makes things simpler for the user. Stick Foo or {Foo, arg} where you want it in the supervision tree, and you can now interact with Foo using the functions from the same module (or “submodules”). To me that seems very straightforward. Again, YMMV ![]()
Well, you said earlier in this thread that it’s important that we’re discussing it, so here I am, discussing it ![]()








