1.需求 寫一個基於memcache的cache模塊, 需要在key前面加上特定的首碼, 所以user cache的原始的store函數應該寫成 由於加首碼的操作(key_encode/1)是所有存入cache前必須要做的事, 所以我們可以考慮通過metaprogramming來定義一個行為叫bef ...
1.需求
寫一個基於memcache的cache模塊, 需要在key前面加上特定的首碼, 所以user cache的原始的store函數應該寫成
# user.ex
def store(user_id, value) do key = Cache.key_encode(user_id, :user)
... end
由於加首碼的操作(key_encode/1)是所有存入cache前必須要做的事, 所以我們可以考慮通過metaprogramming來定義一個行為叫before_store/2來做這件事,然後在put前hook before_store,但這會讓代碼非常難以理解。
我覺得更好的方法是在編譯store/2期間去檢查它的開始有沒有執行過這個加首碼的encode函數, 這才能讓讓代碼更容易理解。
所以我們的潛規則是在模塊中的每一個函數的第一行,必須是Cache.key_encode/2
2. 使用@on_definition檢查模塊的每個函數第一行必須調用Cache.key_encode/2
我們接下來要使用@on_definition 在編譯器去檢查指定模塊是不是符合這個自定義的潛規則。
mix new on_definition_play
cd on_definition_play
# lib/user.ex defmodule User do @on_definition {Cache.Enforcement, :on_def} def store_user(user_id, user) do key = Cache.key_encode(user_id, :user) Cache.put(key, user) end # 這個是沒有做key_encode的例子,應該編譯不過 def store_comment(user_id, comment) do Cache.put(user_id, comment) end end
看上面的我們定義了on_definition屬性,接下來我們就來實現這個on_def/6
defmodule Cache do # 這裡只是用到了memcache_client做例子,你可以使用其它backend def put(key, value) do Memcache.Client.put(key, value) end def get(key) do Memcache.Client.get(key) end def key_encode(key, prefix) do "#{prefix}:#{inspect key}" end defmodule Enforcement do def on_def(env, _kind, _name, args, _guards, body) do check_start_with_key_encode(env, args, body) end defp check_start_with_key_encode(_env, [{_, meta, _} | _args], body) do line = Keyword.get(meta, :line)
# 從body裡面取出第一行,然後再check它的格式 expr = get_first_line(body) IO.inspect expr case expr do :print_to_see_this_struct-> # 我們現在也不知道這東西是個什麼東西,所以先用IO.inspect/1打出來看看,然後再對格式 :ok _ -> raise Cache.LacksEncodeError, message: "Function line#{line} must begin with a Cache.key_encode/2" end end
# 定義函數里使用的簡略模式 def func, do: defp get_first_line({:__block__, _, expr_list}) do List.first(expr_list) end defp get_first_line(expr) do expr end end defmodule LacksEncodeError do defexception [:message] end end
我們也不知道第一行編成AST後會是什麼樣子,所以我們先把正確的格式給IO.inspect看一看。然後再匹配上去 :)
所以根據inspect的結果我們可以最後把check_start_with_key_encode/3寫成:
defp check_start_with_key_encode(_env, [{_, meta, _} | _args], body) do line = Keyword.get(meta, :line) expr = get_first_line(body) case expr do {:=, _, [{_, _, _}, {{:., _, [{:__aliases__, _, [:Cache]},#就是它! :key_encode]}, _,#就是它! _}]} -> :ok _ -> raise Cache.LacksEncodeError, message: "Function line#{line} must begin with a Cache.key_encode/2" end end
這裡再運行mix compile就會得到
> mix compile == Compilation error on file lib/user.ex == ** (Cache.LacksEncodeError) Function line9 must begin with a Cache.key_encode/2 lib/cache.ex:31: Cache.Enforcement.check_start_with_key_encode/3 (stdlib) erl_eval.erl:669: :erl_eval.do_apply/6
大功告成!
3. 結論:
@on_definition時會調用on_def/6所以我們可以在編譯期間對每一個函數自定義你所需要的任何潛規則(但是也不要濫用哦:) )
4. Resources
Module docs 這裡面還有其它的compile callback函數和選項,值得好好看看