1.Behaviour介紹 Erlang/Elixir的Behaviour類似於其它語言中的介面(interfaces),本質就是在指定behaviours的模塊中強制要求導出一些指定的函數,否則編譯時會warning。 其中Elixir中使用到behaviour的典範就是GenServer, Ge ...
1.Behaviour介紹
Erlang/Elixir的Behaviour類似於其它語言中的介面(interfaces),本質就是在指定behaviours的模塊中強制要求導出一些指定的函數,否則編譯時會warning。
其中Elixir中使用到behaviour的典範就是GenServer, GenEvent。
曾經Elixir有一個叫Behaviour的模塊,但是在1.1時就已被deprecated掉了,現在你並不需要用一個Behaviour模塊才能定義一個behaviour啦。
讓我們一步步實現一個自定義的behaviour吧。
2. Warehouse和Warehouse.Apple測試用例
我們來定義一個倉庫,它只要進貨和出貨,它的狀態就是庫存量和類型,然後再在上一層封裝一個具體的apple倉庫,它基於倉庫,但它對外只顯示庫存量。
mix new behaviour_play
cd behaviour_play
先寫測試,搞明白我們希望的效果
# test/behaviour_play_test.exs
defmodule BehaviourPlayTest do use ExUnit.Case doctest BehaviourPlay test "warehouse working" do :ok = Warehouse.new(%{category: :fruit, store: 0}) assert Warehouse.query(:fruit) == %{category: :fruit, store: 0} :ok = Warehouse.increase(:fruit, 10) assert Warehouse.query(:fruit) == %{category: :fruit, store: 10} :ok = Warehouse.decrease(:fruit, 2) assert Warehouse.query(:fruit) == %{category: :fruit, store: 8} assert {:not_enough, 8} = Warehouse.decrease(:fruit, 9) end # 隱藏了是什麼類型的水果這個參數 test "add a apple warehouse" do :ok = Warehouse.Apple.new assert Warehouse.Apple.query == 0 :ok = Warehouse.Apple.increase(5) assert Warehouse.Apple.query == 5 assert {:not_enough, 5} == Warehouse.Apple.decrease(6) :ok = Warehouse.Apple.decrease(4) assert Warehouse.Apple.query == 1 end end
我們現在運行測試肯定是失敗的,因為我們還沒寫warehouse.ex,但是先寫測試是一個好習慣~
3. 構建Warehouse和Warehouse.Apple
# lib/warehouse.ex defmodule Warehouse do use GenServer def new(%{category: category, store: store} = init_state) when is_integer(store) and store >= 0 do {:ok, _pid} = GenServer.start(__MODULE__, init_state, name: category) :ok end def query(pid) do GenServer.call(pid, :query) end def increase(pid, num) when is_integer(num) and num > 0 do GenServer.call(pid, {:increase, num}) end def decrease(pid, num)when is_integer(num) and num > 0 do GenServer.call(pid, {:decrease, num}) end # GenServer callback def handle_call(:query, _from, state) do {:reply, state, state} end def handle_call({:increase, num}, _from, state) do {:reply, :ok, %{state| store: state.store + num}} end def handle_call({:decrease, num}, _from, state = %{store: store})when store >= num do {:reply, :ok, %{state| store: store - num}} end def handle_call({:decrease, _num}, _from, state) do {:reply, {:not_enough, state.store}, state} end end
以上我們為把每一個warehouse都定義成新建立的一個GenServer進程。這時我們運行一下測試(mix test),會發現測試1已通過,但是我們具體指定到某一種類型apple的倉庫還沒有建出來。
# lib/apple.ex
defmodule Warehouse.Apple do def new do Warehouse.new(%{category: __MODULE__, store: 0}) end def query do state = Warehouse.query(__MODULE__) state.store end def increase(num) do Warehouse.increase(__MODULE__, num) end end
上面我們故意少定義了decrease/1這個函數,但是執行mix compile, 我們居然可以無任何warning的通過啦,我們只有到運行test時才能發現這個錯。
可是希望的結果是在編譯期間就能檢查出這種低級失誤來,而不是要到使用時才發現(如果我們沒有完備的test,就發現不了啦)
所以我們加入behaviour。
4.使用behaviour包裝Apple
# lib/warehouse.ex的最前面加上@callback屬性 defmodule Warehouse do @callback new() :: :ok @callback query() :: number @callback increase(num :: number) :: :ok @callback decrease(num :: number) :: :ok| {:not_enough, number} #接原來的內容...
然後在apple倉庫中引入這個behaviour
# lib/apple.ex defmodule Warehouse.Apple do @behaviour Warehouse # 接原來的內容
這時再compile就可以看到對應的warning啦。
> mix compile Compiled lib/warehouse.ex lib/apple.ex:1: warning: undefined behaviour function decrease/1 (for behaviour Warehouse)
我們再把按指示把decrease/1補全就
# lib/apple.ex def decrease(num) do Warehouse.decrease(__MODULE__, num) end
mix compile & mix test就全過啦。
6.幾個小細節
5.1 use GenServer後的callback並沒有doc,不會顯示在help裡面
我們use GenServer後,會自動生成GenServer對應的6個callback函數。但是當我們使用
iex(1)> h Warehouse. #按下tab自動補全 Apple decrease/2 increase/2 new/1 query/1
並沒有看到這些callback函數的doc...
6.2 use GenServer後為什麼可以不必定義全部的callback.
可以在這裡看看use GenServer時發生了什麼,它先使用@behaviour GenServer, 希望定義這6個函數的預設行為,最後再把他們defoverridable。
這就是為什麼我們在Warehouse裡面沒有定義init/1時它卻沒有warning的原因。這如果在Erlang中就是會warning的,因為他們沒有這麼flexible 的 Macro系統。
5.3 use實現的原理
可以參照看elixir的源代碼, 你需要在原模塊定義一個巨集__using__,所以我們的終極版本應該是
# lib/warehouse.ex 添加 defmacro __using__(options) do quote location: :keep do # 如果出錯,把錯誤的error trace打到本模塊來 @behaviour Warehouse def new, do: Warehouse.new(%{category: unquote(options)[:category], store: 0}) def query, do: Warehouse.query(unquote(options)[:category]).store def increase(num), do: Warehouse.increase(unquote(options)[:category], num) def decrease(num), do: Warehouse.decrease(unquote(options)[:category], num)
defoverridable [new: 0, query: 0, increase: 1, decrease: 1] end end
然後把apple.ex裡面只需要use Warehouse一句話就搞定啦。
所以我們可以如果想定義很多的水果apple, banana, pear,基本就是一句話的事~
#lib/apple.ex defmodule Warehouse.Apple do use Warehouse, category: __MODULE__ end #lib/banana.ex defmodule Warehouse.Banana do use Warehouse, category: __MODULE__ end # lib/pear.ex defmodule Warehouse.Pear do use Warehouse, category: __MODULE__ end
這樣的封裝就很簡化了很多代碼,是不是感覺寫elixir很爽呀~~~
show my Elixir apps around