Background
I am reading Designing Elixir Systems with OTP where they introduce the concept of Single Level of Abstraction.
Code
This concept looks rather bizarre and abstract but with an example it becomes easier to understand. Using the examples in the book, imagine we have a starting code like the following:
def select_question(quiz), do:
quiz
|> Map.put( :current_question, select_a_random_question(quiz) )
|> move_template(:used)
|> reset_template_cycle
This code is not horribly hard to read, but we are dealing with 2 levels of abstraction here:
- The
Map.put
deals with Elixir basic data types (map) - The rest of the code affects an abstraction called question.
So based on this principle, the authors proposed a new version of the code:
def select_question(quiz), do:
quiz
|> pick_current_question()
|> move_template(:used)
|> reset_template_cycle()
defp pick_current_question(quiz), do:
Map.put(quiz, :current_question, select_a_random_question(quiz))
My issues
This principle looks nice, but I have a couple of issues with it:
- It adds a level of indirection
- When is enough enough? When do you know when to use it?
To better explain, I will give another example from the book:
def new(%Template{} = template), do:
template.generators
|> Enum.map(&build_substitution/1)
|> evaluate(template)
This piece of code also deals with 2 abstractions:
- Enumerable data types from elixir
- The concept of a template
So, according to this rule, the code could be improved:
def new(%Template{} = template), do:
template.generators
|> generate_substitutions()
|> evaluate(template)
defp generate_substitutions(%{substitution: _sub} = generator), do:
Enum.map(&build_substitution/1)
However the authors choose not to do it.
Question
I wonder if they decided the level of indirection was not worth it. If so, how and when do you decide the indirection is worth it or not? What criteria do you usually use?