Making Our Server into a Component

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.

images/wheres-the-code.png

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:

otp-server/3/sequence/lib/sequence.ex
 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:

otp-server/3/sequence/lib/sequence/server.ex
 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:

otp-server/3/sequence/lib/sequence/impl.ex
 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.