Design Patterns in Elixir
Mix.install([
{:elixir_xml_to_map, "~> 3.0"},
{:jason, "~> 1.4.0"},
{:csv, "~> 3.0.4"},
{:ecto_sql, "~> 3.0"},
{:postgrex, ">= 0.0.0"},
{:elixir_trees, github: "crisefd/elixir_trees"}
])
Protocols for File parsing
defmodule FileHelpers do
def file_to_stream!(filename) do
filename |> Path.expand(__DIR__) |> File.stream!()
end
def file_to_binary!(filename) do
filename |> Path.expand(__DIR__) |> File.read!()
end
end
defprotocol Input do
def parse!(filename)
end
defprotocol Output do
def to_json(file)
end
defmodule Format.XML do
defstruct [:version, :content]
end
defmodule Format.CSV do
defstruct [:headers, :rows]
end
defimpl Input, for: Format.XML do
import FileHelpers
def parse!(filename) do
content =
filename
|> file_to_binary!()
|> XmlToMap.naive_map()
%Format.XML{version: "1.0", content: content}
end
end
defimpl Input, for: Format.CSV do
import FileHelpers
def parse!(filename) do
stream =
filename
|> file_to_stream!()
|> CSV.decode!()
headers = Enum.take(stream, 1)
rows = stream |> Stream.drop(1) |> Enum.to_list()
%Format.CSV{headers: headers, rows: rows}
end
end
csv =
Use macro for parsing
defmodule Input do
defstruct [:content]
defmacro __using__(_opts) do
quote do
@callback parse!(filename :: binary) :: map()
def file_to_stream!(filename) do
filename |> Path.expand(__DIR__) |> File.stream!()
end
def file_to_binary!(filename) do
filename |> Path.expand(__DIR__) |> File.read!()
end
end
end
end
defmodule Input.TXT do
use Input
end
defmodule Input.XML do
use Input
def parse!(filename) do
content =
filename
|> file_to_binary!()
|> XmlToMap.naive_map()
%Input{content: content}
end
end
defmodule Input.CSV do
use Input
def parse!(filename) do
content =
filename
|> file_to_stream!()
|> CSV.decode!(headers: true)
|> Enum.to_list()
%Input{content: content}
end
end
"input.xml" |> Path.expand(__DIR__) |> File.read!() |> XmlToMap.naive_map()
xml = Input.XML.parse!("./input.xml")
csv = Input.CSV.parse!("./input.csv")
txt = Input.TXT.parse!("./input.txt")
%{xml: xml, csv: csv, txt: txt}
defmodule Output.Simple.JSON do
def encode!(input) do
case input do
%Input.CSV{} = csv -> from_csv(csv)
%Input.XML{} = xml -> from_xml(xml)
end
end
defp from_csv(csv) do
# Transform CSV to JSON
end
defp from_xml(xml) do
# Tranform XML to JSON
end
end
defprotocol Output.JSON do
def encode!(input)
end
defimpl Output.JSON, for: Input.XML do
def encode!(input) do
input |> Map.from_struct() |> Jason.encode!()
end
end
defimpl Output.JSON, for: Input.CSV do
def encode!(input) do
input.data
|> Enum.map(fn row ->
input.headers |> Enum.zip(row) |> Map.new()
end)
|> Jason.encode!()
end
end
xml_out = Output.JSON.encode!(xml)
csv_out = Output.JSON.encode!(csv)
%{xml: xml_out, csv: csv_out}
Decorator Pattern
defmodule ListHouses do
import Ecto.Query
def list(filter) do
base_query()
|> with_type(filter)
|> with_available(filter)
|> Repo.all()
end
defp base_query(), do: from(h in House)
defp with_type(query, %{type: type})
when type in [:apartment, :house] do
where(query, [h], h.type == ^type)
end
defp with_type(query, _filter), do: query
defp with_available(query, %{available: available})
when is_boolean(available) do
where(query, [h], h.available == ^available)
end
defp with_available(query, _filter), do: query
end
Composite Pattern
defmodule Order do
defstruct [:products]
def total(order) do
order.products
|> Enum.map(&Price.total/1)
|> List.flatten()
|> Enum.sum()
end
end
defmodule Product do
defstruct [:name, :price, addons: []]
end
defmodule Shipping do
defstruct [:price]
end
defprotocol Price do
def total(item)
end
defimpl Price, for: Product do
def total(product) do
# Not tail-call optimised. Use only for small trees.
[product.price | Enum.map(product.addons, &Price.total/1)]
end
end
defimpl Price, for: Shipping do
def total(shipping), do: shipping.price
end
order = %Order{
products: [
%Product{
name: "Charging Cable",
price: 10
},
%Product{
name: "Smartphone",
price: 800,
addons: [
%Product{
name: "Name Engraving",
price: 20,
addons: [
%Product{
name: "Golden Letters",
price: 3
}
]
}
]
},
%Shipping{
price: 5
}
]
}
Order.total(order)