2-Agent
========
本章我們將創建一個名為```KV.Bucket```的模塊。這個模塊負責存儲可被不同進程讀寫的鍵值對。
如果你跳過了“入門”手冊,或者是太久以前讀的,那么建議你最好重新閱讀一下關于 **進程** 的那一章。
它是本節所內容的起點。
## 2.1-狀態的麻煩
Elixir是一種“(變量值)不可變”的語言。默認情況下,沒有什么是被共享的。
如果想要提供某種狀態,通過其創建可以從不同地方訪問的“桶”,我們有兩種選擇:
* 進程
* ETS([Erlang Term Storage](http://www.erlang.org/doc/man/ets.html))
我們之前介紹過進程,但ETS是個新東西,在后面的章節中再去探討。
而當用到進程時,我們很少會去自己動手從底層做起,而是用Elixir和OTP中抽象出來的東西代替:
* [Agent](http://elixir-lang.org/docs/stable/elixir/Agent.html) -
對狀態簡單的封裝
* [GenServer](http://elixir-lang.org/docs/stable/elixir/GenServer.html) -
“通用的服務器”(進程)。它封裝了狀態,提供了同步或異步調用,支持代碼熱更新等等
* [GenEvent](http://elixir-lang.org/docs/stable/elixir/GenEvent.html) -
“通用的事件”管理器,允許向多個接收者發布事件消息
* [Task](http://elixir-lang.org/docs/stable/elixir/Task.html) -
計算處理的異步單元,可以派生出進程并稍后收集計算結果
我們在本“進階”手冊中會逐一討論這些抽象物。
記住它們都是在進程基礎上實現的,使用Erlang虛擬機提供的基本特性,
如```send```,```receive```,```spawn```和```link```。
## 2.2-Agents
[Agent](http://elixir-lang.org/docs/stable/elixir/Agent.html)是對狀態簡單的封裝。
如果你想要一個可以保存狀態的地方(進程),那么Agent就是不二之選。
讓我們在工程里啟動一個```iex```對話:
```
$iex -S mix
```
然后“玩弄”一下Agent:
```elixir
iex> {:ok, agent} = Agent.start_link fn -> [] end
{:ok, #PID<0.57.0>}
iex> Agent.update(agent, fn list -> ["eggs"|list] end)
:ok
iex> Agent.get(agent, fn list -> list end)
["eggs"]
iex> Agent.stop(agent)
:ok
```
這里用某個初始狀態(空列表)啟動了一個agent,然后執行了一個命令來修改這個狀態,
加了一個新的列表項到頭部。```Agent.update/3```的第二個參數是一個匿名函數:
它使用agent當前狀態為輸入,返回想要的新狀態。
最終我們獲取整個列表。```Agent.get/3```函數的第二個參數是個匿名函數:
它使用當前狀態為輸入,返回的值就是```Agent.get/3```的返回值。
一旦我們用完agent,我們調用```Agent.stop/1```來終止agent進程。
現在我們用Agent來實現```KV.Bucket```。當時在開始之前,我們先寫些測試。
新建文件```test/kv/bucket_test.exs```(回想一下```.exs```文件),內容是:
```elixir
defmodule KV.BucketTest do
use ExUnit.Case, async: true
test "stores values by key" do
{:ok, bucket} = KV.Bucket.start_link
assert KV.Bucket.get(bucket, "milk") == nil
KV.Bucket.put(bucket, "milk", 3)
assert KV.Bucket.get(bucket, "milk") == 3
end
end
```
我們的第一條測試很直白:啟動一個```KV.Bucket```,然后執行```get/2```和```put/2```操作。
最后判斷結果。我們不需要顯式地停止agent進程。
因為該test里面用到的agent進程是鏈接到測試進程的,測試進程一結束它就會跟著結束。
同時還要注意我們向```ExUnit.Case```傳遞了一個```async:true```的選項。
這個選項使得該測試用例與其它同樣包含```:async```選項的測試用例并行執行。
這種方式能夠更好地利用計算機多核的能力。但要注意,這樣的話,測試用例不能依賴或改變某些全局的值。
比如測試需要向文件系統里寫入文字,或者注冊進程,或者訪問數據庫等。
你在放置```:async```標記前必須考慮會不會在兩個測試之間造成資源競爭。
不管是不是異步執行的,很明顯我們的測試會失敗,因為該實現的功能一個都沒實現。
為了修復失敗的用例,我們來創建文件```lib/kv/bucket.ex```,輸入以下內容。
你可以不看下方的代碼,自己隨便嘗試著創建agent的行為:
```elixir
defmodule KV.Bucket do
@doc """
Starts a new bucket.
"""
def start_link do
Agent.start_link(fn -> %{} end)
end
@doc """
Gets a value from the `bucket` by `key`.
"""
def get(bucket, key) do
Agent.get(bucket, &Map.get(&1, key))
end
@doc """
Puts the `value` for the given `key` in the `bucket`.
"""
def put(bucket, key, value) do
Agent.update(bucket, &Map.put(&1, key, value))
end
end
```
我們使用圖(Map)來存儲我們的鍵和值。函數捕捉符號```&```在《入門》中介紹過。
現在```KV.Bucket```模塊定義好了,測試都通過了!你可以執行```mix test```試試。
## 2.3-ExUnit回調函數
在繼續為```KV.Bucket```加入更多功能之前,先講一講ExUnit的回調函數。
你可能已經想到,每一個```KV.Bucket```的測試用例都需要用到bucket。
它要在該測試用例啟動時設置好,還要在該測試用例結束時停止。
幸運的是,ExUnit支持回調函數,使我們跳過這重復機械的任務。
讓我們使用回調機制重寫剛才的測試:
```elixir
defmodule KV.BucketTest do
use ExUnit.Case, async: true
setup do
{:ok, bucket} = KV.Bucket.start_link
{:ok, bucket: bucket}
end
test "stores values by key", %{bucket: bucket} do
assert KV.Bucket.get(bucket, "milk") == nil
KV.Bucket.put(bucket, "milk", 3)
assert KV.Bucket.get(bucket, "milk") == 3
end
end
```
我們首先利用```setup/1```宏,創建了設置bucket的回調函數。
這個函數會在每條測試用例執行前被執行一次,并且是與測試在同一個進程里。
注意我們需要一個機制來傳遞創建好的```bucket```的pid給測試用例。
我們使用 _測試上下文_ 來達到這個目的。
當在回調函數里返回```{:ok, bucket: bucket}```的時候,
ExUnit會把該返回值元祖(字典)的第二個元素merge進測試上下文中。
測試上下文是一個圖,我們可以在測試用例的定義中匹配它,從而獲取這個上下文的值給用例中的代碼使用:
```elixir
test "stores values by key", %{bucket: bucket} do
# `bucket` is now the bucket from the setup block
end
```
更多信息可以參考[ExUnit.Case](http://elixir-lang.org/docs/stable/ex_unit/ExUnit.Case.html)模塊文檔,
以及[回調函數](http://elixir-lang.org/docs/stable/ex_unit/ExUnit.Callbacks.html)。
## 2.4-其它Agent行為
除了“讀取”或者“修改”agent的狀態,agent還允許我們使用
函數```Agent.get_and_update/2```“讀取并修改”它維持的狀態。
我們用這個函數來實現刪除```KV.Bucket.delete/2```功能---從bucket中刪除一個值,并返回該值:
```elixir
@doc """
Deletes `key` from `bucket`.
Returns the current value of `key`, if `key` exists.
"""
def delete(bucket, key) do
Agent.get_and_update(bucket, &Map.pop(&1, key))
end
```
現在輪到你來給上面的代碼寫個測試啦。
你可以閱讀[Agent模塊的文檔](http://elixir-lang.org/docs/stable/elixir/Agent.html)
獲取更多信息。
## 2.5-Agent中的C/S模式
在進入下一章之前,讓我們討論一下agent中的C/S二元模式。
先來展開剛剛寫好的```delete/2```函數:
```elixir
def delete(bucket, key) do
Agent.get_and_update(bucket, fn dict->
Map.pop(dict, key)
end)
end
```
我們傳遞給agent的函數中的任何東西,都會出現在agent的進程里。
在這里,因為agent進程負責接收和回復我們的消息,因此可以說agent進程就是個服務器(服務端)。
而那個方法之外的任何東西,都被看成是在客戶端的范圍內。
這個區別很重要。如果有大量的工作要做,你必須考慮這個工作是放在客戶端還是在服務器上執行。比如:
```elixir
def delete(bucket, key) do
:timer.sleep(1000) # puts client to sleep
Agent.get_and_update(bucket, fn dict ->
:timer.sleep(1000) # puts server to sleep
Map.pop(dict, key)
end)
end
```
當服務器上執行一個很耗時的工作時,所有其它對該服務器的請求都必須等待,直到那個工作完成。
這會造成客戶端的超時。
下一章我們會探索通用服務器GenServer,它在概念上對服務器與客戶端的隔離更明顯。