Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Design patterns in Elixir

Design patterns in Elixir

Avatar for Krzysztof Kempiński

Krzysztof Kempiński

November 09, 2018
Tweet

Other Decks in Programming

Transcript

  1. Who invented that? • as an architectural concept in 1977-1979

    by Christopher Alexander • after 10 years Kent Beck and Ward Cunningham started experimenting with it in programming • “Design Patterns: Elements of Reusable Object-Oriented Software” in 1994 by "Gang of Four"
  2. What for? • reinventing the wheel • proven and tested

    solutions • increase code readability • increase code reusability • good topic for a recruitment process
  3. Design patterns and FP • software design patterns >= OO

    paradigm • OO patterns ≠ FP patterns • OO patterns == FP functions ?
  4. [Creational patterns] BUILDER defmodule ComputerBuilder do def build, do: %Computer{}

    def intel(computer), do: %{computer | cpu: :intel} def set_memory(computer, size), do: %{computer | memory: size} def add_disk(computer, drive), do: %{computer | drives: [drive|computer.drives]} def add_hard_disk(computer, size), do: add_disk(computer, size) end defmodule Computer do defstruct memory: 0, cpu: nil, drives: [] end import ComputerBuilder computer = build() |> intel |> set_memory(12_000_000_000) |> add_hard_disk(256_000_000_000)
  5. [Creational patterns] FACTORY defmodule ShapeFactory do def create(:circle, diameter: diameter),

    do: {:circle, diameter} def create(:circle, radius: radius), do: {:circle, radius*2} def create(:rectangle, width: width, height: height), do: {:rectangle, width, height} def create(:rectangle, x1: x1, x2: x2, y1: y1, y2: y2), do: {:rectangle, x2-x1, y2-y1} def create(:square, size: size), do: {:rectangle, size, size} end defmodule Graphics do def draw({:circle, d}), do: IO.puts("Drawing circle, diameter: #{d}") def draw({:rectangle, w, h}), do: IO.puts("Drawing rectangle, dimensions: #{w}*#{h}") end factory = ShapeFactory circle = factory.create(:circle, diameter: 10) Graphics.draw(circle) square = factory.create(:square, size: 20) Graphics.draw(square) rectangle = factory.create(:rectangle, width: 5, height: 15) Graphics.draw(rectangle)
  6. [Creational patterns] SINGLETON defmodule Singleton do use GenServer @name :the_singleton_name

    @initial_value "starting value" def start_link, do: GenServer.start_link(__MODULE__, @initial_value, name: @name) def value, do: GenServer.call(@name, :read) def update(value), do: GenServer.call(@name, {:write, value}) def handle_call(:read, _from, value), do: {:reply, value, value} def handle_call({:write, value}, _from, _old_value), do: {:reply, :ok, value} end Singleton.start_link Singleton.value |> IO.puts Singleton.update("new value") Singleton.value |> IO.puts
  7. [Behavioral patterns] COMMAND defmodule CopyFile do def execute(source, destination) do

    IO.puts("cp #{source} #{destination}") end def unexecute(source, destination) do IO.puts("rm #{destination}") end end defmodule CreateFile do def execute(path, data) do IO.puts(~s|echo "#{data}" > #{path}|) end def unexecute(path, _data) do IO.puts("mv #{path} /path/to/trash") end end defmodule DeleteFile do def execute(path) do IO.puts("mv #{path} /path/to/trash") end def unexecute(path) do IO.puts("mv /path/to/trash/#{Path.basename(path)} #{path}") end end
  8. [Behavioral patterns] COMMAND defmodule Runner do use GenServer def start_link

    do state = %{done: [], undone: []} GenServer.start_link(__MODULE__, state, name: __MODULE__) end def execute(commands), do: GenServer.call(__MODULE__, {:execute, commands}) def undo(), do: GenServer.call(__MODULE__, :undo) def redo(), do: GenServer.call(__MODULE__, :redo) def handle_call({:execute, commands}, _from, state) do commands = List.wrap(commands) Enum.each commands, fn command -> {mod, args} = command apply(mod, :execute, List.wrap(args)) end {:reply, :ok, %{state | done: Enum.reverse(commands) ++ state.done}} end def handle_call(:undo, _from, state=%{done: [command|t]}) do {mod, args} = command apply(mod, :unexecute, List.wrap(args)) state = %{state | done: t, undone: [command | state.undone]} {:reply, :ok, state} end def handle_call(:redo, _from, state=%{undone: [command|t]}) do {mod, args} = command apply(mod, :execute, List.wrap(args)) state = %{state | undone: t, done: [command | state.done]} {:reply, :ok, state} end end
  9. [Behavioral patterns] COMMAND Runner.start_link commands = [ {CreateFile, ["file1.txt", "hello

    world"]}, {CopyFile, ["file1.txt", "file2.txt"]}, {DeleteFile, "file1.txt"} ] Runner.execute(commands) Runner.undo Runner.undo Runner.undo Runner.redo Runner.redo Runner.redo
  10. [Behavioral patterns] OBSERVER defmodule Employee do defstruct name: nil, title:

    nil, salary: 0 end defmodule Payroll do use GenEvent def handle_event({:changed, employee}, state) do IO.puts "Cut a new check for #{employee.name}!" IO.puts "His salary is now #{employee.salary}!" {:ok, state} end end defmodule HR do use GenServer def start_link(employee, events) do state = %{events: events, employee: employee} GenServer.start_link(__MODULE__, state, name: __MODULE__) end def handle_call({:update, changes}, _from, state) do updated = Map.merge(state.employee, changes) GenEvent.notify(state.events, {:changed, updated}) {:reply, :ok, %{state | employee: updated}} end def update(changes), do: GenServer.call(__MODULE__, {:update, changes}) end
  11. [Behavioral patterns] OBSERVER fred = %Employee{name: "Fred Flinstone", title: "Crane

    Operator", salary: 30000} {:ok, pid} = GenEvent.start_link HR.start_link(fred, pid) GenEvent.add_handler(pid, Payroll, []) HR.update(%{salary: 35000})
  12. [Behavioral patterns] STRATEGY defmodule HTMLFormatter do @behaviour Formatter def output_report(context)

    do IO.puts "<html>" IO.puts " <head>" IO.puts " <title>#{context.title}</title>" IO.puts " </head>" IO.puts " <body>" Enum.each context.text, fn line -> IO.puts " <p>#{line}</p>" end IO.puts " </body>" IO.puts "</html>" end end defmodule Formatter do @callback output_report(context :: map) :: nil end defmodule PlainTextFormatter do @behaviour Formatter def output_report(context) do IO.puts "***** #{context.title} *****" Enum.each(context.text, &IO.puts/1) end end
  13. [Behavioral patterns] STRATEGY defmodule Report do use GenServer def start_link(formatter),

    do: GenServer.start_link(__MODULE__, formatter, name: __MODULE__) def output_report(context), do: GenServer.call(__MODULE__, {:output_report, context}) def change_format(formatter), do: GenServer.call(__MODULE__, {:change_format, formatter}) def init(formatter), do: {:ok, formatter} def handle_call({:output_report, context}, _from, formatter) do formatter.output_report(context) {:reply, :ok, formatter} end def handle_call({:change_format, new_formatter}, _from, _state), do: {:reply, :ok, new_formatter} end
  14. [Behavioral patterns] TEMPLATE METHOD def output_lines, do: Enum.each(@text, &output_line/1) def

    output_line(line), do: raise(ArgumentError, "not implemented") def output_body_end, do: nil def output_end, do: nil defoverridable [output_start: 0, output_head: 0, output_body_start: 0, output_line: 1, output_body_end: 0, output_end: 0] end end end defmodule Report do defmacro __using__(_) do quote do @title "Monthly Report" @text ["Things are going", "really really well"] def output_report do output_start output_head output_body_start output_lines output_body_end output_end end def output_start, do: nil def output_head, do: nil def output_body_start, do: nil
  15. [Behavioral patterns] TEMPLATE METHOD defmodule PlainTextReport do use Report def

    output_head, do: IO.puts "**** #{@title} ****\n" def output_line(line), do: IO.puts line end defmodule HTMLReport do use Report def output_start, do: IO.puts "<html>" def output_head do IO.puts " <head>" IO.puts " <title>#{@title}</title>" IO.puts " </head>" end def output_body_start, do: IO.puts "<body>" def output_line(line), do: IO.puts " <p>#{line}</p>" def output_body_end, do: IO.puts "</body>" def output_end, do: IO.puts "</html>" end
  16. [Structural patterns] COMPOSITE import Cooking cake = part(:cake, [ part(:batter,

    [ ingredient(:flour, 1, :cups), ingredient(:sugar, 0.5, :cups), ingredient(:eggs, 2) ]), part(:frosting, [ ingredient(:sugar, 100, :grams), ingredient(:lemon_juice, 2, :tbl) ]) ]) defmodule Cooking do def part(name, ingredients), do: {:branch, name, ingredients} def ingredient(name, quantity, unit \\ :units), do: {:leaf, name, quantity, unit} end
  17. [Structural patterns] FACADE # Facade delegate to underlying components defmodule

    MySystem do defdelegate a, to: MySystem.ComponentA defdelegate b, to: MySystem.ComponentB def c(param), do: MySystem.ComponentB.d(param, 2) end # Call facade MySystem.a() MySystem.b() MySystem.c(2) # Private implementation details defmodule MySystem.ComponentA do def a, do: IO.puts "component a" end # More private implementation details defmodule MySystem.ComponentB do def b, do: IO.puts "component b" def d(param, count), do: param * count end
  18. [Structural patterns] PROXY def handle_call({:deposit, money}, _from, state) when money

    > 0 do new_state = %{ balance: state.balance + money, transactions: [{:deposit, money}|state.transactions] } {:reply, {:ok, new_state.balance}, new_state} end def handle_call({:withdraw, money}, _from, state=%{balance: balance}) when money < balance do new_state = %{ balance: state.balance - money, transactions: [{:withdraw, money}|state.transactions] } {:reply, {:ok, new_state.balance}, new_state} end def handle_call(:balance, _from, state), do: {:reply, {:ok, state.balance}, state} end defmodule BankAccount do use GenServer @initial_state %{balance: 0, transactions: []} def start_link, do: GenServer.start_link(__MODULE__, @initial_state) def init(state), do: {:ok, state} def deposit(account, money), do: GenServer.call(account, {:deposit, money}) def withdraw(account, money), do: GenServer.call(account, {:withdraw, money}) def balance(account), do: GenServer.call(account, :balance)
  19. [Structural patterns] PROXY {:ok, account} = BankAccount.start_link # without interceptor

    BankAccount.deposit(account, 100) |> IO.inspect BankAccount.withdraw(account, 10) |> IO.inspect # spawn a proxy to intercept proxy = spawn PrivacyProxy, :intercept, [account] # calls to balance are now intercepted BankAccount.balance(proxy) |> IO.inspect BankAccount.deposit(proxy, 10) |> IO.inspect # hide balance: whenever the message :balance is sent, return {:ok, :hidden} defmodule PrivacyProxy do def intercept(account) do receive do {:"$gen_call", from, :balance} -> GenServer.reply(from, {:ok, :hidden}) {:"$gen_call", from, message} -> # forward everything else result = GenServer.call(account, message) GenServer.reply(from, result) end intercept(account) # continue intercepting end end
  20. [Architectural patterns] DATA MAPPER • transfer of data between a

    database and an in-memory data representation (domain layer) • Ecto schema + Repo defmodule User do use Ecto.Schema schema "users" do field :name, :string field :age, :integer, default: 0 has_many :posts, Post end end
  21. [Architectural patterns] PIPELINE • Plug.Conn and Ecto.Changeset • you can

    build your own (make main data structure first) defmodule Order do defstruct products: [], price: 0, taxes: 0, country: "" def add_product(order, name, price) do products = [name | order.products] new_price = order.price + price %{order | products: products, price: new_price} end def calculate_country_tax(%Order{country: "US"} = order) do taxes = order.taxes + order.price * 0.05 %{order | taxes: taxes} end def calculate_country_tax(order), do: order end order = %Order{country: "US"} order |> add_product("laptop", 1000) |> add_product("mouse", 50) |> calculate_country_tax()