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

Design patterns in Elixir

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

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()