SOLID Elixir

Andrey Chernykh
5 min readJan 27, 2017

Being seasoned Ruby backend developer I follow conventions, best practices, design patterns, as well as SOLID principles without a moment’s hesitation. Years of hands-on experience did their work.

I’ve been writing Elixir code only for a year. A few months took me to get used to Elixir’s functional nature and the state absence. Somehow, with frequent mind blows I got used to it and liked it. Now my Elixir code works and do what I expect (most time :)). Next step: how to turn code that is just a working code into well-written, readable, maintainable one? Well, use the same approaches: best practices, design patterns and principles. But in many cases design pattern or principle, that you have been using in OO-language for years, are hard/impossible to adapt to Elixir in the ways you got used to use them.

Today I want to speculate a bit and experiment: I’ll try to adapt (1) well-known within OOP-community SOLID principles to small piece of code written in Elixir. The goal of this article — introduce to Elixir newcomers some approaches of code organisation and to show that OOP experience is not so needless with Elixir.

(1) That’s it, I’ll try to describe my vision, based on my experience and knowledge. Comments with your thoughts are welcome.

SOLID (as we all remember) stands for:

S — Single responsibility principle

O — Open/closed principle

L — Liskov substitution principle

I — Interface segregation principle

D — Dependency inversion principle

Let’s write some code for the sake of example.

# here is the way we use Reports moduleiex> {:ok, report} = Reports.monthly_report(10)iex> {:ok, decorated_report} =  Reports.make_colorful_and_fun(report)iex> {:ok, file} = Reports.save_as_pdf(decorated_report)

This module was written in order to operate reports. It allows you to generate two reports (monthly and annual), decorate a report and save it to a file (either *.doc or *.pdf). Although, this code is very controversial, sometimes someone can write it like this. I used to write a lot of such code at the start of my developer path.

What’s wrong with it? Let’s discuss it through refactoring according to SOLID.

[S]ingle responsibility principle

Module Reports exposes a bunch of functions which are, of course, related to each other, but bear responsibility for various behaviour. At the moment we generate, decorate and save a report with one module. Although, it works, it is hard to maintain. If we need to add, for example, sending via email feature, we’ll have to add additional function/functions to the module. And as each of public function can rely on a few private functions, it can turn into mess very fast.

But we can refactor our module something like this:

Here we’ve just splitted Reports module into another two, each of them encapsulates and bear responsibility for specific behaviour.

iex> {:ok, report} = Reports.monthly_report(10)iex> {:ok, decorated_report} = report |> Reports.Decoration.make_colorful |> Reports.Decoration.make_funiex> {:ok, file} = Reports.Export.to_pdf(decorated_report)

[O]pen/closed principle

Take a look at the brand new Reports.Export module. What can be wrong with it? Recall the principle definition:

software entities should be open for extension, but closed for modification

If we’ll decide to export a report to *.xls, we’ll have to modify public interface of the module by adding the function to_xls/1.

Instead, we can rewrite Reports.Export module in order to conform to Open/closed principle.

After refactoring the module has one public function (2) to_file/2 which awaits for two arguments, where the second — a point of extension. From this moment we can add as many export options as we want without the module’s interface changing.

(2) Here we use pattern matching in order to export according to desired file format, but it can be rewritten with case/2 if you prefer it.

iex> {:ok, report} = Reports.monthly_report(10)iex> {:ok, decorated_report} = report |> Reports.Decoration.make_colorful |> Reports.Decoration.make_funiex> {:ok, file} = Reports.Export.to_file(decorated_report, :pdf)

[I]nterface segregation principle

Sorry for not following the letters order, but the example refactoring order.

many client-specific interfaces are better than one general-purpose interface

In our example Reports module itself looks like general-purpose module. And we have two reports: monthly and annual, but usually a company’s management needs far more :) Moreover, each report processing can differ from another (remember about private functions, not only public). That’s why it can be difficult to maintain and extend such general-purpose modules.

So, let’s break Report module into pieces.

Now we have two (and more in the future if needed) modules with clear usage (and processing, inner functions) distinction.

iex> {:ok, report} = Reports.MonthlyReport.generate(10)iex> {:ok, decorated_report} = report |> Reports.Decoration.make_colorful |> Reports.Decoration.make_funiex> {:ok, file} = Reports.Export.to_file(decorated_report, :pdf)

[L]iskov substitution principle & [D]ependency inversion principle

Liskov substitution principle: “objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program”

Dependency inversion principle: “depend upon abstractions, not concretions”

Liskov substitution principle seems to be about inheritance. When base type could be replaced by its subtype without any obstacles. It’s hard to figure out how to apply this principle to Elixir, since it doesn’t have OOP-like inheritance.

But personally I think about this principle in common as the “principle of expectations”: if you replace base type by its subtype you expect the same behaviour. Well, Elixir even has behaviours and they are about expectations: when you define a behaviour and then apply it to modules (adopters), in the future you can expect from the each adopter that it provides necessary functionality.

A lot of code in this snippet. Let’s puzzle out what we did.

We createdReports.Report module in order to define reports type and structure. Then we used this type in Reports.Decoration.Decorator behaviour definition. Reports.Decoration.FormalDecorator, Reports.Decoration.ColorsDecorator and Reports.Decoration.FunDecorator are the behaviour adopters. From this moment it doesn’t matter which decorator of three we might want to use, as we can always invoke decorate/1 with the same set of arguments and get expected result (of the same type).

Elixir behaviours helped us with Dependency inversion principle as well: rather than depend on certain decorator and duplicate code, Reports.Decoration.apply/2 expects a report and a module, which adopts Reports.Decoration.Decorator Then it just invokes decorate/1 on decorator and get the result. So, from now we don’t stick with concrete argument, we depend on Reports.Decoration.Decorator abstraction and don’t need to think about its inner implementation.

iex> {:ok, report} = Reports.MonthlyReport.generate(10)iex> {:ok, decorated_report} = report |> Reports.Decoration.apply(Reports.Decoration.ColorsDecorator) |> Reports.Decoration.apply(Reports.Decoration.FunDecorator)iex> {:ok, file} = Reports.Export.to_file(decorated_report, :pdf)

Here is below what we’ve got at the end.

And recall from what we started our refactoring.

Seems like we wrote a lot of code instead of one initial module. But if we fill the initial module with implementation and try to add some more reports (and we’ll be very lucky if those reports will differ in a logic a little) or export features, we’ll see how difficult it can be and how public interface of the module will start to grow, turning into one massive hulking God-module.

The result code, on other hand, is much more extendable and maintainable, despite there amount of modules. It’s much easier to add a new report or decorator, change export abilities. And the behaviour brought some type specification and checking as a bonus.

Looking forward for your feedback: thoughts, questions, likes, shares, etc. :)

--

--