As we mentioned in our previous post, some of us at SB attended ElixirConf EU 2018 in Warsaw, last April. We already covered some of the most interesting topics in the post and here’s another one that we think you may enjoy: it’s the summary of Georgina McFadyen’s talk on SOLID properties in functional languages and Elixir.
It’s not enough to have working code: we need to make it clean, maintainable, testable; in other words apply some design to it. Here’s where SOLID principles came in for OO languages. The question Georgina McFadyen asked in the talk is: could we apply those principles to Elixir (and functional languages more generally)?
The talk was a good refresher on SOLID principles and how to apply them to an Elixir codebase. Wikipedia has extensive definition of all the principles, but here I’m using McFadyen’s shorthands, which were great to get to the core of each principle.
Single Responsibility principle (S)
Every module or class should be responsible for a specific part of your system.
Also, “a class should have only one reason to change” — Robert C. Martin
(This still applies to Elixir if we replace “class” with module or function).
- Focused unit tests
- Fewer higher level tests
- Less dependencies per test
- Similar functionality grouped
However, every module adds overhead in terms of maintenance and testing, so always ensure each module can justify its existence.
Open/close principle (O)
Ability to add new code without touching existing code.
An example of this principle is how validation benefits from a plug-in architecture, with a generic rule-checking mechanism and different validation rules, each implemented as a separate module (i.e. a “plug-in rule”).
This requires defining a contract for the rules, which can be done with Elixir Behaviours.
The architecture allows us to source rules from a configuration file.
In this architecture, adding validation to a field can be done without touching existing code and testing becomes easier (you can have a config file for test and one for other environments).
Liskov Substitution Principle (L)
In the OO world, the core of the principle is that it should be possible to use the base/parent class in place of any of its subclasses.
Elixir is functional, so it doesn’t rely on inheritance as much as OO languages. However, we can adapt the definition to Behaviours:
Where we have code that expects a generic type (i.e. Behaviour), ensure you are only using function calls defined on that Behaviour.
This really needs to be understood by watching the video: the example can’t be summarised in a few words.
Interface Segregation Principle (I)
Clients should not depend on contracts they don’t use.
In Elixir, applying this principle would mean splitting large modules or Behaviours.
Dependency Inversion (D)
The original definition is quite complex, but it could be summarised with a couple of statements:
“Separate high- and low-level layers”.
“Depend on abstraction, rather than the details”.
Example: abstract away access to a DB, so it doesn’t matter if under the hood you have a local or remote database, a fake one, or a series of mocks.
The examples presented in the talk were proof that you can apply all the SOLID principles to Elixir and still get idiomatic code. The only ill fit is Liskov’s principle, which is very much rooted in the OO world.
In the case of Elixir, then, McFadyen suggests we could use the acronym SORTID, which would translate to the following principles:
- S: have small, focused functions
- O: use higher-order functions
- RT: use pure functions for Referential Transparency
- I: use behaviours to keep interfaces small and focused
- D: use abstractions to decouple
Finally, McFadyen leaves with an open question at the end of the talk. Functional languages are immutable and rely on recursion a lot: should we create some design principles specifically for those?
You can find slides and a video of the talk on the ElixirConf EU website.