Earlier I said that what Elixir calls an application, most people would call a component or a service. That’s certainly what our sequence server is: a freestanding chunk of code that enjoyed generating successive numbers.
Despite being the canonical way of writing this, I don’t like my implementation. It puts three things into a single source file:
Have another look at the code here. If you didn’t know what it did, how would you find out? Where’s the code that does the component’s logic? (The image gives you a hint.) It isn’t obvious, and this is just a trivial service. Imagine working with a really complex one, with lots of logic.
That’s why I’m experimenting with splitting the API, implementation, and server into three separate files.
We’ll start afresh:
| $ mix new sequence |
| $ cd sequence |
| $ mkdir lib/sequence |
| $ touch lib/sequence/impl.ex lib/sequence/server.ex |
| $ tree |
| ├── README.md |
| ├── config |
| │ └── config.exs |
| ├── lib |
| │ ├── sequence |
| │ │ ├── impl.ex |
| │ │ └── server.ex |
| │ └── sequence.ex |
| ├── mix.exs |
| └── test |
| ├── sequence_test.exs |
| └── test_helper.exs |
We’ll put the API in the top-level lib/sequence.ex module, and the implementation and server in the two lower-level modules.
The API is the public face of our component. It is simply the top half of the previous server module:
| defmodule Sequence do |
| |
| @server Sequence.Server |
| |
| def start_link(current_number) do |
| GenServer.start_link(@server, current_number, name: @server) |
| end |
| |
| def next_number do |
| GenServer.call(@server, :next_number) |
| end |
| |
| def increment_number(delta) do |
| GenServer.cast(@server, {:increment_number, delta}) |
| end |
| |
| |
| end |
This forwards calls on to the server implementation:
| defmodule Sequence.Server do |
| use GenServer |
| alias Sequence.Impl |
| |
| def init(initial_number) do |
| { :ok, initial_number } |
| end |
| |
| def handle_call(:next_number, _from, current_number) do |
| { :reply, current_number, Impl.next(current_number) } |
| end |
| |
| def handle_cast({:increment_number, delta}, current_number) do |
| { :noreply, Impl.increment(current_number, delta) } |
| end |
| |
| def format_status(_reason, [ _pdict, state ]) do |
| [data: [{'State', "My current state is '#{inspect state}', and I'm happy"}]] |
| end |
| end |
Unlike the previous server, this code contains no “business logic” (which in our case is adding either 1 or some delta to our state). Instead, it uses the implementation module to do this:
| defmodule Sequence.Impl do |
| |
| def next(number), do: number + 1 |
| def increment(number, delta), do: number + delta |
| |
| end |
Now, you’re probably looking at this and thinking, “all that work just to implement a counter?” And you’d be right. But this chapter isn’t about implementing a counter. It’s all about implementing real-world servers. Because Elixir makes it easy to bundle all the code for a server into one module, most people do, and then they end up with some fairly highly coupled (and hard-to-test) code.
So think of this example, but with some real, complex business logic. Imagine how you might write it.
You’d probably start with an API, just to see what it would look like. Then you might want to write and test some of the business logic. So go into the implementation module and do just that. What’s more, test that code directly as you write it: no need to run it inside a server for that.
As you progress with the implementation, you may learn things that require changes to the overall API. Feel free.
Then, when you’re feeling good about the code, add a server module and have the API use it.
The thing I like about this approach is that it leaves me a pure implementation of the actual logic on my component, independent of whether I choose to deploy it as a server. I can use (and test) the logic either as direct function calls or indirectly via a server.