[Note] Design Patterns in Elixir

4 minute read

This is a review note for José Valim's speech on YouTube

What's Design Pattern

A famous pic from Functional Programming Patterns (NDC London 2013)

/pattern-design-between-oop-and-fp.png

But this might cause some misunderstandings about design patterns. An OOP programmer might think: 'Look, how clearly design patterns are defined!' Meanwhile, an FP programmer might think: 'Oh, functional programming is much better than OOP programming. There are no design patterns, everything is just functions!'

Obviously, all of these assumptions are incorrect. Borrowing the definition from 'A Pattern Language: Towns, Buildings, Construction' by Christopher Alexander, Sara Ishikawa, and Murray Silverstein with Max Jacobson, Ingrid Fiksdahl-King, and Shlomo Angel:

– Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice.

Design patterns are not about the solutions; they are about recurring problems. Therefore, design patterns are not exclusive to OOP. Functional programming languages do not need to translate design patterns from OOP; they have their own design patterns.

Design Pattern Overview

Generally there are 23 design patterns, here we just talk about some of them which most specially.

01 Mediator

Problem

Imagine a GUI application with inputs, buttons, radio buttons, and dropdowns. The problem arises when changing any of these elements, such as selecting an option, requires updating other related elements. This can become very complex and require a massive amount of work. It's a very common scenario in modern development.

The Mediator Design Pattern provides an intuitive solution. Each component doesn't need to worry about how the others work. Instead, they all communicate with the mediator, which then decides how to coordinate their interactions.

Examples in Elixir

  • Phoenix Controllers, Phoenix LiveView

Solutions in Elixir

  • Functions(and modules) for controlling and coordinating logic

  • Processes for controlling and coordinating events

02 Facade

Problem

/facade.png

Imagine you have many subsystems, each containing numerous modules and components that need to communicate with each other. This can become very complex and overwhelming.

The Facade Design Pattern acts as an intermediary between these internal subsystems and the external systems. All interactions are routed through the facade, which provides simple, clean APIs for external use.

Examples in Elixir

  • Phoenix Contexts, Logger, the Elixir compiler

Solutions in Elixir

  • Modules providing a simplified API to the more general facilities of a subsystem

03 Strategy

Problem

/strategy.png The Strategy Design Pattern solves the problem of selecting and implementing an algorithm at runtime. It allows a class to delegate the implementation of a specific behavior to different algorithmic strategies, making the code more flexible and easier to maintain.

Example in Elixir

Solutions in Elixir

  • Pattern matching

 1def compose(text, dimensions, strategy) do
 2  ...
 3  case strategy do
 4    :simple -> simple_compose(...)
 5    :tex -> tex_compost(...)
 6    :array -> array_compose(...)
 7  end
 8end
 9
10compose(text, dimensions, :simple)
11compose(text, dimensions, :tex)
  • Anonymous functions

 1def compost(text, dimensions, strategy) do
 2  ...
 3  strategy.(...)
 4end
 5
 6def simple_compose(text, dimensions) do
 7  compose(text, dimensions, fn ... end)
 8end
 9
10def tex_compose(text, dimensions) do
11  compose(text, dimensions, fn ... end)
12end
  • Behaviours

1@callback compose(text, dimensions)
2
3def compost(text, dimensions, strategy) do
4  ...
5  strategy.compose()
6end
7
8compose(text, dimensions, SimpleComposer)
9compose(text, dimensions, TexComposer)
  • Protocols

 1defprotocol Composer do
 2  def compose(text, dimensions)
 3end
 4
 5def compose(text, dimensions, strategy) do
 6  ...
 7  Composer.compose(...)
 8end
 9
10compose(text, dimensions, %SimpleComposer{})
11compose(text, dimensions, %TexComposer{})

04 Observer

Problem

The Observer Design Pattern addresses the problem of maintaining consistency between related objects without creating tight coupling between them. It provides a way for an object (the subject) to notify a list of dependent objects (observers) about changes in its state, so they can update themselves accordingly.

Examples in Elixir

  • Phoenix PubSub, Registry, telemetry

Solutions in Elixir

  • Often message-passing across processes

  • May be local or distributed

Design Patterns We No Longer Need in Elixir

We've introduced four design patterns in Elixir. In fact, many design patterns become unnecessary due to the features of Elixir and functional programming:

/design-patterns.png

There's some instance:

Flyweight

The Flyweight Design Pattern addresses the problem of efficiently managing a large number of fine-grained objects in order to reduce memory usage and improve performance. It achieves this by sharing common parts of the object state among multiple objects, rather than creating separate instances for each object.

It's a common problem in OOP language like:

1function a() { new Character("a") }
2function b() { new Character("b") }
3function c() { new Character("c") }

Because the `Character` is an object, every time you call it is's a new instance. It has his own state, own behavior, so that means each of them have their space in memorys.

And how we implement in Elixir with the same thing:

1def a, do: %Character{ char: ?a }
2def b, do: %Character{ char: ?b }
3def c, do: %Character{ char: ?c }

Our character is just data here, there's no behavior attached to it, there's no mutability, so the compiler just make a instance of this struct, and any time you call the function, the compiler just point it to the same place in memory. Obviously, the problem which need be solved by Flyweight will not appear in Elixir.

Interpreter

The Interpreter Design Pattern addresses the problem of designing and implementing a language interpreter for a specific language or notation. It provides a way to evaluate sentences in a language by representing its grammar with a class hierarchy and defining an interpretation method for each grammar rule.

Solution in Elixir

1{:sequence,
2  "raining",
3  {:repeat, {:or, "dogs", "cats"}}}
4
5def interpreter({:sequence, left, right}, context)
6def interpreter({:repeat, node}, context)
7def interpreter({:or, left, right}, context)
8def interpreter(literal, context) when is_binary(literal)

Conclusion

  • Design patterns are applicable to Elixir

  • Elixir decomposes objects into 3 dimensions

  • Behaviours, Protocols, and message passing are tools for decoupling(rule of least expressiveness)