A common trend I see in Elixir projects is that modules tend to become large. Sometimes very large. This isn’t necessarily an issue, but it goes against some deep seated heuristics I have for building software.
As my Chord
project started to get complex, I repeatedly found myself reaching for a pattern to keep my module size and complexity down, while still maintaining a friendly and approachable API.
Let’s dig into some examples.
What’s the problem?
The Chord
module is the heart of my Chord
project. Using Chord
you can generate guitar chord voicings, generate possible fingerings for a given voicing, and even calculate the distances between various chord voicings and fingerings.
It’s conceivable that lots of this functionality should live directly under the Chord
module. For example, we’d want to be able to ask for Chord.voicings/1
, or Chord.fingerings/1
, or even convert a chord into a chord chart with Chord.to_string/1
.
The problem is that each of these pieces of functionality comes along with a non-trivial implementation. If we put our voicings/1
, fingerings/1
, and to_string/1
functions in the Chord
module, their implementations would likely live in the Chord
module as well. In my mind, this would quickly turn Chord
into an unmaintainable mess.
There has to be a better way.
What’s the solution?
It turns out there is a better way. And, like most better ways, it turns out that the solution to our problem is obviously simple.
Let’s use our voicings/1
function as an example. Rather than defining our voicings/1
function and implementation within our Chord
module, we’ll create a Chord.Voicing
module and define our voicings/1
function there.
defmodule Chord.Voicing do
def voicings(notes, notes_in_chord \\ nil),
do: ...
end
Now our Chord.Voicing
module is entirely concerned with the act of generating chord voicings for a given set of notes.
However, we still want this functionality available through our Chord
module. To accomplish this, we simply need to write a Chord.voicings/1
function that matches the signature of our Chord.Voicing.voicings/1
module and passes the call straight through to our Chord.Voicing
module:
defmodule Chord do
def voicings(notes, notes_in_chord \\ nil),
do: Chord.Voicing.voicings(notes, notes_in_chord)
end
We can continue on with this pattern by creating a new module to implement each of our features: Chord.Fingering
, Chord.Renderer
. From there we can flesh our our Chord
module to wire our convenience functions up to their actual implementations:
defmodule Chord do
def voicings(notes, notes_in_chord \\ nil),
do: Chord.Voicing.voicings(notes, notes_in_chord)
def to_string(chord, chord_name \\ nil),
do: Chord.Renderer.to_string(chord, chord_name)
def fingerings(chord),
do: Chord.Fingering.fingerings(chord)
end
Beautiful.
Enter Delegates
Using the above pattern, we might make a mistake when passing the arguments from our first function head into the function in our implementation module (I’ve done it before).
Thankfully, Elixir gives us a way to prevent this mistake:
defmodule Chord do
defdelegate voicings(notes, notes_in_chord \\ nil), to: Chord.Voicing
defdelegate to_string(chord, chord_name \\ nil), to: Chord.Renderer
defdelegate fingerings(chord), to: Chord.Fingering
end
Using the defdelegate
macro, we can define the interface for each of our voicings/2
, to_string/2
, and fingerings/1
functions in our Chord
module, and point each of these function heads to their implementation module.
Elixir automatically wires each of the delegated functions together, preventing any mindless developer mistakes from creeping into our codebase.
What’s in a name?
In the previous example, the Chord
module is essentially acting as a “facade” that wraps and hides the complexity of our Chord.Voicing
, Chord.Fingering
, and Chord.Renderer
modules.
I use the term “facade” loosely, and in real-life, I don’t use it at all. The “facade pattern”, and honestly all classic Gang of Four design patterns, carry baggage that I like to think I’ve let go of in my transition into the world of functional programming.
Another less weighty way to think of Chord
is as an “API module”. It’s sole purpose is to act as an “application programming interface” within our application.
What would you call this kind of pattern?