2018/05/28

CQRS: Command Query Responsibility Separation

CQS (Command Query Separation) 是由 Bertrand Meyer (Eiffel 語言的爸爸) 在 1988 於 "Object Oriented Software Construction" 這本書中提出的軟體架構概念,所有的 computing method 只會分成兩類,一種是執行某個 action 的 command,另一種是呼叫查詢並取得回傳資料,但不應該同時做兩種工作。換句話說,發問時,不能在該 method 裡面修改答案。

CQRS (Command Query Responsibility Separation) 應用 CQS 的概念,進一步將 Query 及 Command 物件分離,分別處理取得資料及修改資料的工作。

CQS 遇到的問題,會是 re-entrant 及 multi-thread,就是當一件工作處理到一半,被新的工作中斷這樣的問題,也就是 thread-safe 的問題,也就造成實作複雜度的問題。

然而這樣的問題,搭配著 Event Sourcing 的方法,將某個 APP state 的變更,收集成一個 sequence of events,以這種方式處理 command action,就能解決 thread-safe 的問題。

不過我們還是會遇到,command action 執行的速度跟 query 的時機點的問題,如果修改資料的動作在 query 以前還沒有完成,那麼前端就會查詢到舊狀態的資料,但資料還是會達到最終一致性,而不會有強一致性。

CQRS 的優點:

  1. Command 及 Query 分工明確,可分別進行效能調整及最佳化
  2. 將業務上的命令和查詢的職責分離能夠提高系統的性能、可擴展性和安全性
  3. 企業邏輯簡單清楚,能夠從事件歷程看到系統中的那些行為或者操作導致了系統的狀態變化。
  4. 將開發的邏輯概念,從數據驅動 (Data-Driven) 轉到任務驅動 (Task-Driven) 以及事件驅動 (Event-Driven)

在以下狀況,可以考慮使用CQRS模式:

  1. 當在業務邏輯層有很多操作需要相同的實體或者對象進行操作的時候。CQRS使得我們可以對讀和寫定義不同的實體和方法,從而可以減少或者避免對某一方面的更改造成衝突
  2. 用在特定任務的用戶互動系統,通常系統會引導用戶通過一系列複雜的步驟和操作,通常會需要一些複雜的領域模型。寫入資料的部分有很多和業務邏輯相關的命令操作,輸入驗證,業務邏輯驗證來保證數據的一致性。讀取資料沒有業務邏輯,僅僅是回傳 DTO。讀與寫的資料只需要達到最終一致性。
  3. 適用於一些需要對查詢性能和寫入性能分開進行優化的系統,尤其是讀/寫比非常高的系統。例如在很多系統中讀取資料的使用次數遠大於寫入資料。
  4. 對於系統在將來會隨著時間不斷演化,有可能會包含不同版本的模型,或者業務規則經常變化的系統
  5. 需要和其他系統整合,特別是需要和事件歷程 Event Sourcing 進行整合的系統,這樣子系統的臨時異常不會影響整個系統的其他部分。

在以下狀況,不適合使用CQRS:

  1. 領域模型或者業務邏輯比較簡單,這種情況下使用CQRS會把系統弄得太複雜
  2. 對於簡單的,CRUD模式的用戶介面以及與之相關的數據訪問操作已經足夠的話,都只是一個簡單的對數據進行增刪改查,沒有必要使用CQRS
  3. 不適合在整個系統中全部都使用 CQRS,在特定模組中CQRS可能比較有用

以下是一些查詢到的 CQRS 架構圖,從圖片可以看到跟傳統的 CRUD Data-Driven 架構的差異。

這種架構區分了 Business 及 Query model 的 DataBase,也可以想成將資料寫入了 Business Database,而前端使用者不會觸及該資料庫,是比較強調資料安全性的方式,但不能保證 Query Model 的 DB 裡面的資料一定會跟 Business Model DB 的資料一樣。

這種架構比較接近一個一般性的系統,資料庫是單一的,且可以在 Event Handler 中確認並檢查 Database 及 Analysis Database 的資料一致性。

這種架構圖比較強調資料流的過程,但基本上架構跟上面那個很接近,不過在 Comamnd 的部分,可注意到 Command 沒有 Reply DTO,也可以說,只要 Comamnd 有送進 Command Handler,就視為一個成功執行的 Command。

這裡強調 Command Bus 是以非同步的方式,送進 Command Handler,非同步可強化系統效能的表現,但如果要用同步的方式也可以,要等到 Command Event 送進 Event Database 後,才回應給前端確認該命令已經完成。而 Query 是用同步的方式,發送 Query 並等待要回傳的 ViewModel。

References

CQRS - Martin Fowler

CQRS 介紹

CQRS 命令查詢職責分離模式介紹

從CQS到CQRS

From CQS to CQRS

深度長文:我對CQRS/EventSourcing架構的思考

DDD CQRS架構和傳統架構的優缺點比較

Introduction to Domain Driven Design, CQRS and Event Sourcing

2018/05/21

Elixir 9 Protocol

inpsect 可以用 printable binary 形式回傳任何 value。但 elixir 是用什麼方式實作的? 是不是 guard clause

def inspect(value) when is_atom(value), do: ...
def inspect(value) when is_binary(value), do: ...

protocol 允許不同的資料類型用於相同的函數,不同資料型別的相同函數形態會有相同的行為。很像是 behavior,但 behavior 用在 module 裡面,protocol 可在 module 外面實作,這表示我們可以自由擴充 module 不足的功能。

defprotocol 定義 protocol,defprotocol 只定義 function,defimpl 實作要放在不同的地方。

defprotocol Inspect do
    def inspect(thing, opts)
end

增加了這兩個實作,就可以 inspect PID

defimpl Inspect, for: PID do
    def inspect(pid, _opts) do
        "#PID" <> IO.iodata_to_binary(:erlang.pid_to_list(pid))
    end
end

defimpl Inspect, for: Reference do
    def inspect(ref, _opts) do
        '#Ref' ++ rest = :erlang.ref_to_list(ref)
        "#Reference" <> IO.iodata_to_binary(rest)
    end
end
iex(1)> inspect self()
"#Process<0.89.0>"

可在 for: 後面使用的 Types 有

Any
Atom
BitString
Float
Function
Integer
List
Map
PID
Port
Record
Reference
Tuple

is_collection.exs

defprotocol Collection do
  @fallback_to_any true
  def is_collection?(value)
end

defimpl Collection, for: [List, Tuple, BitString, Map] do
  def is_collection?(_), do: true
end

defimpl Collection, for: Any do
  def is_collection?(_), do: false
end

Enum.each [ 1, 1.0, [1,2], {1,2}, %{}, "cat" ], fn value ->
  IO.puts "#{inspect value}:  #{Collection.is_collection?(value)}"
end
$ elixir is_collection.exs
1:  false
1.0:  false
[1, 2]:  true
{1, 2}:  true
%{}:  true
"cat":  true

Protocol and Structs

Elixir 沒有 classes,但支援 user-defined types

defmodule Blob do
  defstruct content: nil
end
iex(1)> c "basic.exs"
[Blob]
iex(2)> b = %Blob{content: 123}
%Blob{content: 123}
iex(3)> inspect b
"%Blob{content: 123}"

## structs 其實是 map,key為 __struct__
iex(4)> inspect b, structs: false
"%{__struct__: Blob, content: 123}"

Built-In Protocols

elixir 有以下內建的 protocols

  • Enumerable and Collectable
  • Inspect
  • List.Chars
  • String.Chars

首先定義一個以 0s 1s 表示的 integer

bitmap.exs

defmodule Bitmap do
  defstruct value: 0

  @doc """
  A simple accessor for the 2^bit value in an integer

      iex> b = %Bitmap{value: 5}
      %Bitmap{value: 5}
      iex> Bitmap.fetch_bit(b,2)
      1
      iex> Bitmap.fetch_bit(b,1)
      0
      iex> Bitmap.fetch_bit(b,0)
      1
  """
  def fetch_bit(%Bitmap{value: value}, bit) do
    use Bitwise

    (value >>> bit) &&& 1
  end
end
  • Enumerable and Collectable

Enumerable 定義了三個 functions

defprotocol Enumerable do
    # collection 的元素數量
    def count(collection)

    # 是否包含某個 value
    def member?(collection, value)

    # reduce fun to collection elements
    def reduce(collection, acc, fun)
end

針對剛剛的 Bitmap 實作 Enumerable

defimpl Enumerable,  for: Bitmap do
  import :math, only: [log: 1]

  def reduce(bitmap, {:cont, acc}, fun) do
    bit_count =  Enum.count(bitmap)
    _reduce({bitmap, bit_count}, { :cont, acc }, fun)
  end

  defp _reduce({_bitmap, -1}, { :cont, acc }, _fun), do: { :done, acc }

  defp _reduce({bitmap, bit_number}, { :cont, acc }, fun) do
    with bit = Bitmap.fetch_bit(bitmap, bit_number),
    do:  _reduce({bitmap, bit_number-1}, fun.(bit, acc), fun)
  end

  defp _reduce({_bitmap, _bit_number}, { :halt, acc }, _fun), do: { :halted, acc }

  defp _reduce({bitmap, bit_number}, { :suspend, acc }, fun), 
  do: { :suspended, acc, &_reduce({bitmap, bit_number}, &1, fun), fun } 

  def member?(value, bit_number) do
    { :ok, 0 <= bit_number && bit_number < Enum.count(value) }
  end

  def count(%Bitmap{value: value}) do              
    { :ok, trunc(log(abs(value))/log(2)) + 1 }
  end
end


fifty = %Bitmap{value: 50}

IO.puts Enum.count fifty    # => 6

IO.puts Enum.member? fifty, 4    # => true
IO.puts Enum.member? fifty, 6    # => false

IO.inspect Enum.reverse fifty       # => [0, 1, 0, 0, 1, 1, 0]
IO.inspect Enum.join fifty, ":"     # => "0:1:1:0:0:1:0"
iex(1)> c ("bitmap.exs")
[Bitmap]
iex(2)> c ("bitmap_enumerable.exs")
6
true
false
[0, 1, 0, 0, 1, 1, 0]
"0:1:1:0:0:1:0"
[Enumerable.Bitmap]

defimpl Collectable,  for: Bitmap do
  use Bitwise

  # 回傳 tuple,(1) value (2) function
  def into(%Bitmap{value: target}) do
    {target, fn
      acc, {:cont, next_bit} -> (acc <<< 1) ||| next_bit
      acc,  :done            -> %Bitmap{value: acc}
      _, :halt               -> :ok
    end}
  end
end
iex(3)> c("bitmap_collectable.exs")
[Collectable.Bitmap]
iex(4)> Enum.into [1,1,0,0,1,0], %Bitmap{value: 0}
%Bitmap{value: 50}
  • Inspect
defmodule Bitmap do
  defstruct value: 0

  defimpl Inspect do
    def inspect(%Bitmap{value: value}, _opts) do
      "%Bitmap{#{value}=#{as_binary(value)}}"
    end
    defp as_binary(value) do
      to_string(:io_lib.format("~.2B", [value]))
    end
  end
end
iex(6)> c("bitmap_inspect.exs")
[Bitmap, Inspect.Bitmap]
iex(7)> fifty = %Bitmap{value: 50}
%Bitmap{50=110010}
iex(8)> inspect fifty
"%Bitmap{50=110010}"
iex(9)> inspect fifty, structs: false
"%{__struct__: Bitmap, value: 50}"
iex(10)> %Bitmap{value: 12345678901234567890}
%Bitmap{12345678901234567890=1010101101010100101010011000110011101011000111110000101011010010}

defmodule Bitmap do
  defstruct value: 0

  defimpl Inspect, for: Bitmap do
    import Inspect.Algebra
    def inspect(%Bitmap{value: value}, _opts) do
      concat([
        nest(
         concat([
           "%Bitmap{",
           break(""),
           nest(concat([to_string(value),
                        "=",
                        break(""),
                        as_binary(value)]),
                2),
         ]), 2),
        break(""),
        "}"])
    end
    defp as_binary(value) do
      to_string(:io_lib.format("~.2B", [value]))
    end
  end
end
iex(1)> c("bitmap_algebra.exs")
[Bitmap, Inspect.Bitmap]
iex(2)>
nil
iex(3)> big_bitmap = %Bitmap{value: 12345678901234567890}
%Bitmap{12345678901234567890=
    1010101101010100101010011000110011101011000111110000101011010010}
iex(4)>
nil
iex(5)> IO.inspect big_bitmap
%Bitmap{12345678901234567890=
    1010101101010100101010011000110011101011000111110000101011010010}
%Bitmap{12345678901234567890=
    1010101101010100101010011000110011101011000111110000101011010010}
iex(6)> IO.inspect big_bitmap, structs: false
%{__struct__: Bitmap, value: 12345678901234567890}
%Bitmap{12345678901234567890=
    1010101101010100101010011000110011101011000111110000101011010010}
  • List.Chars & String.Chars

bitmap_string.exs

defimpl String.Chars, for: Bitmap do
  def to_string(bitmap) do
    import Enum
    bitmap
    |> reverse
    |> chunk(3)
    |> map(fn three_bits -> three_bits |> reverse |> join end)
    |> reverse
    |> join("_")
  end
end
iex(1)> c("bitmap.exs")
[Bitmap]
iex(2)> c("bitmap_enumerable.exs")
[Enumerable.Bitmap]
iex(3)> c("bitmap_string.exs")
[String.Chars.Bitmap]


iex(4)> fifty = %Bitmap{value: 50}
%Bitmap{value: 50}
iex(5)> "Fifty in bits is: #{fifty}"
"Fifty in bits is: 110_010"

References

Programming Elixir

2018/05/14

Elixir 8 Macros

Macro 可能會讓程式碼更難閱讀,如果可以使用 function,就不要用 macro。本文以 erlang/elixir 不支援的 if 語法,說明如何以 macro 實作 if 語法。

if Statement

myif «condition» do
    «evaluate if true»
else
    «evaluate if false»
end

----

myif «condition»,
    do: «evaluate if true»,
    else: «evaluate if false

可用 function 實作 if 的語法

defmodule My do
    def myif(condition, clauses) do
        do_clause = Keyword.get(clauses, :do, nil)
        else_clause = Keyword.get(clauses, :else, nil)

    case condition do
        val when val in [false, nil]
            -> else_clause
        _otherwise
            -> do_clause
        end
    end
end
$ iex my.exs
iex(1)> My.myif 1==2, do: (IO.puts "1==2"), else: (IO.puts "1 != 2")
1==2
1 != 2
:ok

但結果是不正確的,因為 elixir 同時 evaluate do: 及 else:

Macro Inject Code

defmacro 定義 macro,當傳送參數給 macro,elixir 不會直接 evaluate,而是會以 tuple 方式發送程式碼。

defmodule My do
  defmacro macro(param) do
    IO.inspect param
  end
end

defmodule Test do
  require My

  # These values represent themselves
  My.macro :atom        #=> :atom
  My.macro 1            #=> 1
  My.macro 1.0          #=> 1.0
  My.macro [1,2,3]      #=> [1,2,3]
  My.macro "binaries"   #=> "binaries"
  My.macro { 1, 2 }     #=> {1,2}
  My.macro do: 1        #=> [do: 1]

  # And these are represented by 3-element tuples,以三個元素的 tuple 表示這些 macro

  My.macro { 1,2,3,4,5 }
  # =>  {:"{}",[line: 20],[1,2,3,4,5]}

  My.macro do: ( a = 1; a+a )
  # =>  [do:
  #      {:__block__,[],
  #        [{:=,[line: 22],[{:a,[line: 22],nil},1]},
  #         {:+,[line: 22],[{:a,[line: 22],nil},{:a,[line: 22],nil}]}]}]


  My.macro do
    1+2
  else
    3+4
  end
  # =>   [do: {:+,[line: 24],[1,2]},
  #       else: {:+,[line: 26],[3,4]}]

end
$ iex dumper.exs
:atom
1
1.0
[1, 2, 3]
"binaries"
{1, 2}
[do: 1]
{:{}, [line: 29], [1, 2, 3, 4, 5]}
[do: {:__block__, [],
  [{:=, [line: 32], [{:a, [line: 32], nil}, 1]},
   {:+, [line: 32], [{:a, [line: 32], nil}, {:a, [line: 32], nil}]}]}]
[do: {:+, [line: 40], [1, 2]}, else: {:+, [line: 42], [3, 4]}]

在一個 module 定義 macro,另一個 module 使用時,必須要先用 require,這樣才能確保 macro module 在目前這個 module 前先被編譯。


quote function

quote 可讓 code 保持尚未 evaluated 的形式。quote/2 能把 Elixir 代碼轉換成底層表示形式。

iex(1)> quote do: :atom
:atom
iex(2)> quote do: 1
1
iex(3)> quote do: 1.0
1.0
iex(4)> quote do: [1,2,3]
[1, 2, 3]
iex(5)> quote do: "binaries"
"binaries"
iex(6)> quote do: {1,2}
{1, 2}
iex(7)> quote do: [do: 1]
[do: 1]

iex(8)> quote do: {1,2,3,4,5}
{:{}, [], [1, 2, 3, 4, 5]}

iex(9)> quote do: (a = 1; a + a)
{:__block__, [],
 [{:=, [], [{:a, [], Elixir}, 1]},
  {:+, [context: Elixir, import: Kernel],
   [{:a, [], Elixir}, {:a, [], Elixir}]}]}

iex(10)> quote do: [ do: 1 + 2, else: 3 + 4]
[do: {:+, [context: Elixir, import: Kernel], [1, 2]},
 else: {:+, [context: Elixir, import: Kernel], [3, 4]}]

eg.exs

defmodule My do
  defmacro macro(code) do
    IO.inspect code
    # 會 evaluate code
    code
  end
end
defmodule Test do
  require My
  My.macro(IO.puts("hello"))
end

eg1.exs

defmodule My do
  defmacro macro(code) do
    IO.inspect code
    # 只會 evaluate do 裡面的 code,quota 裡面的 code 會回傳給呼叫 macro 的 code,然後被 evaluate
    quote do: IO.puts "Different code"
  end
end
defmodule Test do
  require My
  My.macro(IO.puts("hello"))
end
$ elixir eg.exs
{{:., [line: 17], [{:__aliases__, [counter: 0, line: 17], [:IO]}, :puts]},
 [line: 17], ["hello"]}
hello

$ elixir eg1.exs
{{:., [line: 17], [{:__aliases__, [counter: 0, line: 17], [:IO]}, :puts]},
 [line: 17], ["hello"]}
Different code

unquote function

知道了如何獲取代碼的內部表示,那怎麼修改它呢?可利用 unquote/1 來插入新的代碼和值。當我們 unquote 一個表達式的時候,會把它運行的結果插入到 AST。

iex(1)> denominator = 2
2
iex(2)> quote do: divide(42, denominator)
{:divide, [], [42, {:denominator, [], Elixir}]}
iex(3)> quote do: divide(42, unquote(denominator))
{:divide, [], [42, 2]}

Expanding a list using unquote_splicing

# insert [3,4]
iex(4)> Code.eval_quoted(quote do: [1,2,unquote([3,4])])
{[1, 2, [3, 4]], []}

# insert 3,4 到前面的 list
iex(5)> Code.eval_quoted(quote do: [1,2,unquote_splicing([3,4])])
{[1, 2, 3, 4], []}

# '1234' 是 lists of characters
iex(6)> Code.eval_quoted(quote do: [?a, ?= ,unquote_splicing('1234')])
{'a=1234', []}


iex(7)> fragment = quote do: IO.puts("hello")
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["hello"]}
iex(8)> Code.eval_quoted fragment
hello
{:ok, []}
iex(9)> Code.eval_string("[a, a*b, c]", [a: 2, b: 3, c: 4])
{[2, 6, 4], [a: 2, b: 3, c: 4]}

myif Macro

defmodule My do
  defmacro if(condition, clauses) do
    do_clause   = Keyword.get(clauses, :do, nil)
    else_clause = Keyword.get(clauses, :else, nil)
    quote do
      case unquote(condition) do
        val when val in [false, nil] -> unquote(else_clause)
        _                            -> unquote(do_clause)
      end
    end
  end
end

defmodule Test do
  require My
  My.if 1==2 do
    IO.puts "1 == 2"
  else
    IO.puts "1 != 2"
  end
end
$ elixir myif.ex
1 != 2

References

Programming Elixir