檢視原始碼 元程式反模式
此文件概述與元程式相關的潛在反模式。
大量程式碼產生
問題
此反模式與產生過多程式碼的巨集有關。當巨集產生大量程式碼時,會影響編譯器和/或執行時期的工作方式。原因在於 Elixir 可能必須多次展開、編譯和執行程式碼,這將使編譯變慢,並使產生的編譯人工製品更大。
範例
想像一下您正在為 Web 應用程式定義路由器,其中您可能有 get/2
等巨集。在每次呼叫巨集(可能數百次)時,get/2
內的程式碼將會展開並編譯,這可能會產生大量的程式碼。
defmodule Routes do
defmacro get(route, handler) do
quote do
route = unquote(route)
handler = unquote(handler)
if not is_binary(route) do
raise ArgumentError, "route must be a binary"
end
if not is_atom(handler) do
raise ArgumentError, "handler must be a module"
end
@store_route_for_compilation {route, handler}
end
end
end
重構
為了移除此反模式,開發人員應簡化巨集,將其部分工作委派給其他函式。如下所示,透過將 quote/1
內的程式碼封裝在函式 __define__/3
中,我們減少了每次呼叫巨集時展開和編譯的程式碼,並改為呼叫函式來執行大部分工作。
defmodule Routes do
defmacro get(route, handler) do
quote do
Routes.__define__(__MODULE__, unquote(route), unquote(handler))
end
end
def __define__(module, route, handler) do
if not is_binary(route) do
raise ArgumentError, "route must be a binary"
end
if not is_atom(handler) do
raise ArgumentError, "handler must be a module"
end
Module.put_attribute(module, :store_route_for_compilation, {route, handler})
end
end
不必要的巨集
問題
巨集 是強大的元程式機制,可用於 Elixir 中來擴充語言。雖然使用巨集本身並非反模式,但此元程式機制僅應在絕對必要時使用。每當使用巨集時,但可以使用函式或其他現有的 Elixir 結構來解決相同的問題,程式碼就會變得不必要地更複雜且更難以閱讀。由於巨集更難以實作和推理,因此不加選擇地使用它們可能會損害系統的演進,降低其可維護性。
範例
MyMath
模組實作 sum/2
巨集,以執行接收兩個數字作為參數的總和。雖然此程式碼沒有語法錯誤,並且可以正確執行以取得預期的結果,但它不必要地更複雜。透過將此功能實作為巨集而不是傳統函式,程式碼變得不那麼清楚
defmodule MyMath do
defmacro sum(v1, v2) do
quote do
unquote(v1) + unquote(v2)
end
end
end
iex> require MyMath
MyMath
iex> MyMath.sum(3, 5)
8
iex> MyMath.sum(3 + 1, 5 + 6)
15
重構
為了移除此反模式,開發人員必須使用更簡單撰寫和理解的結構(例如命名函式)來取代不必要的巨集。以下所示的程式碼是先前範例重構的結果。基本上,sum/2
巨集已轉換為傳統的命名函式。請注意,require/2
呼叫不再需要
defmodule MyMath do
def sum(v1, v2) do # <= The macro became a named function
v1 + v2
end
end
iex> MyMath.sum(3, 5)
8
iex> MyMath.sum(3+1, 5+6)
15
use
取代 import
問題
Elixir 有 import/1
、alias/1
和 use/1
等機制,用於建立模組間的相依性。使用這些機制實作的程式碼本身並非程式碼異味。然而,import/1
和 alias/1
指令具有詞彙範圍,且僅協助模組呼叫其他模組的函式,use/1
指令具有較廣的範圍,這可能會造成問題。
use/1
指令允許模組將任何類型的程式碼注入另一個模組,包括傳播相依性。如此一來,使用 use/1
指令會讓程式碼更難閱讀,因為要了解在參照模組時會發生什麼事,必須具備參照模組的內部詳細資料。
範例
以下顯示的程式碼是此反模式的範例。它定義了三個模組:ModuleA
、Library
和 ClientApp
。ClientApp
透過 use/1
指令重複使用 Library
的程式碼,但不知道其內部詳細資料。這讓 ClientApp
的作者更難視覺化其模組中現在有哪些模組和功能可用。更糟的是,Library
也匯入了 ModuleA
,而 ModuleA
定義了一個 foo/0
函式,與 ClientApp
中定義的區域函式衝突
defmodule ModuleA do
def foo do
"From Module A"
end
end
defmodule Library do
defmacro __using__(_opts) do
quote do
import Library
import ModuleA # <= propagating dependencies!
end
end
def from_lib do
"From Library"
end
end
defmodule ClientApp do
use Library
def foo do
"Local function from client app"
end
def from_client_app do
from_lib() <> " - " <> foo()
end
end
當我們嘗試編譯 ClientApp
時,Elixir 會偵測到衝突並擲出下列錯誤
error: imported ModuleA.foo/0 conflicts with local function
└ client_app.ex:4:
重構
若要移除此反模式,我們建議函式庫作者避免提供 __using__/1
回呼,只要能用 alias/1
或 import/1
指令取代即可。在以下程式碼中,我們假設 use Library
已不再可用,且 ClientApp
已用這種方式重新整理,這樣一來,程式碼會更清楚,且先前顯示的衝突已不存在
defmodule ClientApp do
import Library
def foo do
"Local function from client app"
end
def from_client_app do
from_lib() <> " - " <> foo()
end
end
iex> ClientApp.from_client_app()
"From Library - Local function from client app"
其他說明
在需要執行模組匯入和別名設定以外的操作時,提供 use MyModule
可能有其必要性,因為它提供了 Elixir 生態系統中一個常見的擴充點。
因此,為了提供指引和明確性,我們建議函式庫作者在他們的 @moduledoc
中包含一個告誡區塊,說明 use MyModule
如何影響開發人員的程式碼。舉例來說,GenServer
文件大綱中說明了
use GenServer
當您
use GenServer
時,GenServer
模組會設定@behaviour GenServer
並定義一個child_spec/1
函式,因此您的模組可以用作監控樹中的子模組。
可以將此摘要視為程式碼生成的「營養標示」。務必只列出對模組公開 API 所做的變更。例如,如果 use Library
設定了一個名為 @_some_module_info
的內部屬性,而這個屬性從未打算公開,請避免在營養標示中記錄它。
為方便起見,產生上述告誡區塊的標記符號如下
> #### `use GenServer` {: .info}
>
> When you `use GenServer`, the `GenServer` module will
> set `@behaviour GenServer` and define a `child_spec/1`
> function, so your module can be used as a child
> in a supervision tree.