Ada 樣式指南/程式結構
適當的結構可以提高程式清晰度。這類似於較低級別的可讀性,並有助於使用可讀性指南(第 3 章)。Ada 提供的各種程式結構化功能旨在提高整體設計清晰度。這些指南展示瞭如何將這些功能用於其預期目的。
子包的概念支援子系統概念,其中子系統在 Ada 中表示為庫單元的層次結構。一般而言,大型系統應結構化為一系列子系統。子系統應用於表示邏輯相關的庫單元,這些單元共同實現單個、高階抽象或框架。
抽象和封裝由包概念和私有型別支援。相關資料和子程式可以組合在一起,並被更高層級視為單個實體。資訊隱藏透過強型別化和將包和子程式規範與其主體分離來強制執行。例外和任務是影響程式結構的額外 Ada 語言元素。
結構良好的程式易於理解、增強和維護。結構不良的程式在維護過程中經常被重新結構化,只是為了讓工作更容易。下面列出的許多指南通常作為一般程式設計指南給出。
- 將每個庫單元包的規範放在與其主體不同的檔案中。
- 避免定義不打算用作主程式的庫單元子程式。如果定義了此類子程式,則為每個庫單元子程式建立一個顯式規範,在單獨的檔案中。
- 最小化子單元的使用。
- 優先使用子庫單元,而不是子單元,將子系統結構化為易於管理的單元。
- 將每個子單元放在單獨的檔案中。
- 使用一致的檔案命名約定。
- 優先使用私有子單元規範,而不是巢狀在包主體中,並使用它來擴充套件父單元的抽象或服務。
- 將私有子單元規範用於(其他)子單元需要的資料和子程式,這些子單元擴充套件了父單元的抽象或服務。
以下檔名說明了一種可能的檔案組織和相關的一致命名約定。庫單元名稱對主體使用 adb 字尾。字尾 ads 表示規範,任何包含子單元的檔案都使用透過用下劃線分隔主體名稱和子單元名稱來構造的名稱
text_io.ads — the specification text_io.adb — the body text_io_integer_io.adb — a subunit text_io_fixed_io.adb — a subunit text_io_float_io.adb — a subunit text_io_enumeration_io.adb — a subunit
根據您的檔案系統允許您在檔名中使用的字元,您可以在檔名中更清楚地顯示父級名稱和子單元名稱之間的區別。例如,如果您的檔案系統允許使用“#”字元,則可以使用“#”分隔主體名稱和子單元名稱
text_io.ads — the specification text_io.adb — the body text_io#integer_io.adb — a subunit text_io#fixed_io.adb — a subunit text_io#float_io.adb — a subunit text_io#enumeration_io.adb — a subunit
某些作業系統區分大小寫,儘管 Ada 本身不區分大小寫。例如,您可以選擇使用全小寫檔名的約定。
本指南強調將規格說明和主體檔案分開的主要原因是為了最大程度地減少每次修改後所需的重新編譯次數。通常,在軟體開發過程中,單元主體比規格說明更新頻率更高。如果主體和規格說明位於同一個檔案中,那麼每次編譯主體時,即使規格說明沒有改變,也會對其進行編譯。由於規格說明定義了單元與其所有使用者之間的介面,因此對規格說明的這種重新編譯通常需要重新編譯所有使用者以驗證它們是否符合規格說明。如果使用者的規格說明和主體也一起存放,那麼這些單元的任何使用者也將需要重新編譯,以此類推。這種連鎖反應可能會迫使大量原本可以避免的編譯,嚴重拖慢專案的開發和測試階段。這就是為什麼你應該將所有庫單元(非巢狀單元)的規格說明放在與主體檔案不同的檔案中。
庫單元子程式應儘量簡化。庫單元子程式的唯一實際用途是作為主子程式。在幾乎所有其他情況下,將子程式嵌入到包中會更好。這提供了一個位置(包主體)來區域性化子程式所需的資料。此外,它還能減少系統中單獨模組的數量。
一般來說,你應該為在 with 子句中提到的任何庫單元子程式使用單獨的規格說明。這使得 with 的單元依賴於庫單元子程式的規格說明,而不是其主體。
你應該儘量減少使用子單元,因為它們會產生維護問題。出現在父主體中的宣告在子單元中可見,這增加了對子單元全域性的資料量,從而增加了更改的潛在連鎖反應。子單元阻礙了重用,因為它們鼓勵將原本可重用的程式碼直接放入子單元,而不是放到從多個子程式呼叫的公共例程中。
隨著 Ada 95 中子庫單元的出現,你可以避免大多數對子單元的使用。例如,與其使用子單元來實現一個大型巢狀主體,你應該嘗試將此程式碼封裝到一個子庫單元中,並新增必要的上下文子句。你可以修改子單元的主體,而無需重新編譯子系統中的任何其他單元。
使用多個獨立檔案的一個額外好處是,它允許不同的實現者使用傳統的編輯器同時修改系統的不同部分,這些編輯器不允許對單個檔案進行多個併發更新。
最後,將主體和規格說明分開使得可以為同一個規格說明提供多個主體,或者為同一個主體提供多個規格說明。儘管 Ada 要求在任何給定時間系統中每個主體都只有一個規格說明,但維護多個主體或多個規格說明以用於系統的不同構建仍然很有用。例如,一個規格說明可以有多個主體,每個主體都以不同的時間與空間效率權衡來實現相同的功能,或者對於機器依賴程式碼,每個目標機器可能有一個主體。在開發和測試期間,維護多個包規格說明也很有用。你可以開發一個規格說明交付給客戶,另一個規格說明用於單元測試。第一個只匯出在系統正常執行期間從包外部呼叫的那些子程式。第二個將匯出包的所有子程式,以便可以獨立測試每個子程式。
建議使用一致的檔案命名約定,以便更容易管理根據本指南可能產生的大量檔案。
在實現包規格說明中定義的抽象時,你通常需要編寫支援子程式來操縱資料的內部表示。這些子程式不應在介面上匯出。你可以選擇將它們放在父程式的包主體中,或者放在父包主體上下文子句中命名的子包中。當你在父包主體中放置它們時,你使它們對父包的所有客戶都不可訪問,包括在子包中宣告的父包的擴充套件。如果這些子程式需要實現父抽象的擴充套件,你將不得不修改父規格說明和主體,因為你必須在父規格說明中宣告擴充套件。這種技術將迫使重新編譯整個包(規格說明和主體)以及所有其客戶。
或者,你可以在私有子包中實現支援子程式。由於父單元的規格說明沒有修改,因此它及其客戶不需要重新編譯。在父單元主體中可能宣告的資料和子程式現在必須在私有子包的規格說明中宣告,以使它們對父單元主體以及擴充套件父單元服務或抽象的任何子單元可見。(另請參見指南 4.1.6 和 4.2。)這種私有子包的使用通常會最大程度地減少單元族及其客戶之間的重新編譯次數。
在宣告子包為私有時,你獲得了與在父包主體中宣告它類似的效果,因為父包的客戶不能在上下文子句中命名私有子包。你獲得了靈活性,因為現在你可以使用子包擴充套件父抽象,而無需重新編譯父規格說明或其主體,假設你沒有修改父包或其主體。這種額外的靈活性通常會彌補單元之間依賴性的增加,在本例中,父主體(和其他子包主體)上的附加上下文子句命名了支援子程式的私有子包。
配置編譯指示
[edit | edit source]指南
[edit | edit source]- 如果可能,透過編譯器選項或其他不需要修改原始碼的方法來表達配置編譯指示。
- 當配置編譯指示必須放在原始碼中時,考慮將它們隔離到每個分割槽的一個編譯單元;如果指定,建議使用該分割槽的 main 子程式。
原理
[edit | edit source]配置編譯指示通常用於選擇分割槽範圍或系統範圍的選項。通常,它們反映高階軟體架構決策(例如,pragma Task_Dispatching_Policy)或在特定應用領域中使用軟體(例如,安全關鍵軟體)。如果配置編譯指示嵌入到軟體元件中,並且該元件在不再適用該編譯指示的不同上下文中被重用,那麼它可能會在新的應用程式中引起問題。此類問題可能包括編譯系統拒絕其他合法原始碼或執行時出現意外行為。鑑於配置編譯指示的廣泛範圍,這些問題可能很嚴重。此外,原始系統的維護可能需要更改一些這些系統範圍的決策。如果配置編譯指示散佈在整個軟體中,那麼可能難以找到需要更改的行。
因此,建議所有配置編譯指示都儘可能儲存在單個編譯單元中,以便於定位和修改。如果此編譯單元不太可能被重用(例如,主子程式),那麼與未來重用者發生衝突的可能性就會降低。最後,如果這些系統範圍的決策完全沒有嵌入到程式碼中,而是透過編譯器選項等方式指示,那麼上述問題發生的可能性就更小。
例外情況
[edit | edit source]某些編譯指示(例如,pragma Suppress)可以使用多種形式,包括作為配置編譯指示。本指南不適用於這些編譯指示在不作為配置編譯指示的情況下使用。
子程式
[edit | edit source]指南
[edit | edit source]- 使用子程式來增強抽象。
- 將每個子程式限制為執行單個操作。
示例
[edit | edit source]你的程式需要作為選單驅動使用者介面包的一部分繪製使用者選項選單。由於選單的內容會根據使用者狀態而改變,因此正確的方法是編寫一個子程式來繪製選單。這樣,輸出子程式就只有一個目的,並且確定選單內容的方法在其他地方描述。
...
----------------------------------------------------------------------
procedure Draw_Menu
(Title : in String;
Options : in Menu) is
...
begin -- Draw_Menu
Ada.Text_IO.New_Page;
Ada.Text_IO.New_Line;
Ada.Text_IO.Set_Col (Right_Column);
Ada.Text_IO.Put_Line (Title);
Ada.Text_IO.New_Line;
for Choice in Alpha_Numeric loop
if Options (Choice) /= Empty_Line then
Valid_Option (Choice) := True;
Ada.Text_IO.Set_Col (Left_Column);
Ada.Text_IO.Put (Choice & " -- ");
Ada.Text_IO.Put_Line (Options (Choice));
end if;
...
end loop;
end Draw_Menu;
----------------------------------------------------------------------
原理
[edit | edit source]子程式是一種非常有效且為人熟知的抽象技術。子程式透過隱藏特定活動的細節來提高程式的可讀性。子程式不必被呼叫多次才能證明其存在。
註釋
[edit | edit source]指南 10.7.1 討論了處理子程式呼叫開銷的問題。
函式
[edit | edit source]指南
[edit | edit source]- 當子程式的主要目的是提供單個值時,使用函式。
- 儘量減少函式的副作用。
- 如果值不需要是靜態的,請考慮使用無引數函式。
- 如果值應該被從型別派生的型別繼承,請使用無引數函式(而不是常量)。
- 如果值本身可能會改變,請使用無引數函式。
示例
[edit | edit source]雖然從檔案中讀取一個字元會改變接下來讀取的字元,但這被認為是次要的副作用,與以下函式的主要目的相比。
function Next_Character return Character is separate;
但是,使用這樣的函式可能會導致一個微妙的問題。任何時候評估順序都是未定義的,函式返回的值的順序將實際上是未定義的。在這個例子中,放置在 Word 中的字元的順序以及後面的兩個字元傳遞給 Suffix 引數的順序是未知的。Next_Character 函式的任何實現都無法保證哪個字元將放在哪裡。
Word : constant String := String'(1 .. 5 => Next_Character);
begin -- Start_Parsing
Parse(Keyword => Word,
Suffix1 => Next_Character,
Suffix2 => Next_Character);
end Start_Parsing;
當然,如果順序不重要(如在隨機數生成器中),那麼評估順序就不重要。
以下示例顯示了使用無引數函式而不是常量的用法。
type T is private;
function Nil return T; -- This function is a derivable operation of type T
function Default return T; -- Also derivable, and the value can be changed by
-- recompiling the body of the function
同樣的示例可以使用常量編寫。
type T is private;
Nil : constant T;
Default : constant T;
原理
[edit | edit source]副作用是對任何不是子程式區域性變數的變數的改變。這包括在函式返回後,其他子程式和條目在從函式呼叫期間對變數的更改,如果更改仍然存在。副作用是不鼓勵的,因為它們難以理解和維護。此外,Ada 語言沒有定義在表示式中或作為子程式的實際引數時,函式的評估順序。因此,依賴於函式副作用發生順序的程式是錯誤的。任何地方都要避免使用副作用。
包
[edit | edit source]指南
[edit | edit source]- 使用包進行資訊隱藏。
- 使用帶有標記型別和私有型別的包來建立抽象資料型別。
- 使用包來模擬與問題領域相關的抽象實體。
- 使用包將相關的型別和物件宣告分組在一起(例如,用於兩個或多個庫單元的通用宣告)。
- 將機器依賴項封裝在包中。將特定裝置的軟體介面放在包中,以方便更改到不同的裝置。
- 將低階實現決策或介面放在包中的子程式中。
- 使用包和子程式來封裝和隱藏可能更改的程式細節(Nissen 和 Wallis 1984)。
示例
[edit | edit source]讀取外部檔案的名稱和其他屬性高度依賴於機器。名為 Directory 的包可以包含型別和子程式宣告,以支援對包含外部檔案的外部目錄的廣義檢視。它的內部可能反過來依賴於更具體的硬體或作業系統的其他包。
package Directory is
type Directory_Listing is limited private;
procedure Read_Current_Directory (D : in out Directory_Listing);
generic
with procedure Process (Filename : in String);
procedure Iterate (Over : in Directory_Listing);
...
private
type Directory_Listing is ...
end Directory;
---------------------------------------------------------------
package body Directory is
-- This procedure is machine dependent
procedure Read_Current_Directory (D : in out Directory_Listing) is separate;
procedure Iterate (Over : in Directory_Listing) is
...
begin
...
Process (Filename);
...
end Iterate;
...
end Directory;
原理
[edit | edit source]包是 Ada 中主要的結構化機制。它們旨在作為抽象、資訊隱藏和模組化的直接支援。例如,它們對封裝機器依賴性以幫助移植很有用。單個規範可以具有多個主體,隔離實現特定的資訊,以便程式碼的其他部分不需要更改。
封裝潛在更改區域有助於透過防止系統無關部分之間的不必要依賴關係來最大限度地減少實現該更改所需的工作量。
註釋
[edit | edit source]對本指南最普遍的異議通常涉及效能損失。有關子程式開銷的討論,請參見指南 10.7.1。
子庫單元
[edit | edit source]指南
[edit | edit source]- 如果一個新的庫單元代表了對原始抽象的邏輯擴充套件,請將其定義為子庫單元。
- 如果一個新的庫單元是獨立的(例如,引入了僅部分依賴於現有抽象的新抽象),那麼將新抽象封裝在單獨的庫單元中。
- 使用子包來實現子系統。
- 對子系統中應該對子系統客戶機可見的部分使用公共子單元。
- 對子系統中不應該對子系統客戶機可見的部分使用私有子單元。
- 對僅在實現包規範中使用的區域性宣告使用私有子單元。
- 使用子包來實現建構函式,即使它們返回訪問值。
示例
[edit | edit source]以下視窗系統示例取自 Cohen 等人 (1993),它說明了子單元在設計子系統中的一些用法。父級(根)包宣告其客戶機和子系統需要的型別、子型別和常量。各個子包提供視窗抽象的特定部分,例如原子、字型、圖形輸出、游標和鍵盤資訊。
package X_Windows is
...
private
...
end X_Windows;
package X_Windows.Atoms is
type Atom is private;
...
private
...
end X_Windows.Atoms;
package X_Windows.Fonts is
type Font is private;
...
private
...
end X_Windows.Fonts;
package X_Windows.Graphic_Output is
type Graphic_Context is private;
type Image is private;
...
private
...
end X_Windows.Graphic_Output;
package X_Windows.Cursors is
...
end X_Windows.Cursors;
package X_Windows.Keyboard is
...
end X_Windows.Keyboard;
原理
[edit | edit source]使用者可以使用更少的混亂的介面建立更精確的包,使用子庫包根據需要擴充套件介面。父級只包含相關功能。父級提供一個通用的介面,而子單元提供更完整的程式設計介面,根據它們正在擴充套件或定義的抽象的該方面進行定製。
子包建立在 Ada 的模組化優勢之上,其中“不同的規範和主體將包的使用者介面(規範)與其實現(主體)分離”(Rationale 1995,§II.7)。子包提供額外的功能,能夠擴充套件父包,而無需重新編譯父包或父包的客戶機。
子包允許您編寫邏輯上不同的包,它們共享私有型別。可見性規則使子規範的私有部分和子體的體對父級的私有部分具有可見性。因此,您可以避免為開發共享私有型別並需要了解其表示的抽象而建立單片包。私有表示對包的客戶不可見,因此包及其子類中的抽象得以維護。
對區域性宣告使用私有子包使您能夠在實現父包及其父包的擴充套件時獲得所需的支撐宣告。透過使用一組通用的支撐宣告(資料表示、資料操作子程式),您可以提高程式的可維護性。您可以修改內部表示和支撐子程式的實現,而無需修改或重新編譯子系統的其餘部分,因為這些支撐子程式是在私有子包的體中實現的。另請參見指南 4.1.1、4.2.1、8.4.1 和 8.4.8。
另請參見指南 9.4.1,瞭解在建立標記型別層次結構中使用子庫單元的討論。
內聚性
[edit | edit source]指南
[edit | edit source]- 使每個包都服務於單一目的。
- 使用包來分組相關資料、型別和子程式。
- 避免不相關的物件和子程式的集合(NASA 1987;Nissen 和 Wallis 1984)。
- 考慮對系統進行重構,將兩個高度相關的單元移入同一個包(或包層次結構)中,或者將相對獨立的單元移入單獨的包中。
示例
[edit | edit source]作為一個不好的例子,名為 Project_Definitions 的包顯然是特定專案的“萬能包”,很可能是一個混亂的集合。它可能採用這種形式,以允許專案成員將單個 with 子句合併到他們的軟體中。
更好的例子是名為 Display_Format_Definitions 的包,其中包含特定格式的特定顯示器所需的所有型別和常量,以及 Cartridge_Tape_Handler,其中包含所有提供與專用裝置介面的型別、常量和子程式。
原理
[edit | edit source]包中實體的相關程度直接影響對包和由包組成的程式的理解程度。分組有不同的標準,有些標準的效果不如其他標準。根據資料或活動的類別(例如,初始化模組)進行分組,或者根據資料或活動的時間特徵進行分組,效果不如根據功能或透過資料進行通訊的需要進行分組(Charette 1986)。
系統的“正確”結構可以對系統的可維護性產生巨大的影響。雖然在當時可能看起來很痛苦,但如果最初的結構不太正確,重要的是進行重構。
另請參見關於異構資料的指南 5.4.2。
註釋
[edit | edit source]傳統的子程式庫通常將功能無關的子程式分組在一起。即使這樣的庫也應該被分解成一組包,每個包都包含一組邏輯上內聚的子程式。
資料耦合
[edit | edit source]指南
[edit | edit source]- 避免在包規範中宣告變數。
示例
[edit | edit source]這是編譯器的一部分。處理錯誤訊息的包和包含程式碼生成器的包都需要知道當前的行號。與其將此儲存在 Natural 型別的共享變數中,不如將其儲存在一個隱藏此類資訊表示細節並透過訪問例程使其可用的包中。
-------------------------------------------------------------------------
package Compilation_Status is
type Line_Number is range 1 .. 2_500_000;
function Source_Line_Number return Line_Number;
end Compilation_Status;
-------------------------------------------------------------------------
with Compilation_Status;
package Error_Message_Processing is
-- Handle compile-time diagnostic.
end Error_Message_Processing;
-------------------------------------------------------------------------
with Compilation_Status;
package Code_Generation is
-- Operations for code generation.
end Code_Generation;
-------------------------------------------------------------------------
原理
[edit | edit source]緊密耦合的程式單元可能難以除錯,並且非常難以維護。透過使用訪問函式保護共享資料,耦合程度降低。這可以防止對資料結構的依賴,並且可以控制對資料的訪問。
註釋
[edit | edit source]對該指南最普遍的反對意見通常涉及效能損失。當變數移動到包主體時,必須提供訪問變數的子程式,並且在每次呼叫這些子程式時都會引入相關的開銷。有關子程式開銷的討論,請參見指南 10.7.1。
任務
[edit | edit source]指南
[edit | edit source]- 使用任務來模擬問題域中的抽象、非同步實體。
- 使用任務為多處理器體系結構定義併發演算法。
- 使用任務執行併發、迴圈或優先順序活動(NASA 1987)。
原理
[edit | edit source]該指南的理由在指南 6.1.2 中給出。第 6 章更詳細地討論了任務。
保護型別
[edit | edit source]指南
[edit | edit source]- 使用保護型別來控制或同步對資料或裝置的訪問。
- 使用保護型別來實現同步任務,例如被動資源監視器。
示例
[edit | edit source]請參見指南 6.1.1 中的示例。
原理
[edit | edit source]該指南的理由在指南 6.1.1 中給出。第 6 章更詳細地討論了併發和保護型別。
可見性
[edit | edit source]Ada 透過其可見性控制功能來強制資訊隱藏和關注點分離的能力是該語言最重要的優勢之一,特別是在“大型系統的各個部分被單獨開發時”。破壞這些功能,例如過度依賴 use 子句,是浪費和危險的。另請參見指南 5.7 和 9.4.1。
介面最小化
[edit | edit source]指南
[edit | edit source]- 僅將包使用所需的內容放入其規範中。
- 將包規範中的宣告數量降至最低。
- 不要僅僅因為構建簡單而包含額外的操作。
- 將包規範中的上下文(with)子句降至最低。
- 重新考慮似乎需要大量引數的子程式。
- 不要僅僅為了限制引數數量而在子程式或包中操作全域性資料。
- 避免不必要的可見性;將程式單元的實現細節隱藏在其使用者之外。
- 使用子庫單元來控制子系統介面部分的可見性。
- 對於那些不應該在子系統外部使用的宣告,使用私有子包。
- 使用子庫單元為不同的客戶端呈現實體的不同檢視。
- 在確定了介面預期客戶端的邏輯之後,設計(和重新設計)介面。
示例
[edit | edit source]-------------------------------------------------------------------------
package Telephone_Book is
type Listing is limited private;
procedure Set_Name (New_Name : in String;
Current : in out Listing);
procedure Insert (Name : in String;
Current : in out Listing);
procedure Delete (Obsolete : in String;
Current : in out Listing);
private
type Information;
type Listing is access Information;
end Telephone_Book;
-------------------------------------------------------------------------
package body Telephone_Book is
-- Full details of record for a listing
type Information is
record
...
Next : Listing;
end record;
First : Listing;
procedure Set_Name (New_Name : in String;
Current : in out Listing) is separate;
procedure Insert (Name : in String;
Current : in out Listing) is separate;
procedure Delete (Obsolete : in String;
Current : in out Listing) is separate;
end Telephone_Book;
-------------------------------------------------------------------------
原理
[edit | edit source]對於規範中的每個實體,都要仔細考慮是否可以將其移到子包或父包主體中。無關細節越少,程式、包或子程式就越容易理解。對於維護人員來說,瞭解包介面究竟是什麼非常重要,這樣他們才能理解更改的影響。子程式的介面超出了引數範圍。從包或子程式內部對全域性資料的任何修改也是對“外部”的未記錄介面。
透過將不必要的子句移到主體中來將規範上的上下文子句降至最低。這種技術使讀者更容易,將庫單元更改時所需的重新編譯區域性化,並有助於防止修改過程中的連鎖反應。另請參見指南 4.2.3。
具有大量引數的子程式通常表明設計決策不當(例如,子程式的功能邊界不合適,或者引數結構不佳)。相反,沒有引數的子程式可能會訪問全域性資料。
在包規範中可見的物件可以被任何有權訪問它們的單元修改。該物件不能被其封閉包保護或抽象地表示。必須持久化的物件應在包體中宣告。其值依賴於其封閉包外部程式單元的物件可能位於錯誤的包中,或者最好透過包規範中指定的子程式訪問。
子庫單元可以提供分層庫的不同檢視。工程師可以為客戶提供與實現者不同的檢視(基本原理 1995,第 10.1 節)。透過建立私有子包,工程師可以提供僅在父庫單元根目錄下的子系統內部可用的設施。私有子包規範中的宣告不會匯出到子系統外部。因此,工程師可以在私有子包中宣告實現抽象所需的實用程式(例如,除錯實用程式 [Cohen 等人 1993]),並確保抽象的使用者(即客戶端)無法訪問這些實用程式。
不同的客戶端可能對本質上相同的資源有不同的需求。與其建立多個版本的資源,不如考慮使用子單元,為不同的目的匯出不同的檢視。
嚴格基於預測客戶端“可能”需要的需求來設計介面會導致介面臃腫和不合適。然後發生的情況是,客戶端試圖“適應”介面並繞過不合適的介面,重複邏輯上應該是共享抽象的一部分的程式碼。有關從可重用性角度考慮介面的討論,請參見指南 8.3.1。
在某些情況下,子程式庫看起來像大型的單片包。在這種情況下,將它們分解成更小的包,根據類別對它們進行分組(例如,三角函式),可能會很有益。
- 使用子包而不是巢狀包來呈現同一抽象的不同檢視。
- 僅為了分組操作或隱藏公共實現細節,才將包規範巢狀在另一個包規範中。
Ada 參考手冊(1995)的附件 A 給出了包規範巢狀的示例。泛型包 Generic_Bounded_Length 的規範巢狀在包 Ada.Strings.Bounded 的規範中。巢狀的包是泛型的,它將緊密相關的操作分組在一起。
將包規範分組到一個包含的包中,強調了這些包之間共性的關係。它還允許它們共享從這種關係產生的公共實現細節。巢狀包允許你組織包的名稱空間,這與在子程式或任務體中巢狀的語義效果形成對比。
抽象有時需要向不同類別的使用者呈現不同的檢視。將一個檢視構建到另一個檢視之上作為額外的抽象並不總是足夠的,因為檢視呈現的操作的功能可能只是部分分離。巢狀規範將各種檢視的設施分組在一起,但仍將其與它們呈現的抽象關聯在一起。由於存在多個使用子句或不協調的限定名稱組合,另一個單元對檢視的濫用混合將很容易檢測到。
請參見指南 4.2.1 中討論的基本原理。
- 考慮使用私有子包代替巢狀。
- 如果不能使用私有子包,則應儘可能地限制程式單元的可見性,方法是在包體中巢狀它們(Nissen 和 Wallis 1984)。
- 最大程度地減少在子程式和任務中巢狀程式單元。
- 最大程度地減少 with 子句適用的範圍。
- 僅使用那些直接需要的單元。
此程式說明了使用子庫單元來限制可見性。過程 Rational_Numbers.Reduce 巢狀在 Rational_Numbers 的主體中,以將其可見性限制在該抽象的實現中。與其將文字輸入/輸出設施對整個有理數層次結構可見,不如只將其對子庫 Rational_Numbers.IO 的主體可見。此示例改編自 Ada 參考手冊(1995,第 7.1 [帶註釋]、7.2 [帶註釋] 和 10.1.1 [帶註釋] 節)。
-------------------------------------------------------------------------
package Rational_Numbers is
type Rational is private;
function "=" (X, Y: Rational) return Boolean;
function "/" (X, Y: Integer) return Rational; -- construct a rational number
function "+" (X, Y: Rational) return Rational;
function "-" (X, Y: Rational) return Rational;
function "*" (X, Y: Rational) return Rational;
function "/" (X, Y: Rational) return Rational; -- rational division
private
...
end Rational_Numbers;
package body Rational_Numbers is
procedure Reduce (R :in out Rational) is . . . end Reduce;
. . .
end Rational_Numbers;
package Rational_Numbers.IO is
procedure Put (R : in Rational);
procedure Get (R : out Rational);
end Rational_Numbers.IO;
with Ada.Text_IO;
with Ada.Integer_Text_IO;
package body Rational_Numbers.IO is -- has visibility to parent private type declaration
procedure Put (R : in Rational) is
begin
Ada.Integer_Text_IO.Put (Item => R.Numerator, Width => 0);
Ada.Text_IO.Put ("/");
Ada.Integer_Text_IO.Put (Item => R.Denominator, Width => 0);
end Put;
procedure Get (R : out Rational) is . . . end Get;
end Rational_Numbers.IO;
限制程式單元的可見性可確保程式單元不會從系統中除預期位置之外的其他地方呼叫。這可以透過將其巢狀在唯一使用它的單元中,透過將其隱藏在包體中而不是在包規範中宣告它,或者透過將其宣告為私有子單元來完成。這避免了錯誤,並透過保證對該單元的本地更改不會產生不可預見的影響,從而簡化了維護人員的工作。
透過對子單元使用 with 子句而不是對整個父單元使用 with 子句來限制庫單元的可見性,在相同方面很有用。在上面的示例中,很明顯包 Text_IO 僅由編譯器的 Listing_Facilities 包使用。
不鼓勵在子程式和任務中進行巢狀,因為這會導致不可重用的元件。這些元件本質上不可重用,因為它們對定義上下文進行了不希望的上級引用。除非你確實想要確保程式單元不會從系統中某個意外的位置呼叫,否則應最大程度地減少這種巢狀形式。
有關子單元使用的討論,另請參見指南 4.2.1。
最小化 with 子句覆蓋範圍的一種方法是僅將其用於真正需要它的子單元。當對庫單元的可見性需求僅限於一兩個子程式時,請考慮將這些子單元作為單獨的編譯單元。
- 仔細考慮任務的封裝。
-------------------------------------------------------------------------
package Disk_Head_Scheduler is
type Words is ...
type Track_Number is ...
procedure Transmit (Track : in Track_Number;
Data : in Words);
...
end Disk_Head_Scheduler;
-------------------------------------------------------------------------
package body Disk_Head_Scheduler is
...
task Control is
entry Sign_In (Track : in Track_Number);
...
end Control;
----------------------------------------------------------------------
task Track_Manager is
entry Transfer(Track_Number) (Data : in Words);
end Track_Manager;
----------------------------------------------------------------------
...
procedure Transmit (Track : in Track_Number;
Data : in Words) is
begin
Control.Sign_In(Track);
Track_Manager.Transfer(Track)(Data);
end Transmit;
----------------------------------------------------------------------
...
end Disk_Head_Scheduler;
-------------------------------------------------------------------------
是否在封閉包的規範或主體中宣告任務,這並非易事。兩者都有充分的論據。
將任務規範隱藏在包體中,並僅透過(子程式)匯出必要的條目,可以減少包規範中多餘的資訊。它允許您的子程式強制執行任務正常執行所需的任何條目呼叫順序。它還允許您實施防禦性任務通訊實踐(參見指南 6.2.2)以及正確使用條件和定時條目呼叫。最後,它允許將條目分組為集合,以便匯出到不同類別的使用者(例如,生產者與消費者),或者隱藏不應該公開的條目(例如,初始化、完成、訊號)。在效能是一個問題,並且沒有順序規則需要強制執行的情況下,可以將條目重新命名為子程式,以避免額外過程呼叫的開銷。
一個論點,可以看作是優點或缺點,是隱藏包體中的任務規範隱藏了任務實現的事實。如果應用程式是這樣的,即對任務實現的更改或從任務實現的更改或任務之間服務的重組不需要讓包的使用者關心,那麼這是一個優點。但是,如果包使用者必須瞭解任務實現才能推斷全域性任務行為,那麼最好不要完全隱藏任務。要麼將其移動到包規範中,要麼添加註釋說明存在任務實現,描述何時呼叫可能會阻塞等等。否則,包實現者有責任確保包的使用者不必擔心死鎖、飢餓和競爭條件等行為。
最後,請記住,隱藏在過程介面後面的任務會阻止使用條件和定時條目呼叫以及條目族,除非您在過程中新增引數和額外程式碼,以使呼叫者能夠將過程定向到使用這些功能。
本節探討了程式結構中異常情況的問題。它討論瞭如何在單元介面中使用異常情況,包括宣告和引發哪些異常,以及在什麼條件下引發異常。有關如何處理、傳播和避免引發異常的資訊,請參閱指南 5.8。有關如何處理可移植性問題的指南,請參閱指南 7.5。
- 對於無法避免的內部錯誤,使用者無法恢復,請宣告一個使用者可見的異常。在抽象內部,提供一種方法來區分不同的內部錯誤。
- 不要從其他上下文中借用異常名稱。
- 匯出(對使用者可見地宣告)所有可能引發的異常的名稱。
- 在包中,記錄每個子程式和任務條目可能引發哪些異常。
- 不要為可以在單元內避免或糾正的內部錯誤引發異常。
- 不要使用同一個異常來報告單元使用者可以區分的不同型別的錯誤。
- 提供詢問函式,允許單元使用者避免引發異常。
- 如果可能,請在引發異常之前避免更改單元中的狀態資訊。
- 在最早的機會捕獲和轉換或處理所有預定義的和編譯器定義的異常。
- 不要顯式引發預定義的或實現定義的異常。
- 永遠不要讓異常傳播到其範圍之外。
此包規範定義了兩個異常,它們增強了抽象
-------------------------------------------------------------------------
generic
type Element is private;
package Stack is
function Stack_Empty return Boolean;
function Stack_Full return Boolean;
procedure Pop (From_Top : out Element);
procedure Push (Onto_Top : in Element);
-- Raised when Pop is used on empty stack.
Underflow : exception;
-- Raised when Push is used on full stack.
Overflow : exception;
end Stack;
-------------------------------------------------------------------------
...
----------------------------------------------------------------------
procedure Pop (From_Top : out Element) is
begin
...
if Stack_Empty then
raise Underflow;
else -- Stack contains at least one element
Top_Index := Top_Index - 1;
From_Top := Data(Top_Index + 1);
end if;
end Pop;
--------------------------------------------------------------------
...
異常應該用作抽象的一部分,以指示抽象無法阻止或糾正的錯誤條件。因為抽象無法糾正此類錯誤,所以它必須將錯誤報告給使用者。在使用錯誤(例如,嘗試以錯誤的順序呼叫操作或嘗試超過邊界條件)的情況下,使用者可能能夠糾正錯誤。在超出使用者控制範圍的錯誤情況下,如果有多種機制可用於執行所需操作,使用者可能能夠解決錯誤。在其他情況下,使用者可能不得不放棄使用該單元,進入功能有限的降級模式。無論哪種情況,都必須通知使用者。
異常是報告此類錯誤的良好機制,因為它們為處理錯誤提供了備用控制流程。這允許錯誤處理程式碼與正常處理程式碼分開。當引發異常時,當前操作將中止,控制權將直接轉移到適當的異常處理程式。
上面的幾個指南是為了最大限度地提高使用者區分和糾正不同型別錯誤的能力。宣告新的異常名稱,而不是引發在其他包中宣告的異常,可以減少包之間的耦合,還可以使不同的異常更易於區分。匯出單元可以引發的所有異常的名稱,而不是在單元內部宣告它們,使得單元使用者可以在異常處理程式中引用這些名稱。否則,使用者只能使用 others 處理程式來處理異常。最後,使用註釋來準確記錄包中宣告的哪些異常可能被每個子程式或任務條目引發,從而使使用者能夠知道在每種情況下哪些異常處理程式是合適的。
在存在抽象使用者無法採取任何明智操作的錯誤情況(例如,沒有解決方法或降級模式)的情況下,最好匯出單個內部錯誤異常。在包內,您應該考慮區分不同的內部錯誤。例如,您可以以不同的方式記錄或處理不同型別的內部錯誤。但是,當您將錯誤傳播給使用者時,您應該使用特殊的內部錯誤異常,表明無法進行使用者恢復。在傳播錯誤時,您還應該提供相關資訊,使用 Ada.Exceptions 中提供的設施。因此,對於任何抽象,您實際上提供了 N + 1 個不同的異常:N 個不同的可恢復錯誤和一個不可恢復錯誤,它們沒有對映到抽象。應用程式需求以及客戶在錯誤資訊方面的需求/願望都會幫助您為抽象識別合適的異常。
因為它們會導致立即轉移控制權,所以異常對於報告不可恢復的錯誤很有用,這些錯誤會阻止操作完成,但不會報告操作完成的附帶狀態或模式。它們不應用於報告單元能夠在使用者不可見的情況下糾正的內部錯誤。
為了為使用者提供最大的靈活性,最好提供詢問函式,使用者可以呼叫這些函式來確定如果呼叫子程式或任務條目是否會引發異常。上面的示例中的 Stack_Empty 函式就是這樣一種函式。它指示如果呼叫 Pop 是否會引發 Underflow。提供此類函式使使用者能夠避免觸發異常。
為了支援其使用者的錯誤恢復,單元應該嘗試避免在引發異常的呼叫期間更改狀態。如果無法完全正確地執行請求的操作,那麼單元應該在更改任何內部狀態資訊之前檢測到這一點,或者應該恢復到請求時的狀態。例如,在引發 Underflow 異常後,上面的堆疊包應該保持與呼叫 Pop 時完全相同的狀態。如果它要針對管理堆疊的內部資料結構進行部分更新,那麼以後的 Push 和 Pop 操作將無法正確執行。這始終是可取的,但並非總是可行的。
應該使用使用者定義的異常而不是預定義的或編譯器定義的異常,因為它們更具描述性,並且更特定於抽象。預定義的異常非常通用,可以由許多不同的情況觸發。編譯器定義的異常不可移植,其含義可能會在同一個編譯器的連續版本之間發生變化。這引入了太多不確定性,無法建立有用的處理程式。
如果您正在編寫抽象,請記住使用者不知道您在實現中使用的單元。這是資訊隱藏的結果。如果您的抽象中引發了任何異常,您必須捕獲並處理它。如果允許原始異常從您的抽象主體傳播出去,使用者將無法提供合理的處理程式。如果您自己的抽象無法有效地恢復,您仍然可以將異常轉換為使用者可以理解的形式。
轉換異常意味著在原始異常的處理程式中引發使用者定義的異常。這為單元使用者引入了有意義的匯出名稱。一旦錯誤情況以應用程式的術語表達,就可以以這些術語進行處理。
- 將每個庫單元包的規範放在與其主體不同的檔案中。
- 避免定義不打算用作主程式的庫單元子程式。如果定義了此類子程式,則為每個庫單元子程式建立一個顯式規範,在單獨的檔案中。
- 最小化子單元的使用。
- 優先使用子庫單元,而不是子單元,將子系統結構化為易於管理的單元。
- 將每個子單元放在單獨的檔案中。
- 使用一致的檔案命名約定。
- 優先使用私有子單元規範,而不是巢狀在包主體中,並使用它來擴充套件父單元的抽象或服務。
- 將私有子單元規範用於(其他)子單元需要的資料和子程式,這些子單元擴充套件了父單元的抽象或服務。
- 如果可能,透過編譯器選項或其他不需要修改原始碼的方法來表達配置選項。.
- 當配置編譯指示必須放在原始碼中時,考慮將它們隔離到每個分割槽的一個編譯單元;如果指定,建議使用該分割槽的 main 子程式。
- 使用子程式來增強抽象。
- 將每個子程式限制為執行單個操作。
- 當子程式的主要目的是提供單個值時,使用函式。
- 儘量減少函式的副作用。
- 如果值不需要是靜態的,請考慮使用無引數函式。
- 如果值應該被從型別派生的型別繼承,請使用無引數函式(而不是常量)。
- 如果值本身可能會改變,請使用無引數函式。
- 使用包進行資訊隱藏。
- 使用帶有標記型別和私有型別的包來建立抽象資料型別。
- 使用包來模擬與問題領域相關的抽象實體。
- 使用包將相關的型別和物件宣告分組在一起(例如,用於兩個或多個庫單元的通用宣告)。
- 將機器依賴項封裝在包中。將特定裝置的軟體介面放在包中,以方便更改到不同的裝置。
- 將低階實現決策或介面放在包中的子程式中。
- 使用包和子程式來封裝和隱藏可能更改的程式細節(Nissen 和 Wallis 1984)。
- 如果一個新的庫單元代表了對原始抽象的邏輯擴充套件,請將其定義為子庫單元。
- 如果一個新的庫單元是獨立的(例如,引入了僅部分依賴於現有抽象的新抽象),那麼將新抽象封裝在單獨的庫單元中。
- 使用子包來實現子系統。
- 對子系統中應該對子系統客戶機可見的部分使用公共子單元。
- 對子系統中不應該對子系統客戶機可見的部分使用私有子單元。
- 對僅在實現包規範中使用的區域性宣告使用私有子單元。
- 使用子包來實現建構函式,即使它們返回訪問值。
- 使每個包都服務於單一目的。
- 使用包來分組相關資料、型別和子程式。
- 避免不相關的物件和子程式的集合(NASA 1987;Nissen 和 Wallis 1984)。
- 考慮對系統進行重構,將兩個高度相關的單元移入同一個包(或包層次結構)中,或者將相對獨立的單元移入單獨的包中。
- 避免在包規範中宣告變數。
- 使用任務來模擬問題域中的抽象、非同步實體。
- 使用任務為多處理器體系結構定義併發演算法。
- 使用任務執行併發、迴圈或優先順序活動(NASA 1987)。
- 使用保護型別來控制或同步對資料或裝置的訪問。
- 使用保護型別來實現同步任務,例如被動資源監視器。
- 僅將包使用所需的內容放入其規範中。
- 將包規範中的宣告數量降至最低。
- 不要僅僅因為構建簡單而包含額外的操作。
- 將包規範中的上下文(with)子句降至最低。
- 重新考慮似乎需要大量引數的子程式。
- 不要僅僅為了限制引數數量而在子程式或包中操作全域性資料。
- 避免不必要的可見性;將程式單元的實現細節隱藏在其使用者之外。
- 使用子庫單元來控制子系統介面部分的可見性。
- 對於那些不應該在子系統外部使用的宣告,使用私有子包。
- 使用子庫單元向不同的客戶端展示實體的不同檢視。
- 在確定了介面預期客戶端的邏輯之後,設計(和重新設計)介面。
- 使用子包而不是巢狀包來呈現同一抽象的不同檢視。
- 僅為了分組操作或隱藏公共實現細節,才將包規範巢狀在另一個包規範中。
- 考慮使用私有子包代替巢狀。
- 如果不能使用私有子包,則應儘可能地限制程式單元的可見性,方法是在包體中巢狀它們(Nissen 和 Wallis 1984)。
- 最大程度地減少在子程式和任務中巢狀程式單元。
- 最大程度地減少 with 子句適用的範圍。
- 僅使用那些直接需要的單元。
- 仔細考慮任務的封裝。
- 對於無法避免的內部錯誤,使用者無法恢復,請宣告一個使用者可見的異常。在抽象內部,提供一種方法來區分不同的內部錯誤。
- 不要從其他上下文中借用異常名稱。
- 匯出(對使用者可見地宣告)所有可能引發的異常的名稱。
- 在包中,記錄每個子程式和任務條目可能引發哪些異常。
- 不要為可以在單元內避免或糾正的內部錯誤引發異常。
- 不要使用同一個異常來報告單元使用者可以區分的不同型別的錯誤。
- 提供詢問函式,允許單元使用者避免引發異常。
- 如果可能,請在引發異常之前避免更改單元中的狀態資訊。
- 在最早的機會捕獲和轉換或處理所有預定義的和編譯器定義的異常。
- 不要顯式引發預定義的或實現定義的異常。
- 永遠不要讓異常傳播到其範圍之外。