鍍金池/ 教程/ 區(qū)塊鏈/ 第 三 章 軟件工程的原則
第 三 章 軟件工程的原則
第 4 章 錯(cuò)誤處理機(jī)制
9 最常見(jiàn)的錯(cuò)誤
6 幾種 Erlang 的特殊慣例
第 5 章 進(jìn)程、服務(wù)及消息
8 文檔
7 特殊的語(yǔ)法規(guī)范
使用 Erlang 編程開(kāi)發(fā)——編程規(guī)則及規(guī)范
第 二 章 結(jié)構(gòu)和Erlang術(shù)語(yǔ)
10 必備文檔

第 三 章 軟件工程的原則

  • 3.1 從模塊中盡量少導(dǎo)出函數(shù)
  • 3.2 努力降低模塊間依賴(lài)性
  • 3.3 將常用的代碼放入庫(kù)中
  • 3.4 將“棘手的”或“臟亂的”代碼分別放入不同的模塊中
  • 3.5 不要假設(shè)調(diào)用者會(huì)如何處理函數(shù)結(jié)果
  • 3.6 將代碼或行為的通用模式抽象出來(lái)
  • 3.7 采用“由上至下”的編程方式
  • 3.8 不要優(yōu)化代碼
  • 3.9 遵循“驚訝最少”原則
  • 3.10 終止副作用
  • 3.11 不要“泄露”模塊內(nèi)的私有數(shù)據(jù)結(jié)構(gòu)
  • 3.12 盡量明確代碼的行為
  • 3.13 不要在編程中采取“防范”措施
  • 3.14 利用設(shè)備驅(qū)動(dòng)來(lái)隔離硬件接口
  • 3.15 利用同一個(gè)函數(shù)來(lái)實(shí)現(xiàn)相反的兩種行為

3.1 從模塊中盡量少導(dǎo)出函數(shù)

模塊是 Erlang 中的基本代碼結(jié)構(gòu)體。模塊可以包含大量的函數(shù),但只有模塊導(dǎo)出列表中的函數(shù)才能從模塊外部調(diào)用。

從模塊外部來(lái)看,模塊的復(fù)雜性跟模塊可導(dǎo)出的函數(shù)數(shù)量有關(guān)。只導(dǎo)出一兩個(gè)函數(shù)的模塊通常要比那些能導(dǎo)出幾十個(gè)函數(shù)的模塊更易于人們理解。

對(duì)于使用者來(lái)說(shuō),可導(dǎo)出/非導(dǎo)出函數(shù)的比率較低的模塊是比較易于接受的,因?yàn)樗麄冎恍枥斫饽K可導(dǎo)出函數(shù)的功能即可。

另外,模塊代碼的作者或者維護(hù)人員還可以采取任何適當(dāng)?shù)姆绞剑诒3滞獠拷涌诓蛔兊那疤嵯赂淖兡K的內(nèi)部結(jié)構(gòu)。

3.2 努力降低模塊間依賴(lài)性

如果模塊需要調(diào)用很多不同模塊中的函數(shù),那么它就難以維護(hù),相比之下,僅調(diào)用有限幾個(gè)模塊函數(shù)的模塊能更輕松地得到維護(hù)。

這是因?yàn)?,每次我們改變模塊接口時(shí),都要檢查代碼中所有調(diào)用該模塊的位置。降低模塊間的依賴(lài)性,可以使這些模塊的維護(hù)變得簡(jiǎn)單。

減少給定模塊所調(diào)用的不同模塊數(shù)目,也可以簡(jiǎn)化系統(tǒng)結(jié)構(gòu)。

同時(shí)也應(yīng)注意,模塊間調(diào)用依賴(lài)性結(jié)構(gòu)最好呈現(xiàn)樹(shù)狀結(jié)構(gòu),而不要出現(xiàn)循環(huán)結(jié)構(gòu)。例如下圖所示的樹(shù)狀結(jié)構(gòu):

http://wiki.jikexueyuan.com/project/erlang-programming-rules/images/3.2_module_dep_ok.png" alt="module-dep-ok" />

最好不要是這樣的結(jié)構(gòu):

http://wiki.jikexueyuan.com/project/erlang-programming-rules/images/3.2_module_dep_bad.png" alt="3.2_module_dep_bad" />

3.3 將常用的代碼放入庫(kù)中

應(yīng)將常用代碼放入庫(kù)中。庫(kù)應(yīng)該是相關(guān)函數(shù)的集合。應(yīng)該努力確保庫(kù)包含同樣類(lèi)型的函數(shù)。比如,若 lists 庫(kù)只包含操縱列表的函數(shù),那么這就是一種非常不錯(cuò)的設(shè)計(jì);而如果 lists_and_maths 庫(kù)中既含有操縱列表的函數(shù),又含有用于數(shù)學(xué)運(yùn)算的函數(shù),那么就是一種非常糟糕的設(shè)計(jì)。

庫(kù)函數(shù)應(yīng)最好沒(méi)有副作用。庫(kù)中若包含帶有副作用的函數(shù),則會(huì)限制它的可重用性。

3.4 將“棘手的”或“臟亂的”代碼分別放入不同的模塊中

在解決某個(gè)問(wèn)題時(shí),往往需要結(jié)合使用整潔與臟亂的代碼。最好將整潔的代碼與臟亂代碼分別放入單獨(dú)的模塊中。

臟亂代碼是指那些做“臟活”的代碼。比如說(shuō):

  • 使用進(jìn)程字典。
  • erlang:process_info/1 用于特殊目的。
  • 做一些沒(méi)想去做但又必須去做的事。

應(yīng)該努力增加整潔代碼,減少混亂代碼。隔離混亂代碼與清晰注釋?zhuān)驅(qū)⒋a中存在的所有副作用和問(wèn)題記錄下來(lái)。

3.5 不要假設(shè)調(diào)用者會(huì)如何處理函數(shù)結(jié)果

不要事先假設(shè)函數(shù)為何被調(diào)用,或者調(diào)用者希望如何處理結(jié)果。

例如,假設(shè)我們調(diào)用一個(gè)例程,它的某些參數(shù)可能是無(wú)效的。在實(shí)現(xiàn)該例程時(shí),不需要知道當(dāng)參數(shù)無(wú)效時(shí),函數(shù)調(diào)用者會(huì)希望采用的行為。

因此我們不應(yīng)該這樣寫(xiě)函數(shù):

do_something(Args) -> 
  case check_args(Args) of 
    ok -> 
      {ok, do_it(Args)}; 
    {error, What} -> 
      String = format_the_error(What), 
      io:format("* error:~s\n", [String]), %% Don't do this
      error 
  end.  

而應(yīng)該這樣寫(xiě)函數(shù):

do_something(Args) ->
  case check_args(Args) of
    ok ->
      {ok, do_it(Args)};
    {error, What} ->
      {error, What}
  end.

error_report({error, What}) ->
  format_the_error(What).

在第一段代碼中,錯(cuò)誤字符串經(jīng)常打印在標(biāo)準(zhǔn)輸出中;而第二段代碼則為程序返回一個(gè)錯(cuò)誤描述符,程序可以決定如何處理錯(cuò)誤描述符。

通過(guò)調(diào)用 error_report/1,函數(shù)可以將錯(cuò)誤描述轉(zhuǎn)化為一個(gè)可輸出的字符串并在需要時(shí)將其打印出來(lái)。但這可能并非是預(yù)期行為——無(wú)論如何,對(duì)結(jié)果的處理決策應(yīng)由調(diào)用方來(lái)決定。

3.6 將代碼或行為的通用模式抽象出來(lái)

如果在代碼的兩個(gè)或多個(gè)位置處出現(xiàn)了同樣模式的代碼,則最好將這種代碼單獨(dú)編寫(xiě)為一個(gè)常用的函數(shù),然后通過(guò)調(diào)用該函數(shù)來(lái)解決問(wèn)題,而不要讓同樣模式的代碼散布在多個(gè)位置。維護(hù)復(fù)制的代碼會(huì)需要付出更大的精力。

如果代碼的兩個(gè)或多個(gè)位置處具有相似模式的代碼(比如,功能基本相同),那么就值得稍微研究一下,想一想是否不用怎么改變問(wèn)題本身,就能使代碼適用于不同的情況,然后還可以編寫(xiě)少量的額外代碼來(lái)描述并應(yīng)對(duì)不同情況之間的差別。

總之,盡量避免使用“復(fù)制”或“粘貼”來(lái)編程,要記得使用函數(shù)!

3.7 采用“由上至下”的編程方式

采用“由上至下”的方式來(lái)編寫(xiě)程序,而不要采用“由下到上”的方式(一開(kāi)始就處理細(xì)節(jié))。采用由上至下的方式,方便隨后逐步實(shí)現(xiàn)細(xì)節(jié),并能最終優(yōu)化原始函數(shù)。代碼將獨(dú)立于表示形式之外,因?yàn)樵谠O(shè)計(jì)較高層次的代碼時(shí),是不知道表示形式的。

3.8 不要優(yōu)化代碼

不要一開(kāi)始就試圖優(yōu)化代碼。首先要保證代碼的正確性,而后(如果需要的情況下)再追求代碼的執(zhí)行效率(在保證正確性的前提下)。

3.9 遵循“驚訝最少”原則

系統(tǒng)的反應(yīng)方式應(yīng)該以讓用戶(hù)感到“驚訝最少”為宜,比如,當(dāng)用戶(hù)在執(zhí)行一定行為時(shí),應(yīng)該能預(yù)知發(fā)生的結(jié)果,而不應(yīng)該為實(shí)際結(jié)果而感到驚訝。

這一點(diǎn)跟一致性有關(guān)。在具有一致性的系統(tǒng)中,多個(gè)模塊的執(zhí)行方式應(yīng)該保持一致,易于理解;而在有些不一致的系統(tǒng)中,每個(gè)模塊都各行其是。

如果某個(gè)函數(shù)的執(zhí)行方式讓你感到驚訝,或者是該函數(shù)解決的是另一個(gè)問(wèn)題,或者是函數(shù)名起錯(cuò)了。

3.10 終止副作用

Erlang 的有些原語(yǔ)具有一定的副作用。使用這些原語(yǔ)的函數(shù)將無(wú)法輕易地重用,因?yàn)檫@些原語(yǔ)會(huì)永久改變函數(shù)的環(huán)境,所以在調(diào)用這種例程前,要清楚了解進(jìn)程的確切狀態(tài)。

盡量利用無(wú)副作用的代碼來(lái)編程。

盡量編寫(xiě)純凈的函數(shù)。

收集具有副作用的函數(shù),清晰地注釋它們的所有副作用。

只需稍加留心,絕大多數(shù)代碼都可以用無(wú)副作用的方式來(lái)編寫(xiě),從而使系統(tǒng)的維護(hù)、測(cè)試變得非常容易,其他人也更容易理解系統(tǒng)。

3.11 不要“泄露”模塊內(nèi)的私有數(shù)據(jù)結(jié)構(gòu)

以下這個(gè)小例子會(huì)更容易闡述這一點(diǎn)。在下例中,為了實(shí)現(xiàn)隊(duì)列,定義了一個(gè)叫做 queue 的小模塊:

-module(queue).
-export([add/2, fetch/1]).

add(Item, Q) -> 
  lists:append(Q, [Item]).

fetch([H|T]) -> 
  {ok, H, T}; 
fetch([]) -> 
  empty.

上述代碼將隊(duì)列實(shí)現(xiàn)為列表的形式。不過(guò)遺憾的是,用戶(hù)在使用該模塊時(shí)必須知道隊(duì)列已經(jīng)被表現(xiàn)為列表形式。通常用到該模塊的程序可能含有以下代碼段:

NewQ = [], % 不要這樣做
Queue1 = queue:add(joe, NewQ), 
Queue2 = queue:add(mike, Queue1), ....

這很糟糕,因?yàn)橛脩?hù)(a)需要知道隊(duì)列被表現(xiàn)為列表,而且(b)實(shí)現(xiàn)者無(wú)法改變隊(duì)列的內(nèi)部表現(xiàn)(從而使他們以后可能想編寫(xiě)一個(gè)更好的模塊)。

所以,最好像下面這樣:

-module(queue).
-export([new/0, add/2, fetch/1]).

new() -> 
  [].

add(Item, Q) -> 
  lists:append(Q, [Item]).

fetch([H|T]) -> 
  {ok, H, T}; 
fetch([]) -> 
  empty.

現(xiàn)在,我們就能像下面這樣來(lái)調(diào)用該模塊了:

NewQ = queue:new(), 
Queue1 = queue:add(joe, NewQ), 
Queue2 = queue:add(mike, Queue1), ...

這樣做,不僅改正了前面談到的問(wèn)題,而且效率更好。假設(shè)用戶(hù)想知道隊(duì)列長(zhǎng)度,那么他們很可能會(huì)忍不住像下面這樣來(lái)調(diào)用模塊:

Len = length(Queue) % 不要這樣做

因?yàn)樗麄冎狸?duì)列被表現(xiàn)為列表的形式。所以再次說(shuō)明,這是一種非常丑陋的編程實(shí)踐,會(huì)讓代碼變得難以維護(hù)和理解。如果用戶(hù)想知道隊(duì)列長(zhǎng)度,那就必須給模塊加入一個(gè)長(zhǎng)度函數(shù),如下所示:

-module(queue).
-export([new/0, add/2, fetch/1, len/1]).

new() -> [].

add(Item, Q) ->
  lists:append(Q, [Item]).

fetch([H|T]) -> 
  {ok, H, T}; 

fetch([]) -> 
  empty.

len(Q) -> 
  length(Q).

現(xiàn)在用戶(hù)可以安全地調(diào)用 queue:len(Queue) 了。

現(xiàn)在我們可以認(rèn)為已經(jīng)將隊(duì)列的所有細(xì)節(jié)都抽象出來(lái)了(隊(duì)列實(shí)際上被稱(chēng)為“抽象數(shù)據(jù)結(jié)構(gòu)”)。

那我們還干嘛那么麻煩?通過(guò)對(duì)實(shí)現(xiàn)的內(nèi)部細(xì)節(jié)予以抽象處理這條編程實(shí)踐,對(duì)于那些會(huì)調(diào)用改變模塊中函數(shù)的模塊,我們完全可以在不改變它們代碼的前提下改變實(shí)現(xiàn)。因此,關(guān)于隊(duì)列這個(gè)例子,還有一個(gè)更好的實(shí)現(xiàn)方式,如下所示:

-module(queue).
-export([new/0, add/2, fetch/1, len/1]).

new() -> 
  {[],[]}.

add(Item, {X,Y}) -> % 加速元素的添加  
  {[Item|X], Y}.

fetch({X, [H|T]}) -> 
  {ok, H, {X,T}}; 

fetch({[], []) -> 
  empty; 

fetch({X, []) -> 
  % 只在有時(shí)才執(zhí)行這種復(fù)雜繁重的運(yùn)算
  fetch({[],lists:reverse(X)}).

len({X,Y}) -> 
  length(X) + length(Y).

3.12 盡量明確代碼的行為

確定性程序(deterministic program)指的是,不管運(yùn)行多少次,行為都能保持一致的程序。非確定性程序有時(shí)會(huì)產(chǎn)生不同的運(yùn)行結(jié)果。從調(diào)試的角度來(lái)看,也應(yīng)盡量保持程序的確定性,因?yàn)殄e(cuò)誤可以重現(xiàn)出來(lái),有助于調(diào)試。

例如,某個(gè)進(jìn)程必須開(kāi)啟 5 個(gè)并行的進(jìn)程,然后檢查這些進(jìn)程是否正確開(kāi)啟。另外,無(wú)需考慮這 5 個(gè)進(jìn)程開(kāi)啟的順序。

我們當(dāng)然可以并行開(kāi)啟 5 個(gè)進(jìn)程,然后檢查它們是否正確開(kāi)啟。但是,最好能同時(shí)開(kāi)啟它們,然后再檢查某一進(jìn)程是否能在下一進(jìn)程開(kāi)啟之前正確開(kāi)啟。

3.13 不要在編程中采取“防范”措施

防范型程序是指那種開(kāi)發(fā)者不信任輸入到系統(tǒng)中的數(shù)據(jù)的程序。總之,開(kāi)發(fā)人員不應(yīng)該測(cè)試函數(shù)輸入數(shù)據(jù)的正確性。系統(tǒng)中的絕大多數(shù)代碼應(yīng)該信任輸入數(shù)據(jù)。只有少量的一部分代碼才應(yīng)該執(zhí)行數(shù)據(jù)檢查,而這通常是發(fā)生在數(shù)據(jù)首次被輸入到“系統(tǒng)”中的時(shí)候,一旦數(shù)據(jù)進(jìn)入系統(tǒng),就應(yīng)該認(rèn)定該數(shù)據(jù)是正確的。

比如:

%% Args: Option is all|normal
get_server_usage_info(Option, AsciiPid) ->
  Pid = list_to_pid(AsciiPid),
  case Option of
    all -> get_all_info(Pid);
    normal -> get_normal_info(Pid)
  end.

如果 Option 不是 normalall,函數(shù)就會(huì)崩潰,本該如此。調(diào)用者應(yīng)負(fù)責(zé)提供正確的輸入數(shù)據(jù)。

3.14 利用設(shè)備驅(qū)動(dòng)來(lái)隔離硬件接口

應(yīng)該通過(guò)使用設(shè)備驅(qū)動(dòng)將硬件從系統(tǒng)中隔離出來(lái)。設(shè)備驅(qū)動(dòng)應(yīng)該實(shí)現(xiàn)硬件接口,使得硬件看起來(lái)像是 Erlang 的進(jìn)程。應(yīng)讓硬件的外在特征和行為像是普通的 Erlang 進(jìn)程。硬件應(yīng)該能夠接受并發(fā)送普通的 Erlang 消息,并在出現(xiàn)錯(cuò)誤時(shí)采用通??衫斫獾姆绞接枰曰貞?yīng)。

3.15 利用同一個(gè)函數(shù)來(lái)實(shí)現(xiàn)相反的兩種行為

假設(shè)有一個(gè)程序,功能是打開(kāi)文件,對(duì)文件執(zhí)行一些操作,以及關(guān)閉文件。編碼如下:

do_something_with(File) -> 
  case file:open(File, read) of, 
    {ok, Stream} ->
      doit(Stream), 
      file:close(Stream) % The correct solution
    Error -> Error
  end.

請(qǐng)注意在同一個(gè)例程中,打開(kāi)文件(file:open)與關(guān)閉文件(file:close)的對(duì)稱(chēng)性。下面的解決方案就比較難以實(shí)行,讓人搞不懂究竟關(guān)閉哪個(gè)文件。所以不要像這樣編程。

do_something_with(File) -> 
  case file:open(File, read) of, 
    {ok, Stream} ->
      doit(Stream)
    Error -> Error
  end.

doit(Stream) -> 
  ...., 
  func234(...,Stream,...).
  ...

func234(..., Stream, ...) ->
  ...,
  file:close(Stream) %% Don't do this