Hey all,
I’m wondering how to articulate what I consider a design smell, and I’d appreciate hearing how you think about this topic to clarify and crystalize my thinking. Including if this isn’t a smell at all to you!, perhaps I got it all backwards
I brushed up against a genserver pattern with functions that delegate to another module’s functions, and doesn’t really do anything stateful beyond capturing a piece of data. Like this:
defmodule MyApp.FooClient do
use GenServer
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def get_file(pid, file_path) do
GenServer.call(pid, {:get_file, "specific_dir/" <> file_path})
end
@impl GenServer
def init(opts) do
conf = Application.get_env...
{:ok, conn} = MyApp.OtherClient.connect(conf)
{:ok, conn}
end
@impl GenServer
def handle_call({:get_file, file_path}, _from, conn) do
result = MyApp.OtherClient.read_file(conn, file_path)
{:reply, result, state}
end
end
Here, MyApp.FooClient
genserver captures a conn
object on init which it passes into MyApp.OtherClient
functions, and there is some light filepath concatenating in the client function. But that’s all the genserver does. As I see it, it causes callers to work like this:
{:ok, pid} = MyApp.FooClient.start_link()
MyApp.FooClient.read(pid, "foo")
But IMO callers could just as well just call:
{:ok, conn} = Application.get_env... |> MyApp.FooClient.connect()
MyApp.FooClient.read(conn, "foo")
The latter keeps the Foo submodules all functional, and the former I think of as an “anemic GenServer” in reference to the timeless topics of anemic objects and anemic domains.
But maybe this is already an established term? I’m new to Elixir so I might’ve missed the official memo. And I would just love to hear how you navigate these tradeoffs, and how you articulate when and when not to reach for a GenServer, to align my thinking and terminology.