跳轉到內容

Ada 風格指南/可重用性

來自華夏公益教科書

可移植性 · 面向物件特性

可重用性是指程式碼可以在不同的應用程式中以最小的修改被使用的程度。當代碼在新的應用程式中被重用時,該新應用程式部分繼承了該程式碼的屬性。如果程式碼是可維護的,則應用程式更易於維護。如果它是可移植的,那麼應用程式更易於移植。因此,本章的指南在應用本書中的所有其他指南時最有用。幾個指南針對可維護性問題。可維護的程式碼易於更改以滿足新的或不斷變化的需求。可維護性在重用中發揮著特殊作用。當試圖重用程式碼時,通常需要更改程式碼以適合新的應用程式。如果程式碼不能輕易更改,那麼它就不太可能被重用。

軟體重用涉及許多問題:是否重用部分,如何在庫中儲存和檢索可重用部分,如何認證部分,如何最大化重用的經濟價值,如何激勵工程師和整個公司重用部分而不是重新發明它們,等等。本章忽略了這些管理、經濟和物流問題,而是專注於如何在 Ada 中編寫軟體部分以提高重用潛力的單個技術問題。其他問題同樣重要,但超出了本書的範圍。

Ada 的設計目標之一是促進可重用部分的建立和使用,以提高生產力。為此,Ada 提供了開發可重用部分並在可用後對其進行調整的功能。包、可見性控制和單獨編譯支援模組化和資訊隱藏(參見第 4.1、4.2、5.3 和 5.7 節中的指南)。這允許分離程式碼的應用程式特定部分,最大化適合重用的通用部分,並允許在模組內隔離設計決策,從而促進更改。Ada 型別系統支援資料定義的本地化,以便一致的更改易於進行。Ada 繼承特性支援型別擴充套件,以便可以為應用程式定製資料定義和介面。泛型單元直接支援開發通用、可適應的程式碼,這些程式碼可以例項化為執行特定功能。Ada 95 對面向物件技術和抽象的改進支援上述所有目標。仔細使用這些特性並符合本書中的指南,會產生更有可能被重用的程式碼。

可重用程式碼的開發方式多種多樣。程式碼可以從以前的專案中獲取。可以從頭開始為特定領域(如數學庫)開發一個可重用的程式碼庫。可重用程式碼可以作為特定應用程式的有意副產品而開發。可重用程式碼的開發方式可能會有所不同,因為設計方法需要它。本指南適用於所有這些情況。

經驗豐富的程式設計師認識到,軟體重用更多地是一個需求和設計問題,而不是編碼問題。本節中的指南旨在在一個開發可重用程式碼的整體方法中發揮作用。本節將不涉及設計、測試等方面的內容。有關與 Ada 語言相關的重用問題的研究,可以在 AIRMICS(1990 年)、Edwards(1990 年)和 Wheeler(1992 年)中找到。

  • 無論開發方法如何,經驗表明,可重用程式碼具有某些特徵,本章做出以下假設。
  • 可重用部分必須易於理解。可重用部分應該是清晰的典範。對可重用部分進行註釋的要求甚至比對特定應用程式部分的要求更加嚴格。
  • 可重用部分必須具有最高質量。它們必須正確、可靠且健壯。可重用部分中的錯誤或弱點可能會產生深遠的影響,因此,其他程式設計師必須對任何提供重用的部分有高度的信心。
  • 可重用部分必須具有適應性。為了最大限度地發揮其重用潛力,可重用部分必須能夠適應各種使用者的需求。
  • 可重用部分應該獨立。應該能夠重用單個部分,而無需採用許多看似無關的其他部分。

除了這些標準之外,可重用部分必須比重新發明更容易重用,必須高效,並且必須可移植。如果重用一個部分比從頭開始建立一個部分需要更多的努力,或者重用的部分效率不夠高,那麼重用就不會輕易發生。有關可移植性的指南,請參見第 7 章。本章不應孤立地閱讀。在很多方面,一個寫得好的、可重用的元件僅僅是一個寫得好的元件的極端例子。前幾章和第 9 章中的所有指南都適用於可重用元件以及特定於單個應用程式的元件。隨著對 1995 年 Ada 標準修訂版的經驗不斷積累,新的指南可能會出現,而其他指南可能會發生變化。此處列出的指南專門適用於可重用元件。

本章中的指南經常使用“考慮……”這樣的措辭,因為硬性規則不能適用於所有情況。您可以在給定情況下做出的具體選擇涉及設計權衡。這些指南的基本原理旨在讓您瞭解其中的一些權衡。

理解和清晰度

[edit | edit source]

對於旨在重用的部分,尤其重要的是它們應該易於理解。部分的作用、使用方法、將來可能進行的預期更改以及工作原理是必須從註釋和程式碼本身的檢查中直接顯現的事實。為了最大限度地提高可重用部分的可讀性,請遵循第 3 章中的指南,其中一些指南在下面重複強調。

與應用程式無關的命名

[edit | edit source]

指南

[edit | edit source]
  • 為可重用部分及其識別符號選擇限制最少的名稱。
  • 選擇通用名稱以避免與通用例項的命名約定衝突。
  • 使用表示可重用部分的行為特徵及其抽象的名稱。

示例

[edit | edit source]

通用堆疊抽象

------------------------------------------------------------------------
generic
   type Item is private;
package Bounded_Stack is
   procedure Push (New_Item    : in     Item);
   procedure Pop  (Newest_Item :    out Item);
   ...
end Bounded_Stack;
------------------------------------------------------------------------

適當地重新命名以供當前應用程式使用

with Bounded_Stack;

...

   type Tray is ...
   package Tray_Stack is 
      new Bounded_Stack (Item => Tray);

基本原理

[edit | edit source]

為可重用部分選擇一個通用或與應用程式無關的名稱,可以鼓勵其廣泛重用。當該部分在特定上下文中使用時,可以對其進行例項化(如果是泛型)或使用更具體的名稱重新命名。

當存在一個明顯的、最簡單、最清晰的名稱選擇用於可重用部分時,最好將該名稱留給該部分的重用者使用,為可重用部分選擇一個更長、更具描述性的名稱。因此,對於泛型堆疊包來說,Bounded_Stack 比 Stack 更好的名稱,因為它將更簡單的名稱 Stack 留給例項化使用。

在可重用部分的名稱中包含行為特徵的指示(但不包含實現的指示),以便具有相同抽象(例如,多個堆疊包)但具有不同限制(有界、無界等)的多個部分可以儲存在同一個 Ada 庫中並用作同一個 Ada 程式的一部分。

縮寫

[edit | edit source]

指南

[edit | edit source]
  • 在識別符號或單元名稱中不要使用縮寫。

示例

[edit | edit source]
------------------------------------------------------------------------
with Ada.Calendar;
package Greenwich_Mean_Time is
   function Clock return Ada.Calendar.Time;
   ...
end Greenwich_Mean_Time;
------------------------------------------------------------------------

以下縮寫可能在應用程式中使用時不清楚

with Ada.Calendar;
with Greenwich_Mean_Time;
...
   function Get_GMT return Ada.Calendar.Time renames
          Greenwich_Mean_Time.Clock;

基本原理

[edit | edit source]

這是一條比指南 3.1.4 更強的指南。無論註釋得多麼好,縮寫都可能在一些未來的重用上下文中造成混亂。即使是普遍接受的縮寫,如 GMT 代表格林威治標準時間,也會造成問題,應該謹慎使用。

本指南與指南 3.1.4 之間的區別在於領域問題。當領域定義明確時,在該領域中接受的縮寫和首字母縮略詞將闡明應用程式的含義。當同一程式碼從其特定於域的上下文中刪除時,這些縮寫可能變得毫無意義。

在上面的示例中,包 Greenwich_Mean_Time 可以用於任何應用程式,而不會丟失含義。但函式 Get_GMT 很容易與不同領域中的其他首字母縮略詞混淆。

註釋

[edit | edit source]

請參見指南 5.7.2,瞭解重新命名子句的正確用法。如果特定應用程式廣泛使用 Greenwich_Mean_Time 領域,則可以在該應用程式中將包 GMT 重新命名為 GMT

with Greenwich_Mean_Time;
...
   package GMT renames Greenwich_Mean_Time;

泛型形式引數

[edit | edit source]

指南

[edit | edit source]
  • 像文件化任何包規範一樣,文件化泛型形式引數的預期行為。

示例

[edit | edit source]

以下示例顯示瞭如何開發非常通用的演算法,但必須對其進行清晰的文件化才能使用。

------------------------------------------------------------------------
generic
   -- Index provides access to values in a structure.  For example,
   -- an array, A.
   type Index is (<>);
   type Element is private;
   type Element_Array is array (Index range <>) of Element;
   -- The function, Should_Precede, does NOT compare the indexes
   -- themselves; it compares the elements of the structure.
   -- The function Should_Precede is provided rather than a "Less_Than" function
   -- because the sort criterion need not be smallest first.
   with function Should_Precede (Left  : in     Element;
                                 Right : in     Element)
     return Boolean;
   -- This procedure swaps values of the structure (the mode won't
   -- allow the indexes themselves to be swapped!)
   with procedure Swap (Index1 : in     Index;
                        Index2 : in     Index;
                        A      : in out Element_Array);
   -- After the call to Quick_Sort, the indexed structure will be
   -- sorted:
   --     For all i,j in First..Last :  i<j  =>  A(i) < A(j).
procedure Quick_Sort (First : in     Index := Index'First;
                      Last  : in     Index := Index'Last);
------------------------------------------------------------------------

基本原理

[edit | edit source]

泛型功能是 Ada 的一個最強大的特性,因為它具有形式化。但是,並非所有關於泛型形式引數的假設都可以在 Ada 中直接表達出來。泛型的任何使用者都必須準確地瞭解該泛型需要什麼才能正確執行。

在某種意義上,泛型規範是一份合同,例項化者必須提供形式引數,作為回報,他們將獲得規範的工作例項。當合同完整並明確所有假設時,雙方都能得到最好的服務。

健壯性

[edit | edit source]

以下指南提高了 Ada 程式碼的健壯性。很容易編寫依賴於你沒有意識到你正在做出的假設的程式碼。當這樣的部分在不同的環境中重用時,它可能會意外地中斷。本節中的指南展示了 Ada 程式碼如何才能自動符合其環境,以及如何才能使其檢查假設違規情況。最後,還給出了一些指南,以警告您 Ada 無法像您希望的那樣及時捕獲的錯誤。

命名數字

[edit | edit source]

指南

[edit | edit source]
  • 使用命名的數字和靜態表示式,可以讓多個依賴關係連結到少量符號。

示例

[edit | edit source]
------------------------------------------------------------------------
procedure Disk_Driver is
   -- In this procedure, a number of important disk parameters are
   -- linked.
   Number_Of_Sectors  : constant :=     4;
   Number_Of_Tracks   : constant :=   200;
   Number_Of_Surfaces : constant :=    18;
   Sector_Capacity    : constant := 4_096;
   Track_Capacity   : constant := Number_Of_Sectors  * Sector_Capacity;
   Surface_Capacity : constant := Number_Of_Tracks   * Track_Capacity;
   Disk_Capacity    : constant := Number_Of_Surfaces * Surface_Capacity;
   type Sector_Range  is range 1 .. Number_Of_Sectors;
   type Track_Range   is range 1 .. Number_Of_Tracks;
   type Surface_Range is range 1 .. Number_Of_Surfaces;
   type Track_Map   is array (Sector_Range)  of ...;
   type Surface_Map is array (Track_Range)   of Track_Map;
   type Disk_Map    is array (Surface_Range) of Surface_Map;
begin  -- Disk_Driver
   ...
end Disk_Driver;
------------------------------------------------------------------------

基本原理

[edit | edit source]

為了重用使用命名的數字和靜態表示式的軟體,只需要重置一個或少量常量,所有宣告和相關程式碼就會自動更改。除了簡化重用,這還減少了出錯的機會,並使用不易出錯的註釋記錄了型別和常量的含義。

無約束陣列

[edit | edit source]

指南

[edit | edit source]
  • 對於陣列形式引數和陣列返回值,使用無約束陣列型別。
  • 在適當的情況下,使區域性變數的大小取決於實際引數的大小。

示例

[edit | edit source]
   ...
   type Vector is array (Vector_Index range <>) of Element;
   type Matrix is array
           (Vector_Index range <>, Vector_Index range <>) of Element;
   ...
   ---------------------------------------------------------------------
   procedure Matrix_Operation (Data : in     Matrix) is
      Workspace   : Matrix (Data'Range(1), Data'Range(2));
      Temp_Vector : Vector (Data'First(1) .. 2 * Data'Last(1));
   ...
   ---------------------------------------------------------------------

基本原理

[edit | edit source]

無約束陣列可以宣告為其大小取決於形式引數的大小。當用作區域性變數時,它們的大小會隨著提供的實際引數自動更改。此功能可用於幫助適應部分,因為區域性變數中必要的大小更改會自動處理。

最小化和記錄假設

[edit | edit source]

指南

[edit | edit source]
  • 最小化單元做出的假設數量。
  • 對於無法避免的假設,使用子型別或約束來自動強制一致性。
  • 對於無法透過子型別自動強制執行的假設,請在程式碼中新增顯式檢查。
  • 記錄所有假設。
  • 如果程式碼依賴於特定特殊需求附件的實現才能正常執行,請在程式碼中記錄此假設。

示例

[edit | edit source]

以下寫得不好的函式記錄了但沒有檢查其假設

   -- Assumption:  BCD value is less than 4 digits.
   function Binary_To_BCD (Binary_Value : in     Natural)
     return BCD;

下一個示例強制執行與假設的一致性,使檢查自動化並將註釋變為不必要

   subtype Binary_Values is Natural range 0 .. 9_999;
   function Binary_To_BCD (Binary_Value : in     Binary_Values)
     return BCD;

下一個示例明確檢查並記錄其假設

   ---------------------------------------------------------------------
   -- Out_Of_Range raised when BCD value exceeds 4  digits.
   function Binary_To_BCD (Binary_Value : in     Natural)
     return BCD is
      Maximum_Representable : constant Natural := 9_999;
   begin  -- Binary_To_BCD
      if Binary_Value > Maximum_Representable then
         raise Out_Of_Range;
      end if;
      ...
   end Binary_To_BCD;
   ---------------------------------------------------------------------

基本原理

[edit | edit source]

任何打算在其他程式中再次使用的部分,尤其是其他程式很可能由其他人編寫,都應該是健壯的。它應該透過定義其介面來強制執行儘可能多的假設,並透過在任何無法透過介面強制執行的假設上新增顯式防禦性檢查來防禦誤用。透過記錄對特殊需求附件的依賴關係,您可以警告使用者他應該只在提供必要支援的編譯環境中重用元件。

註釋

[edit | edit source]

您可以透過仔細選擇或構造形式引數的子型別來限制輸入值的範圍。當您這樣做時,編譯器生成的檢查程式碼可能比您可能編寫的任何檢查都更有效。事實上,這種檢查是語言中強型別化的意圖的一部分。但是,對於使用者選擇引數型別的泛型單元來說,這提出了一個挑戰。您的程式碼必須構建為處理使用者可能選擇為例項化選擇的任何子型別的任何值。

泛型規範中的子型別

[edit | edit source]

指南

[edit | edit source]
  • 在宣告模式為 in out 的泛型形式物件時,使用第一個子型別。
  • 注意在宣告泛型形式子程式的引數或返回值時使用子型別作為子型別標記。
  • 使用屬性而不是字面量。

示例

[edit | edit source]

在以下示例中,似乎為泛型形式物件 Object 提供的任何值都將被約束在 1..10 的範圍內。它還似乎在任何例項化中執行時傳遞給 Put 例程的引數以及 Get 例程返回的值也將受到類似約束

   subtype Range_1_10 is Integer range 1 .. 10;
   ---------------------------------------------------------------------
   generic
      Object : in out Range_1_10;
      with procedure Put (Parameter : in     Range_1_10);
      with function  Get return Range_1_10;
   package Input_Output is
      ...
   end Input_Output;
   ---------------------------------------------------------------------

但是,情況並非如此。鑑於以下合法例項化

   subtype Range_15_30 is Integer range 15 .. 30;
   Constrained_Object : Range_15_30 := 15;
   procedure Constrained_Put (Parameter : in     Range_15_30);
   function  Constrained_Get return Range_15_30;
   package Constrained_Input_Output is
      new Input_Output (Object => Constrained_Object,
                        Put    => Constrained_Put,
                        Get    => Constrained_Get);
   ...

Object、Parameter 和 Get 的返回值被約束在 15..30 範圍內。因此,例如,如果泛型包的正文包含賦值語句

Object := 1;

當執行此例項化時,將引發 Constraint_Error。

基本原理

[edit | edit source]

該語言規定,當對泛型形式物件和泛型形式子程式的引數以及返回值執行約束檢查時,將強制執行實際子型別的約束(而不是形式子型別)(Ada 語言參考手冊 1995, §§12.4 [Annotated] 和 §12.6 [Annotated])。因此,在形式 in out 物件引數中指定的子型別以及在形式子程式配置檔案中指定的子型別不需要與實際物件或子程式的子型別匹配。

因此,即使使用已經例項化並測試過多次的泛型單元,以及在例項化時沒有報告任何錯誤的例項化,也可能出現執行時錯誤。因為泛型形式的子型別約束被忽略,Ada 語言參考手冊 (1995, §§12.4 [Annotated] 和 §12.6 [Annotated]) 建議在這些地方使用基本型別的名稱以避免混淆。即使這樣,您也必須小心,不要假設可以使用基本型別的任何值,因為例項化會強制執行泛型實際引數的子型別約束。為了安全起見,始終透過包含 'First、'Last、'Pred 和 'Succ 之類的屬性的符號表達式來引用型別的特定值,而不是透過字面量。

對於泛型,屬性提供了保持通用性的方法。可以使用字面量,但字面量存在違反某些約束的風險。例如,假設陣列的索引從 1 開始可能會在泛型例項化為基於零的陣列型別時造成問題。

註釋

[edit | edit source]

新增一個定義泛型形式物件子型別的泛型形式引數並不能解決上述理由中討論的約束檢查規則的影響。您可以使用任何允許的子型別例項化泛型形式型別,並且您不能保證此子型別是第一個子型別

generic
   type Object_Range is range <>;
   Objects : in out Object_Range;
   ...
package X is
   ...
end X;

您可以使用任何 Integer 子型別例項化子型別 Object_Range,例如 Positive。但是,實際變數 Object 可以是 Positive'Base,即 Integer,並且其值不能保證大於 0。

泛型單元中的過載

[編輯 | 編輯原始碼]
  • 謹慎對待過載同一個泛型包匯出的子程式的名稱。
------------------------------------------------------------------------
generic
   type Item is limited private;
package Input_Output is
   procedure Put (Value : in     Integer);
   procedure Put (Value : in     Item);
end Input_Output;
------------------------------------------------------------------------

基本原理

[編輯 | 編輯原始碼]

如果上面的示例中顯示的泛型包被例項化為 Integer(或 Integer 的任何子型別)作為與泛型形式引數 Item 相對應的實際型別,那麼兩個 Put 過程具有相同的介面,並且對 Put 的所有呼叫都是模稜兩可的。因此,此包不能與型別 Integer 一起使用。在這種情況下,最好為所有子程式指定明確的名稱。請參閱 Ada 參考手冊 1995,第 12.3 節 [註釋] 獲取更多資訊。

隱藏的任務

[編輯 | 編輯原始碼]
  • 在規範中,記錄任何透過 with'ing 規範並使用規範的任何部分而被啟用的任務。
  • 記錄從泛型單元中隱藏的任務中訪問哪些泛型形式引數。
  • 記錄任何多執行緒元件。

基本原理

[編輯 | 編輯原始碼]

當可重用程式碼進入即時系統領域時,任務的影響成為一個主要因素。即使任務可能用於其他目的,它們對排程演算法的影響仍然是一個問題,必須清楚地記錄下來。有了清楚記錄的任務,即時程式設計師就可以分析效能、優先順序等以滿足時間要求,或者必要時修改甚至重新設計元件。

必須仔細規劃對資料結構的併發訪問以避免錯誤,特別是對於非原子資料結構(有關詳細資訊,請參閱第 6 章)。如果泛型單元從包含在泛型單元中的任務內訪問其泛型形式引數之一(讀取或寫入泛型形式物件的值,或呼叫讀取或寫入資料的泛型形式子程式),那麼就有可能發生併發訪問,使用者可能沒有計劃。在這種情況下,應該在泛型規範中的註釋中提醒使用者。

  • 將異常從可重用部分傳播出去。僅當您確定處理在所有情況下都合適時,才在可重用部分內處理異常。
  • 在執行泛型形式子程式正確操作的任何必要清理後,傳播泛型形式子程式引發的異常。
  • 在引發異常時,將狀態變數置於有效狀態。
  • 在引發異常時,將引數保持不變。
------------------------------------------------------------------------
generic
   type Number is limited private;
   with procedure Get (Value :    out Number);
procedure Process_Numbers;

------------------------------------------------------------------------
procedure Process_Numbers is
   Local : Number;
   procedure Perform_Cleanup_Necessary_For_Process_Numbers is separate;
   ...
begin  -- Process_Numbers
   ...
   Catch_Exceptions_Generated_By_Get:
      begin
         Get (Local);
      exception
         when others =>
            Perform_Cleanup_Necessary_For_Process_Numbers;
            raise;
      end Catch_Exceptions_Generated_By_Get;
   ...
end Process_Numbers;
------------------------------------------------------------------------

基本原理

[編輯 | 編輯原始碼]

在大多數情況下,引發異常是因為發生了不希望發生的事件(例如浮點溢位)。這些事件通常需要以完全不同的方式處理,使用特定軟體部分的不同方法。很難預測使用者希望如何處理異常的所有方式。將異常傳出該部分是最安全的處理方式。

特別是,當泛型形式子程式引發異常時,泛型單元無法理解原因或知道要採取什麼糾正措施。因此,此類異常應始終傳播回泛型例項化的呼叫者。但是,泛型單元必須首先清理自身,將其內部資料結構恢復到正確狀態,以便在呼叫者處理當前異常後可以對其進行未來的呼叫。出於這個原因,所有對泛型形式子程式的呼叫都應該在 when others 異常處理程式的範圍內,如果內部狀態被修改,如上面的示例所示。

當呼叫可重用部分時,該部分的使用者應該能夠準確地知道已執行了什麼操作(在適當的抽象級別)。為了實現這一點,可重用部分必須始終執行其指定功能的所有操作或都不執行;它永遠不能做一半。因此,任何透過引發或傳播異常而提前終止的可重用部分都應該返回給呼叫者,對內部或外部狀態沒有影響。實現這一點的最簡單方法是在進行任何狀態更改(修改內部狀態變數、呼叫其他可重用部分以修改其狀態、更新檔案等)之前測試所有可能的異常條件。當這不可能時,最好在引發或傳播異常之前將所有內部和外部狀態恢復為呼叫該部分時當前的值。即使這不可能,也必須在該部分規範的註釋標題中記錄這種潛在的危險情況。

當引發異常時,模式為 out 或 in out 的引數也會出現類似的問題。Ada 語言區分“按值傳遞”和“按引用傳遞”引數傳遞。在某些情況下,需要“按值傳遞”;在其他情況下,需要“按引用傳遞”;在剩餘的情況下,允許使用任一機制。潛在問題出現在語言未指定要使用的引數傳遞機制的那些情況下。當引發異常時,不會發生複製回,但對於按引用傳遞引數的 Ada 編譯器(在允許選擇的情況下),實際引數已經更新。當引數按值傳遞時,更新不會發生。為了減少歧義,提高可移植性,並避免在引發異常時,某些實際引數被更新,而另一些沒有被更新的情況,最好將 out 和 in out 引數的值視為狀態變數,僅在確定不會引發異常後才更新它們。另請參閱指南 7.1.8。

可重用部分的範圍可以從低階構建塊(例如,資料結構、排序演算法、數學函式)到大型可重用子系統。構建塊的級別越低,可重用部分就越不可能知道如何處理異常或產生有意義的結果。因此,低階部分應該傳播異常。但是,大型可重用子系統應該能夠獨立於其重用範圍內的差異來處理任何預期的異常。

適應性

[編輯 | 編輯原始碼]

可重用部分通常需要在特定應用程式中使用之前進行更改。它們應該被結構化,以便更改容易,並且儘可能地區域性化。實現適應性的一種方法是建立具有完整功能的通用部分,其中只有一部分可能在給定的應用程式中需要。實現適應性的另一種方法是使用 Ada 的泛型構造來生成可以適當地例項化不同引數的部分。這兩種方法都避免了透過更改程式碼來適應部分的容易出錯的過程,但它們有侷限性,並且可能帶來一些開銷。應該儘可能地提供預期的更改,即開發人員可以合理預見到的更改。無法預見的更改只能透過仔細地結構化一個可適應的部分來適應。許多與可維護性相關的考慮因素都適用。如果程式碼質量高,清晰,並且符合資訊隱藏等公認的設計原則,那麼在無法預見的情況下進行適應就更容易。

完整的功能

[編輯 | 編輯原始碼]
  • 在可重用部分或一組部分中提供核心功能,以便可以在此抽象級別中對其進行有意義的擴充套件,使其能夠被重複使用。
  • 更具體地說,為每個可能包含動態資料的的資料結構提供初始化和終結過程。
  • 對於需要初始化和終結的資料結構,請考慮儘可能從型別 Ada.Finalization.Controlled 或 Ada.Finalization.Limited_Controlled 派生它們。
   Incoming : Queue;
   ...
   Set_Initial (Incoming);     -- initialization operation
   ...
   if Is_Full (Incoming) then  -- query operation
      ...
   end if;
   ...
   Clean_Up (Incoming);        -- finalization operation

基本原理

[編輯 | 編輯原始碼]

此功能在設計/程式設計抽象時尤為重要。您需要在抽象的完整性和可擴充套件性之間取得平衡。完整性確保您正確地配置了抽象,沒有關於其執行環境的內建假設。它還確保函式的適當分離,以便它們對當前應用程式有用,並且在其他組合中對其他應用程式有用。可擴充套件性確保重用者可以透過擴充套件新增功能,使用標記型別層次結構(參見指南 8.4.8 和第 9 章)或子庫包(參見指南 4.1.6、8.4.1 和 9.4.1)。

在設計重用時,您需要從乾淨的抽象角度思考。如果您提供的功能太少,並且依賴於重用者擴充套件抽象,他們可能會遇到缺乏凝聚力的抽象。這種混雜的抽象繼承了許多操作,並非所有操作都是必要的,也不都是協同工作的。

當可重用部分可以使用動態資料合理地實現時,任何必須控制記憶體的應用程式都可以使用初始化和終結例程來防止記憶體洩漏。然後,如果資料結構變得動態,對這些問題敏感的應用程式可以輕鬆地進行調整。

預定義型別 Ada.Finalization.Controlled 或 Ada.Finalization.Limited_Controlled 提供自動的、使用者定義的初始化、調整和終結過程。當您宣告受控型別和物件時,您可以保證編譯器將插入必要的初始化、調整和終結呼叫,從而使您的程式碼更不容易出錯,更易於維護。當覆蓋受控型別上的 Initialize 和 Finalize 例程時,請確保呼叫父 Initialize 或 Finalize。

此示例說明了結束條件函式。抽象應該在使用者有機會破壞它之前自動初始化。如果不可能,則應為它提供初始化操作。在任何情況下,它都需要終結操作。提供初始化和終結操作的一種方法是從預定義型別 Ada.Finalization.Controlled 或 Ada.Finalization.Limited_Controlled 派生抽象。在任何可能的情況下,應提供查詢操作來確定何時即將超過限制,以便使用者可以避免導致異常被引發。

為許多物件提供重置操作也很有用。要了解重置和初始化可能有所不同,請考慮個人計算機上“熱啟動”和“冷啟動”的類似情況。

即使並非所有這些操作都適合抽象,考慮這些操作的練習有助於形成一組完整的操作,其中其他操作可能被另一個應用程式使用。

語言的一些實現將包的所有子程式連結到可執行檔案中,而不管它們是否被使用,這使得未使用的操作成為負擔(參見指南 8.4.5)。在這種情況下,如果開銷很大,請建立功能齊全部分的副本,並用註釋註釋掉未使用的操作,並說明它們在此應用程式中是多餘的。

泛型單元

[編輯 | 編輯原始碼]
  • 使用泛型單元避免程式碼重複。
  • 為泛型單元引數化以實現最大適應性。
  • 重用泛型單元的常見例項,以及泛型單元本身。

基本原理

[編輯 | 編輯原始碼]

Ada 不允許在執行期間將資料型別作為實際引數傳遞給子程式。此類引數必須在例項化泛型單元時指定為泛型形式引數。因此,如果您想編寫一個子程式,該子程式在呼叫時對它操作的物件的資料型別存在差異,那麼您必須將子程式編寫為泛型單元,併為每個資料型別引數組合例項化它一次。然後可以將單元的例項化作為常規子程式呼叫。

您可以透過宣告訪問子程式值或泛型形式子程式引數來將子程式作為實際引數傳遞。有關權衡的討論,請參見指南 5.3.4。

如果您發現自己編寫了兩個非常相似的例程,它們只在它們操作的資料型別或它們呼叫的子程式方面有所不同,那麼最好將例程編寫一次作為泛型單元,並例項化兩次以獲得所需的兩個版本。當以後需要修改這兩個例程時,只需要在一個地方進行更改。這極大地簡化了維護工作。

做出這種選擇後,請考慮這兩個例項可能共有的其他方面,這些方面不是例程本質所必需的。將它們作為泛型形式引數分解出來。當以後需要第三個類似的例程時,如果您已經預見到了它與另外兩個例程之間的所有差異,那麼可以透過第三次例項化自動生成它。引數化的泛型單元可以非常可重用。

編寫泛型單元而不是非泛型單元所付出的努力似乎很大。然而,一旦您熟悉泛型設施,編寫泛型單元並不比編寫非泛型單元困難或耗時得多。在很大程度上,這只是一個練習問題。此外,只要該單元被重用,在單元開發中付出的任何努力都將得到收回,如果該單元被放置在具有足夠可見性的重用庫中,它肯定會被重用。不要將您對潛在重用的思考侷限於您正在進行的應用程式或您非常熟悉的其他應用程式。您不熟悉或將來可能會重用您的軟體的應用程式可能會重用您的軟體。

編寫泛型單元並將其放置在您的重用庫中後,您可能要做的第一件事就是根據您的特定需求例項化它一次。此時,最好考慮一下哪些例項很可能被廣泛使用。如果是這樣,將每個這樣的例項放置在您的重用庫中,以便其他人可以找到和共享它們。

另請參見指南 9.3.5。

形式私有和受限私有型別

[編輯 | 編輯原始碼]
  • 當您在泛型體內部不需要對該型別物件的賦值時,請考慮使用受限私有型別作為泛型形式型別。
  • 當您在泛型體內部需要對該型別物件的正常賦值時,請考慮使用非受限私有型別作為泛型形式型別。
  • 當您需要在泛型體內部對該型別物件強制執行特殊賦值語義時,請考慮使用從 Ada.Finalization.Controlled 派生的形式標記型別。
  • 匯出限制最少的型別,以維護資料和抽象的完整性,同時允許替代實現。
  • 請考慮使用受限私有抽象型別作為擴充套件形式私有標記型別的泛型的泛型形式型別。

第一個示例顯示了一種僅提供資料結構的模板情況,在這種情況下,在泛型體內部顯然不需要賦值。

------------------------------------------------------------------------
generic
   type Element_Type is limited private;
package Generic_Doubly_Linked_Lists is
   type Cell_Type;
   type List_Type is access all Element_Type;
   type Cell_Type is
      record
         Data     : Element_Type;
         Next     : List_Type;
         Previous : List_Type;
      end record;
end Generic_Doubly_Linked_Lists;

第二個示例顯示了一個模板,它從作為泛型形式引數傳遞的(非賦值)操作中組合新操作。

generic
   type Element_Type is limited private;
   with procedure Process_Element (X : in out Element_Type);
   type List_Type is array (Positive range <>) of Element_Type;
procedure Process_List (L : in out List_Type);
procedure Process_List (L : in out List_Type) is
begin -- Process_List
   for I in L'Range loop
      Process_Element (L(I));
   end loop;
end Process_List;
------------------------------------------------------------------------
generic
   type Domain_Type is limited private;
   type Intermediate_Type is limited private;
   type Range_Type is limited private;
   with function Left (X : Intermediate_Type) return Range_Type;
   with function Right (X : Domain_Type) return Intermediate_Type;
function Generic_Composition (X : Domain_Type) return Range_Type;
-- the function Left o Right
function Generic_Composition (X : Domain_Type) return Range_Type is
begin  -- generic_Composition
   return Left (Right (X));
end Generic_Composition;

第三個示例顯示瞭如何使用 Ada 的受控型別來提供特殊的賦值語義。

with Ada.Finalization;
generic
   type Any_Element is new Ada.Finalization.Controlled with private;
   Maximum_Stack_Size : in Natural := 100;
package Bounded_Stack is
   type Stack is private;
   procedure Push (On_Top      : in out Stack;
                   New_Element : in     Any_Element);
   procedure Pop  (From_Top    : in out Stack;
                   Top_Element :    out Any_Element);
   Overflow  : exception;
   Underflow : exception;
   ...
private
   type Stack_Information;
   type Stack is access Stack_Information;
end Bounded_Stack;

基本原理

[編輯 | 編輯原始碼]

為了使泛型元件在儘可能多的上下文中可用,它應該儘量減少對環境的假設,並明確說明任何必要的假設。在 Ada 中,泛型單元做出的假設可以透過泛型形式引數的型別明確說明。受限私有泛型形式型別阻止泛型單元對該型別物件的結構或為該型別物件定義的操作做出任何假設。私有(非受限)泛型形式型別允許假設為該型別定義了賦值和相等比較操作。因此,受限私有資料型別不能指定為私有泛型形式型別的實際引數。

通常,您應該根據泛型內部對賦值的需要選擇私有或受限私有泛型形式型別。受限私有型別應該用於不需要賦值的抽象,如上面前兩個示例所示。在第三個示例中,需要賦值,指定從受控型別派生的型別以確保正確的賦值語義可用。如果您在泛型體內部需要相等性,您可能還需要重新定義相等性以獲得正確的語義;然後,您需要為 = 函式包括形式泛型子程式引數。

可重用部分匯出的型別的情況正好相反。對於匯出的型別,受限和受限私有指定的限制是針對部分使用者的限制,而不是針對部分本身的限制。為了為可重用部分的使用者提供最大能力,請匯出限制最少的型別。根據需要應用限制,以保護匯出資料結構和抽象的完整性,以滿足為該泛型設想的各種實現。

由於受限私有型別限制性強,因此它們並不總是可重用部件匯出型別的最佳選擇。在允許使用者複製和比較資料物件以及底層資料型別不涉及訪問型別(以便整個資料結構被複制或比較)的情況下,最好匯出一個(非受限)私有型別。在允許使用者複製和比較資料物件以及底層資料型別涉及訪問型別(以便整個資料結構被複制或比較)的情況下,最好匯出一個受控型別和一個(重寫的)相等運算。在不會損害抽象的情況下,可以使用非私有型別(例如,數值型、列舉型、記錄型或陣列型)。

泛型單元的一種用途是建立混合泛型(參見指南 8.3.8)來擴充套件標記型別。在這種情況下,您希望使用最嚴格的型別作為泛型形式型別,即既受限又抽象的形式型別。當您例項化泛型時,如果實際型別是非受限的,則型別擴充套件也將是非受限的。在泛型包中,您必須將型別擴充套件宣告為抽象的。然後,泛型的例項化器可以再次擴充套件該型別以實現所需的混合配置。

註釋

[edit | edit source]

預定義包 Sequential_IO 和 Direct_IO 使用私有型別。這會使受限私有型別的 I/O 要求複雜化,應在設計期間加以考慮。

還有一些情況必須使用受限私有形式型別。這些情況出現在形式型別具有訪問區分量,或者形式型別用作定義型別擴充套件的父型別,而該型別擴充套件本身包含一個受限型別的元件(例如,任務型別),或者形式型別定義了一個新的具有訪問區分量的區分量部分。

使用泛型單元封裝演算法

[edit | edit source]

指南

[edit | edit source]
  • 使用泛型單元封裝獨立於資料型別的演算法。

示例

[edit | edit source]

這是泛型排序過程的規範

------------------------------------------------------------------------
generic
   type Element is private;
   type Data    is array (Positive range <>) of Element;
   with function Should_Precede (Left  : in     Element;
                                 Right : in     Element)
          return Boolean is <>;
 with procedure Swap (Left  : in out Element;
                        Right : in out Element) is <>;
procedure Generic_Sort (Data_To_Sort : in out Data);
------------------------------------------------------------------------

泛型主體看起來就像一個常規過程主體,並且可以充分利用泛型形式引數來實現排序演算法

------------------------------------------------------------------------
procedure Generic_Sort (Data_To_Sort : in out Data) is
begin
   ...
   for I in Data_To_Sort'Range loop
      ...
         ...
         if Should_Precede (Data_To_Sort(J), Data_To_Sort(I)) then
            Swap(Data_To_Sort(I), Data_To_Sort(J));
         end if;
         ...
      ...
   end loop;
   ...
end Generic_Sort;
------------------------------------------------------------------------

泛型過程可以例項化為

   type Integer_Array is array (Positive range <>) of Integer;
   function Should_Precede (Left  : in     Integer;
                            Right : in     Integer)
     return Boolean;

   procedure Swap (Left  : in out Integer;
                   Right : in out Integer);
   procedure Sort is
      new Generic_Sort (Element => Integer,
                        Data    => Integer_Array);

或者

   subtype String_80    is String (1 .. 80);
   type    String_Array is array (Positive range <>) of String_80;
   function Should_Precede (Left  : in     String_80;
                            Right : in     String_80)
     return Boolean;

   procedure Swap (Left  : in out String_80;
                   Right : in out String_80);

   procedure Sort is
      new Generic_Sort (Element => String_80,
                        Data    => String_Array);

並呼叫為

   Integer_Array_1 : Integer_Array (1 .. 100);
   ...
   Sort (Integer_Array_1);

或者

   String_Array_1  : String_Array  (1 .. 100);
   ...
   Sort (String_Array_1);

基本原理

[edit | edit source]

排序演算法可以獨立於被排序的資料型別來描述。此泛型過程將 Element 資料型別作為泛型受限私有型別引數,因此它對實際操作物件的型別假設儘可能少。它還將 Data 作為泛型形式引數,以便例項化可以將整個陣列傳遞給它們以進行排序。最後,它明確要求執行排序所需的兩個運算子:Should_Precede 和 Swap。排序演算法在不引用任何資料型別的情況下被封裝。泛型可以例項化以對任何資料型別的陣列進行排序。8.3.5 使用泛型單元進行資料抽象

指南

[edit | edit source]
  • 考慮使用抽象資料型別(不要與 Ada 的抽象型別混淆)而不是抽象資料物件。
  • 考慮使用泛型單元來實現獨立於其元件資料型別的抽象資料型別。

示例

[edit | edit source]

此示例介紹了一系列不同的技術,可用於生成抽象資料型別和物件。對每種技術的優缺點的討論將在下面的原理部分進行。第一個是抽象資料物件 (ADO),它可用於封裝抽象狀態機。它封裝了一個整數堆疊

------------------------------------------------------------------------
package Bounded_Stack is
   subtype Element is Integer;
   Maximum_Stack_Size : constant := 100;
   procedure Push (New_Element : in     Element);
   procedure Pop  (Top_Element :    out Element);
   Overflow  : exception;
   Underflow : exception;
   ...
end Bounded_Stack;
------------------------------------------------------------------------

第二個示例是抽象資料型別 (ADT)。它與 ADO 不同,因為它匯出了 Stack 型別,允許使用者宣告任意數量的整數堆疊。由於現在可能存在多個堆疊,因此有必要在對 Push 和 Pop 的呼叫中指定 Stack 引數

------------------------------------------------------------------------
package Bounded_Stack is
   subtype Element is Integer;
   type    Stack   is limited private;
   Maximum_Stack_Size : constant := 100;
   procedure Push (On_Top      : in out Stack;
                   New_Element : in     Element);
   procedure Pop  (From_Top    : in out Stack;
                   Top_Element :    out Element);
   Overflow  : exception;
   Underflow : exception;
   ...
private
   type Stack_Information;
   type Stack is access Stack_Information;
end Bounded_Stack;
------------------------------------------------------------------------

第三個示例是無引數泛型抽象資料物件 (GADO)。它與 ADO(第一個示例)的不同之處僅在於它是泛型的,因此使用者可以多次例項化它以獲得多個整數堆疊

------------------------------------------------------------------------
generic
package Bounded_Stack is
   subtype Element is Integer;
   Maximum_Stack_Size : constant := 100;
   procedure Push (New_Element : in     Element);
   procedure Pop  (Top_Element :    out Element);
   Overflow  : exception;
   Underflow : exception;
   ...
end Bounded_Stack;
------------------------------------------------------------------------

第四個示例是對第三個示例的略微變體,仍然是 GADO,但具有引數。它與第三個示例的不同之處在於,它將堆疊的資料型別設定為泛型引數,以便可以建立除 Integer 以外其他資料型別的堆疊。此外,Maximum_Stack_Size 已被設定為一個泛型引數,該引數預設為 100,但可以由使用者指定,而不是由包定義的常量

------------------------------------------------------------------------
generic
   type Element is private;
   Maximum_Stack_Size : in Natural := 100;
package Bounded_Stack is
   procedure Push (New_Element : in     Element);
   procedure Pop  (Top_Element :    out Element);
   Overflow  : exception;
   Underflow : exception;
   ...
end Bounded_Stack;
------------------------------------------------------------------------

第五個示例是泛型抽象資料型別 (GADT)。它與第四個示例中的 GADO 的不同之處與第二個示例中的 ADT 與第一個示例中的 ADO 的不同之處相同;它匯出了 Stack 型別,允許使用者宣告任意數量的堆疊

------------------------------------------------------------------------
generic
   type Element is private;
   Maximum_Stack_Size : in Natural := 100;
package Bounded_Stack is
   type Stack is private;
   procedure Push (On_Top      : in out Stack;
                   New_Element : in     Element);
   procedure Pop  (From_Top    : in out Stack;
                   Top_Element :    out Element);
   Overflow  : exception;
   Underflow : exception;
   ...
private
   type Stack_Information;
   type Stack is access Stack_Information;
end Bounded_Stack;
------------------------------------------------------------------------

基本原理

[edit | edit source]

ADT 相對於 ADO(或 GADT 相對於 GADO)的最大優勢在於,包的使用者可以使用 ADT 宣告任意數量的物件。這些物件可以宣告為獨立變數,也可以宣告為陣列和記錄的元件。它們還可以作為引數傳遞。所有這些對於 ADO 都是不可能的,在 ADO 中,單個數據物件被封裝在包中。此外,ADO 不會比 ADT 提供更多的資料結構保護。當私有型別由 ADT 包匯出時,如上面的示例所示,那麼對於 ADO 和 ADT,唯一可以修改資料的合法操作是包明確定義的操作(在本例中為 Push 和 Pop)。由於這些原因,ADT 或 GADT 幾乎總是優於 ADO 或 GADO。

GADO 在一個方面類似於 ADT:它允許使用者建立多個物件。使用 ADT,可以使用 ADT 包定義的型別宣告多個物件。使用 GADO(即使是沒有任何泛型形式引數的 GADO,如第三個示例所示),可以多次例項化包以生成多個物件。但是,相似之處到此為止。例項化生成的多個物件受到上面針對 ADO 描述的所有限制;它們不能用於陣列或記錄中,也不能作為引數傳遞。此外,每個物件都是不同型別,並且沒有定義對多個物件同時執行的操作。例如,不能執行比較兩個物件的運算或將一個物件分配給另一個物件。使用 ADT 包定義的型別宣告的多個物件不受任何此類限制;它們可以用於陣列和記錄中,也可以作為引數傳遞。此外,它們都被宣告為同一型別,因此 ADT 包可以提供操作來分配、比較、複製等。由於這些原因,ADT 幾乎總是優於無引數 GADO。

GADT 或 GADO 相對於 ADT 或 ADO 的最大優勢在於,GADT 和 GADO 是泛型的,因此可以使用型別、子程式和其他配置資訊對其進行引數化。因此,如上所示,一個泛型包可以支援任何資料型別和任何堆疊大小的有限堆疊,而上面的 ADT 和 ADO 則限於 Integer 堆疊,最多 100 個。出於這個原因,GADO 或 GADT 幾乎總是優於 ADO 或 ADT。

上面的示例列表按功能和靈活性的順序排列,從 ADO 開始,以 GADT 結束。這些優勢在複雜度或開發時間方面並不昂貴。上面 GADT 的規範與 ADO 的規範在編寫或理解方面並沒有顯著差異。主體也幾乎相同。

比較最簡單版本 ADO 的主體

package body Bounded_Stack is
   type Stack_Slots is array (Natural range <>) of Element;
   type Stack_Information is
      record
         Slots : Stack_Slots (1 .. Maximum_Stack_Size);
         Index : Natural := 0;
      end record;
   Stack : Stack_Information;
   ---------------------------------------------------------------------
   procedure Push (New_Element : in     Element) is
   begin
      if Stack.Index >= Maximum_Stack_Size then
         raise Overflow;
      end if;
      Stack.Index := Stack.Index + 1;
      Stack.Slots(Stack.Index) := New_Element;
   end Push;
   ---------------------------------------------------------------------
   procedure Pop (Top_Element :    out Element) is
   begin
      if Stack.Index <= 0 then
         raise Underflow;
      end if;
      Top_Element := Stack.Slots(Stack.Index);
      Stack.Index := Stack.Index - 1;
   end Pop;
   ---------------------------------------------------------------------
   ...
end Bounded_Stack;

與功能最強大、最靈活的版本 GADT 的主體

package body Bounded_Stack is
   type Stack_Slots is array (Natural range <>) of Element;
   type Stack_Information is
      record
         Slots : Stack_Slots (1 .. Maximum_Stack_Size);
         Index : Natural := 0;
      end record;
   ---------------------------------------------------------------------
   procedure Push (On_Top      : in out Stack;
                   New_Element : in     Element) is
   begin
      if On_Top.Index >= Maximum_Stack_Size then
         raise Overflow;
      end if;
      On_Top.Index := On_Top.Index + 1;
      On_Top.Slots(On_Top.Index) := New_Element;
   end Push;
   ---------------------------------------------------------------------
   procedure Pop (From_Top    : in out Stack;
                  Top_Element :    out Element) is
   begin
      if From_Top.Index <= 0 then
         raise Underflow;
      end if;
      Top_Element := From_Top.Slots(From_Top.Index);

      From_Top.Index := From_Top.Index - 1;
   end Pop;
   ---------------------------------------------------------------------
   ...
end Bounded_Stack;

只有一點不同。ADO 聲明瞭一個名為 Stack 的區域性物件,而 GADT 在每個匯出的過程 Push 和 Pop 上都有一個額外的引數(名為 Stack)。

迭代器

[edit | edit source]
  • 為可重用部分中複雜資料結構的遍歷提供迭代器。
  • 考慮提供主動和被動迭代器。
  • 保護迭代器免受迭代期間資料結構修改導致的錯誤。
  • 記錄迭代器在遍歷期間資料結構被修改時的行為。

Ada 提供了多種機制來構建可重用迭代器。以下示例討論了“簡單”泛型、訪問辨別式和型別擴充套件的替代方案。術語主動和被動用於區分迭代機制(即遍歷複雜資料結構的方式)是公開還是隱藏。被動迭代器隱藏了遍歷(例如迴圈機制),並且包含單個操作 iterate,該操作以您對資料結構中每個元素執行的處理進行引數化。相反,主動迭代器公開了用於遍歷資料結構的基本操作(Booch 1987)。

第一個示例展示了一個泛型包,它定義了一個抽象列表資料型別,以及用於遍歷列表的主動和被動迭代器。

------------------------------------------------------------------------
generic
   type Element is limited private;
   ...
package Unbounded_List is
   type List is limited private;
   procedure Insert (New_Element : in     Element;
                     Into        : in out List);
   -- Passive (generic) iterator.
   generic
      with procedure Process (Each : in out Element);
   procedure Iterate (Over : in     List);
   -- Active iterator
   type Iterator is limited private;

   procedure Initialize (Index         : in out Iterator;
                         Existing_List : in     List);

   function  More       (Index         : in     Iterator)
     return Boolean;

   -- The procedure Get_Next combines an "Advance" and "Current" function
   procedure Get_Next   (Index           : in out Iterator;
                         Current_Element :    out Element);
   ...
private
   ...
end Unbounded_List;
------------------------------------------------------------------------

在例項化泛型包並宣告一個列表為

------------------------------------------------------------------------
with Unbounded_List;
procedure List_User is
   type Employee is ...;
   package Roster is
      new Unbounded_List (Element => Employee, ...);
   Employee_List : Roster.List;

被動迭代器被例項化,指定了在呼叫迭代器時應為每個列表元素呼叫的例程的名稱。

   ---------------------------------------------------------------------
   procedure Process_Employee (Each : in out Employee) is
   begin
      ...
      -- Perform the required action for EMPLOYEE here.
   end Process_Employee;
   ---------------------------------------------------------------------
   procedure Process_All is
      new Roster.Iterate (Process => Process_Employee);

然後可以將被動迭代器呼叫為

begin  -- List_User
   Process_All (Employee_List);
end List_User;
------------------------------------------------------------------------

或者,可以使用主動迭代器,而無需被動迭代器所需的第二個例項化

   Iterator         : Roster.Iterator;
   Current_Employee : Employee;
   procedure Process_Employee (Each : in     Employee) is separate;
begin  -- List_User
   Roster.Initialize (Index         => Iterator,
                      Existing_List => Employee_List);

   while Roster.More (Iterator) loop

      Roster.Get_Next (Index           => Iterator,
                       Current_Element => Current_Employee);

      Process_Employee (Current_Employee);

   end loop;
end List_User;
------------------------------------------------------------------------

第二個示例展示了《Rationale》(1995,§3.7.1)中關於如何使用訪問辨別式構建迭代器的程式碼片段。

generic
   type Element is private;
package Sets is
   type Set is limited private;
   ... -- various set operations
   type Iterator (S : access Set) is limited private;
   procedure Start (I : Iterator);
   function Done (I : Iterator) return Boolean;
   procedure Next (I : in out Iterator);
   ...  -- other iterator operations
private
   type Node;
   type Ptr is access Node;
   type Node is
      record
         E    : Element;
         Next : Ptr;
      end record;
   type Set is new Ptr;
   type Iterator (S : access Set) is
      record
         This : Ptr;
      end record;
end Sets;
package body Sets is
   ...  -- bodies of the various set operations
   procedure Start (I : in out Iterator) is
   begin
      I.This := Ptr(I.S.all);
   end Start;
   function Done (I : Iterator) return Boolean is
   begin
      return I.This = null;
   end Done;
   procedure Next (I : in out Iterator) is
   begin
      I.This := I.This.Next;
   end Next;
   ...
end Sets;

迭代器操作允許您迭代集合的元素,其中迭代器物件的元件 This 訪問當前元素。訪問辨別式始終指向當前元素所屬的封閉集合。

第三個示例使用《Rationale》(1995,§4.4.4)中的程式碼片段來展示使用型別擴充套件和分派的迭代器。

type Element is ...
package Sets is
   type Set is limited private;
   -- various set operations
   type Iterator is abstract tagged null record;
   procedure Iterate (S : in Set; IC : in out Iterator'Class);
   procedure Action (E : in out Element;
                     I : in out Iterator) is abstract;
private
   -- definition of Node, Ptr (to Node), and Set
end Sets;
package body Sets is
   ...
   procedure Iterate (S : in Set; IC : in out Iterator'Class) is
      This : Ptr := Ptr (S);
   begin
      while This /= null loop
         Action (This.E, IC);  -- dispatch
         This := This.Next;
      end loop;
   end Iterate;
end Sets;

通用迭代器如下所示

package Sets.Something is
   procedure Do_Something (S : Set; P : Parameters);
end Sets.Something;
package body Sets.Something is
   type My_Iterator is new Iterator with
      record
         -- components for parameters and workspace
      end record;
   procedure Action (E : in out Element;
                     I : in out My_Iterator) is
   begin
      -- do something to element E using data from iterator I
   end Action;
   procedure Do_Something (S : Set; P : Parameters) is
      I : My_Iterator;
   begin  -- Do_Something
      ...  -- copy parameters into iterator
      Iterate (S, I);
      ... copy any results from iterator back to parameters
   end Do_Something;

end Sets.Something;

基本原理

[編輯 | 編輯原始碼]

經常需要遍歷複雜資料結構,如果部分本身沒有提供,則在不違反資訊隱藏原則的情況下很難實現。

主動和被動迭代器各有優缺點,但並非在所有情況下都適用。因此,建議提供這兩種型別的迭代器,讓使用者可以選擇在每種情況下使用哪種迭代器。

被動迭代器比主動迭代器更簡單,錯誤率也更低,就像 for 迴圈比 while 迴圈更簡單,錯誤率也更低。使用者使用被動迭代器時會犯更少的錯誤。只需使用要為每個列表元素執行的例程對其進行例項化,然後為所需的列表呼叫例項化。主動迭代器要求使用者更加謹慎。必須謹慎地按正確順序呼叫迭代器操作,並將正確的迭代器變數與正確的列表變數相關聯。在維護過程中對軟體進行的更改可能會引入錯誤,例如無限迴圈。

另一方面,主動迭代器比被動迭代器更靈活。使用被動迭代器,很難執行多個併發同步迭代。例如,使用主動迭代器遍歷兩個排序列表並將它們合併到第三個排序列表中要容易得多。此外,對於多維資料結構,少量主動迭代器例程可以替換大量被動迭代器,每個被動迭代器實現主動迭代器的一種組合。最後,主動迭代器可以作為泛型形式引數傳遞,而被動迭代器則不能,因為被動迭代器本身就是泛型的,泛型單元不能作為引數傳遞給其他泛型單元。

對於任何型別的迭代器,關於在迭代資料結構時發生什麼的語義問題都會出現。在編寫迭代器時,一定要考慮這種可能性,並在註釋中指明在這種情況下發生的行為。對於使用者來說,這並不總是顯而易見。例如,為了確定某個操作相對於某個數學“集合”的“閉包”,一個常見的演算法是對集合的成員進行迭代,生成新元素並將它們新增到集合中。在這種情況下,重要的是在迭代過程中新增到集合中的元素在隨後的迭代過程中被遇到。另一方面,對於其他演算法,重要的是迭代的集合與迭代開始時存在的集合相同。在優先順序列表資料結構的情況下,如果以優先順序順序迭代列表,則重要的是在迭代過程中插入的優先順序低於當前元素的元素在隨後的迭代過程中不會被遇到,但優先順序更高的元素應該被遇到。在任何情況下,都要有意識地決定迭代器應該如何執行,並在包規範中記錄該行為。

從資料結構中刪除元素也會給迭代器帶來問題。使用者經常會犯的一個錯誤是迭代一個數據結構,並在迭代期間逐個刪除它。如果迭代器沒有為這種情況做好準備,則可能最終會取消引用空指標或犯類似的錯誤。可以透過在每個資料結構中儲存額外資訊來防止這種情況,這些資訊指示它是否當前正在被迭代,並使用此資訊來禁止在迭代期間對資料結構進行任何修改。當資料結構被宣告為有限私有型別時,就像在涉及迭代器時通常應該做的那樣,對該型別定義的唯一操作是在宣告該型別的包中明確宣告,這使得可以將這些測試新增到所有修改操作中。

《Rationale》(1995,§4.4.4)指出,訪問辨別式和型別擴充套件技術是彼此的逆運算。在訪問辨別式方法中,您必須為每個操作編寫出迴圈機制。在型別擴充套件方法中,您編寫一個迴圈並分派到所需的動作。因此,使用訪問辨別式技術的迭代器將被視為主動的,而使用型別擴充套件技術的迭代器將被視為被動的。

您可以使用對子程式型別的訪問作為泛型例項化的替代方法,使用非泛型引數作為指向子程式的指標。然後,您將引用子程式應用於集合中的每個元素(《Rationale》1995,§3.7.2)。但是,這種方法有一些缺點,因為您不能使用它來建立通用迭代器。匿名訪問子程式引數在 Ada 中不允許;因此,以下片段是非法的

procedure Iterate (C      : Collection;
                   Action : access procedure (E : in out Element));

形式引數 Action 必須是命名訪問子型別的,例如

type Action_Type is access procedure (E : in out Element);
procedure Iterate (C      : Collection;
                   Action : Action_Type);

為了使此方法有效,您必須確保操作子程式在作用域內,並且未在另一個子程式內部定義。如果它是作為巢狀過程定義的,則訪問它將是非法的。請參閱《Rationale》(1995,§4.4.4)以獲取更完整的示例。

有關被動和主動迭代器的進一步討論,請參閱《Rationale》(1995,§3.7.1 和 §4.4.4)、Ross(1989)和 Booch(1987)。

十進位制型別輸出和資訊系統附錄

[編輯 | 編輯原始碼]
  • 在圖片輸出中本地化貨幣符號、數字分隔符、基數標記和填充字元。
  • 考慮在圖片佈局中使用 # 字元,以便編輯後的數字輸出長度在不同長度的貨幣符號之間保持不變。
with Ada.Text_IO.Editing;
package Currency is

   type Dollars is delta 0.01 digits 10;
   type Marks   is delta 0.01 digits 10;

   package Dollar_Output is
      new Ada.Text_IO.Editing.Decimal_Output
             (Num                => Dollars,
              Default_Currency   => "$",
              Default_Fill       => '*',
              Default_Separator  => ',',
              Default_Radix_Mark => '.');

   package Mark_Output is
      new Ada.Text_IO.Editing.Decimal_Output
             (Num                => Marks,
              Default_Currency   => "DM",
              Default_Fill       => '*',
              Default_Separator  => '.',
              Default_Radix_Mark => ',');

end Currency;
with Ada.Text_IO.Editing;
with Currency;  use Currency;
procedure Test_Picture_Editing is

   DM_Amount     : Marks;
   Dollar_Amount : Dollars;

   Amount_Picture : constant Ada.Text_IO.Editing.Picture 
      := Ada.Text_IO.Editing.To_Picture ("##ZZ_ZZZ_ZZ9.99");

begin   -- Test_Picture_Editing

   DM_Amount     := 1_234_567.89;
   Dollar_Amount := 1_234_567.89;

   DM_Output.Put (Item => DM_Amount,
                  Pic  => Amount_Picture);

   Dollar_Output.Put (Item => Dollar_Amount,
                      Pic  => Amount_Picture);
   
end Test_Picture_Editing;

基本原理

[編輯 | 編輯原始碼]

貨幣在報表中的顯示方式有所不同。貨幣使用不同長度的不同符號(例如,美國 $、德國 DM 和奧地利 ÖS)。它們使用不同的符號來分隔數字。美國和英國使用逗號來分隔千位數,而歐洲大陸使用句號。美國和英國使用句號作為小數點;歐洲大陸使用逗號。對於涉及跨國使用的財務計算的程式,您需要考慮這些差異。透過封裝它們,您可以限制在調整財務包時更改的影響。

實現 Mixin

[編輯 | 編輯原始碼]
  • 考慮使用抽象標記型別和泛型來定義可重用的功能單元,這些單元可以“混合”到核心抽象中(也稱為 Mixin)。

注意以下模式中使用抽象標記型別作為泛型形式引數以及作為匯出擴充套件型別,該模式摘自《Rationale》(1995,§4.6.2)。

generic
   type S is abstract tagged private;
package P is
   type T is abstract new S with private;
   -- operations on T
private
   type T is abstract new S with
      record
         -- additional components
      end record;
end P;

以下程式碼展示瞭如何例項化泛型以在最終型別擴充套件中“混合”所需的功能。另請參閱準則 9.5.1,瞭解相關的程式碼示例。

-- Assume that packages P1, P2, P3, and P4 are generic packages which take a tagged
-- type as generic formal type parameter and which export a tagged type T
package Q is
   type My_T is new Basic_T with private;
   ... -- exported operations
private
   package Feature_1 is new P1 (Basic_T);
   package Feature_2 is new P2 (Feature_1.T);
   package Feature 3 is new P3 (Feature_2.T);
   package Feature_4 is new P4 (Feature_3.T);
   -- etc.
   type My_T is new Feature_4.T with null record;
end Q;

基本原理

[edit | edit source]

《原理》(1995 年,第 4.6.2 節)討論了使用泛型模板來定義要混合到抽象中的屬性。

泛型模板定義了混入。作為泛型實參提供的型別決定了父類...主體提供了操作,規範匯出了擴充套件型別。

如果你定義了一系列泛型混入包,你就可以對例項化進行序列化。下一個例項化的實際引數是前一個例項化匯出的帶標籤型別。這在示例中的第二個程式碼段中顯示。每個擴充套件都源自前一個擴充套件,因此你有一個線性化的子程式覆蓋序列。由於它們是線性化的,所以你有一個可以用來解決任何衝突的派生順序。

你應該將一個擴充套件(以及相關操作)封裝到每個泛型包中。這提供了更好的關注點分離,以及更可維護、可重用的元件。

有關混入使用的完整討論,請參見指南 9.5.1。

獨立性

[edit | edit source]

可重用部分應該儘可能獨立於其他可重用部分。如果一個潛在的使用者需要使用其他看起來不必要的部件才能重用一個部件,那麼他重用該部件的可能性就會降低。其他部件的“額外負擔”會浪費時間和空間。使用者希望能夠只重用被認為有用的部分。“部分”的概念在這裡故意含糊不清。如果來自該庫的通常被重用的“部分”是整個子系統,那麼單個包不需要獨立於重用庫中的每個其他包。如果整個子系統被認為是提供有用的功能,那麼整個子系統就被重用。但是,子系統不應該與重用庫中的所有其他子系統緊密耦合,以至於難以或不可能在不重用整個庫的情況下重用子系統。可重用部分之間的耦合只有在它為使用者提供了明顯的強大優勢時才會發生。

子系統設計

[edit | edit source]

指南

[edit | edit source]
  • 考慮將子系統結構化,以便僅在特定上下文中使用的操作位於與在不同上下文中使用的操作不同的子包中。
  • 考慮在父包中宣告與上下文無關的功能,在子包中宣告與上下文相關的功能。

基本原理

[edit | edit source]

泛型單元是基本的構建塊。泛型引數化可用於打破程式單元之間的依賴關係,以便它們可以單獨重用。但是,通常情況下,一組單元(尤其是包集)將作為子系統一起重用。在這種情況下,這些包可以被收集到一個子包層次結構中,並使用私有包隱藏內部細節。層次結構可能是泛型的,也可能不是泛型的。使用子包允許在不包含太多無關操作的情況下重用子系統,因為未使用的子包可以在新環境中被丟棄。

另請參見指南 4.1.6 和 8.3.1。

使用泛型引數減少耦合

[edit | edit source]

指南

[edit | edit source]
  • 最小化可重用部件上的 with 子句,尤其是在它們的規範上。
  • 考慮使用泛型引數而不是 with 語句來減少可重用部件上的上下文子句數量。
  • 考慮使用泛型形式包引數直接匯入預先存在的泛型例項中定義的所有型別和操作。

示例

[edit | edit source]

像下面的過程

------------------------------------------------------------------------
with Package_A;
procedure Produce_And_Store_A is
   ...
begin  -- Produce_And_Store_A
   ...
   Package_A.Produce (...);
   ...
   Package_A.Store (...);
   ...
end Produce_And_Store_A;
------------------------------------------------------------------------

可以重寫為一個泛型單元

------------------------------------------------------------------------
generic
   with procedure Produce (...);
   with procedure Store   (...);
procedure Produce_And_Store;
------------------------------------------------------------------------
procedure Produce_And_Store is
   ...
begin  -- Produce_And_Store
   ...
   Produce (...);
   ...
   Store   (...);
   ...
end Produce_And_Store;
------------------------------------------------------------------------

然後例項化

------------------------------------------------------------------------
with Package_A;
with Produce_And_Store;
procedure Produce_And_Store_A is
   new Produce_And_Store (Produce => Package_A.Produce,
                          Store   => Package_A.Store);
------------------------------------------------------------------------

基本原理

[edit | edit source]

上下文(with)子句指定了該單元所依賴的其他單元的名稱。這種依賴關係不能也不應該完全避免,但儘量減少出現在單元規範中的依賴關係數量是個好主意。嘗試將它們移到主體中,使規範獨立於其他單元,以便更容易地單獨理解。此外,以這樣的方式組織你的可重用部分,即單元的主體中不包含大量相互依賴關係。將你的庫劃分為獨立的功能區域,且沒有跨區域的依賴關係,是一個良好的開端。最後,透過使用泛型形式引數而不是 with 語句,如上面的示例所示,來減少依賴關係。如果庫中的單元耦合得太緊密,那麼任何單個部分都無法在不重用大部分或全部庫的情況下被重用。

上面的 Produce_And_Store_A 的第一個(非泛型)版本很難重用,因為它依賴於 Package_A,而 Package_A 可能不是通用的或普遍可用的。如果 Produce_And_Store 操作的重用潛力因這種依賴關係而降低,則應該生成一個泛型單元和一個例項化,如上所示。Package_A 的 with 子句已從 Produce_And_Store 泛型過程(它封裝了可重用演算法)中移到了 Produce_And_Store_A 例項化中。泛型單元不再命名提供所需操作的包,而是簡單地列出所需的操作本身。這增加了泛型單元的獨立性和可重用性。

這種將泛型形式引數用於 with 子句的方法也允許更細粒度的可見性。非泛型版本的 Produce_And_Store_A 上的 with 子句使 Package_A 的所有內容對 Produce_And_Store_A 可見,而泛型版本上的泛型引數僅使 Produce 和 Store 操作對泛型例項化可用。

泛型形式包允許“更安全、更簡單的泛型抽象組合”(原理 1995 年,第 12.6 節)。泛型形式包允許你將一組相關的型別及其操作分組到一個單元中,避免將每個型別和操作作為單獨的泛型形式引數列出。這種技術允許你清楚地表明你正在用另一個泛型擴充套件一個泛型的功能,實際上是用另一個抽象引數化一個抽象。

由於編譯指示造成的耦合

[edit | edit source]

指南

[edit | edit source]
  • 在泛型庫單元的規範中,使用 pragma Elaborate_Body。

示例

[edit | edit source]
---------------------------------------------------------------------------
generic
   ...
package Stack is

   pragma Elaborate_Body (Stack); -- in case the body is not yet elaborated

   ...
end Stack;
---------------------------------------------------------------------------
with Stack;
package My_Stack is
   new Stack (...);
---------------------------------------------------------------------------
package body Stack is
begin
   ...
end Stack;
---------------------------------------------------------------------------

基本原理

[edit | edit source]

編譯單元的細化順序僅受限於編譯順序。此外,無論何時你有一個作為庫單元的例項化或一個在庫包中的例項化,Ada 都要求你在細化例項化本身之前細化正在被例項化的泛型的主體。由於泛型庫單元主體可能在泛型的例項化之後編譯,因此該主體可能不會在例項化時被細化,從而導致 Program_Error。使用 pragma Elaborate_Body 透過要求泛型單元主體在規範之後立即細化來避免這種情況,無論編譯順序如何。

當存在明確的遞迴依賴關係要求時,應使用 pragma Elaborate_Body。這種情況出現在例如,當存在遞迴依賴關係(例如,包 A 的主體依賴於包 B 的規範,而包 B 的主體依賴於包 A 的規範)時。

Pragma Elaborate_All 控制一個單元相對於另一個單元的細化順序。這是另一種耦合單元的方式,在可重用部分中應儘可能避免,因為它限制了可重用部分可以組合的配置數量。但是請認識到,pragma Elaborate_All 提供了更好的細化順序保證,因為如果使用此 pragma 發現細化問題,這些問題將在連結時報告(而不是在執行時執行錯誤)。

在庫單元的細化過程中呼叫子程式(通常是函式)時,子程式的主體必須在庫單元之前細化。可以透過為包含函式的單元新增 pragma Elaborate_Body 來確保此細化發生。但是,如果該函式呼叫其他函式,那麼在包含該函式的單元上放置 pragma Elaborate_All 則更安全。

有關 pragma Pure 和 Preelaborate 的討論,另請參見 Ada 參考手冊(1995,§10.2.1 [註釋]) 和理由(1995,§10.3)。如果使用 pragma Pure 或 Preelaborate,則不需要 pragma Elaborate_Body。

註冊的概念是許多面向物件程式設計框架的基礎。由於其他庫單元需要在細化過程中呼叫它,因此需要確保註冊本身儘早細化。請注意,登錄檔只應依賴於型別層次結構的根型別,並且登錄檔只應儲存指向物件的“類級”指標,而不是更具體的指標。根型別本身不應依賴於登錄檔。有關面向物件特性的更完整討論,請參見第 9 章。

部分族

[編輯 | 編輯原始碼]
  • 建立具有類似規範的泛型或其他部分的族。

Booch 部分(Booch 1987)是應用此指南的示例。

基本原理

[編輯 | 編輯原始碼]

對於不同的應用程式,或者為了改變給定應用程式的屬性,可能需要類似部分的不同版本(例如,有界棧與無界棧)。通常,這些版本所需的不同的行為無法使用泛型引數獲得。提供具有類似規範的部分族使程式設計師可以輕鬆地為當前應用程式選擇合適的版本,或者在應用程式需求發生變化時替換其他版本。

透過替換族成員,由作為部分族成員的子部分構建的可重用部分特別容易調整以適應給定應用程式的需要。

指南 9.2.4 討論了在構建類似部分(即通用介面,多個實現)的不同版本時使用標記型別的用法。

條件編譯

[編輯 | 編輯原始碼]
  • 構建可重用程式碼以利用編譯器進行死程式碼移除。
------------------------------------------------------------------------
package Matrix_Math is
   ...
   type Algorithm is (Gaussian, Pivoting, Choleski, Tri_Diagonal);
   generic
      Which_Algorithm : in     Algorithm := Gaussian;
   procedure Invert ( ... );
end Matrix_Math;
------------------------------------------------------------------------
package body Matrix_Math is
   ...
   ---------------------------------------------------------------------
   procedure Invert ( ... ) is
      ...
   begin  -- Invert
      case Which_Algorithm is
         when Gaussian     => ... ;
         when Pivoting     => ... ;
         when Choleski     => ... ;
         when Tri_Diagonal => ... ;
      end case;
   end Invert;
   ---------------------------------------------------------------------
end Matrix_Math;
------------------------------------------------------------------------

基本原理

[編輯 | 編輯原始碼]

某些編譯器會省略與他們檢測到的永遠不會執行的程式部分相對應的目的碼。條件語句中的常量表達式利用了此功能(如果可用),提供了有限形式的條件編譯。當在不支援此形式的條件編譯的實現中重用部分時,這種做法會產生一個乾淨的結構,透過刪除或註釋掉冗餘程式碼(如果它會產生不可接受的開銷)可以輕鬆地進行調整。

當其他因素阻止程式碼被分成單獨的程式單元時,應該使用此功能。在上面的示例中,最好為每種演算法提供不同的過程。但這些演算法在細微但複雜的方式上有所不同,使得單獨的過程難以維護。

請注意您的實現是否支援死程式碼移除,並準備好採取其他措施來消除冗餘程式碼的開銷(如果有必要)。

表驅動程式設計

[編輯 | 編輯原始碼]
  • 只要可能且適當,編寫表驅動的可重用部分。

表驅動的可重用軟體的典型代表是解析器生成系統。輸入資料形式及其輸出的規範,以及一些專用程式碼,被轉換為表格,這些表格將被使用預定演算法的預先存在的程式碼使用,這些演算法在生成的解析器中使用。其他形式的“應用程式生成器”的工作原理類似。

基本原理

[編輯 | 編輯原始碼]

表驅動(有時稱為資料驅動)程式的行為依賴於在編譯時 with 的資料或在執行時從檔案讀取的資料。在適當的情況下,表驅動程式設計提供了一種非常強大的方法來建立通用的、易於定製的、可重用部分。

有關在實現表驅動程式時使用訪問子程式型別的簡短討論,請參見指南 5.3.4。

考慮通用部分的行為差異是否可以透過在編譯時或執行時定義的某些資料結構來定義,如果是,則構建該部分使其成為表驅動的。當設計部分用於特定應用程式領域但需要針對該領域中的特定應用程式進行專門化時,這種方法最有可能適用。在評論驅動部分所需資料的結構時要特別注意。

表驅動程式通常比相應的 case 或 if-elsif-else 網路更有效,並且更容易閱讀,以計算要查詢或查詢的項。

字串處理

[編輯 | 編輯原始碼]
  • 使用預定義的包進行字串處理。

在 Ada 95 中,不再需要編寫以下程式碼

function Upper_Case (S : String) return String is

   subtype Lower_Case_Range is Character range 'a'..'z';

   Temp : String := S;
   Offset : constant := Character'Pos('A') - Character'Pos('a');

begin
   for Index in Temp'Range loop
      if Temp(Index) in Lower_Case_Range then
         Temp(Index) := Character'Val (Character'Pos(Temp(Index)) + Offset);
      end if;
   end loop;
   return Temp;
end Upper_Case;

with Ada.Characters.Latin_1;
function Trim (S : String) return String is
   Left_Index  : Positive := S'First;
   Right_Index : Positive := S'Last;
   Space : constant Character := Ada.Characters.Latin_1.Space;
begin
   while (Left_Index < S'Last) and then (S(Left_Index) = Space) loop
      Left_Index := Positive'Succ(Left_Index);
   end loop;

   while (Right_Index > S'First) and then (S(Right_Index) = Space) loop
      Right_Index := Positive'Pred(Right_Index);
   end loop;

   return S(Left_Index..Right_Index);
end Trim;

假設變數 S 的型別為 String,以下表達式

Upper_Case(Trim(S))

現在可以替換為更便攜且預先存在的語言定義的操作,例如

with Ada.Characters.Handling;  use Ada.Characters.Handling;
with Ada.Strings;              use Ada.Strings;
with Ada.Strings.Fixed;        use Ada.Strings.Fixed;

...
To_Upper (Trim (Source => S, Side => Both))

基本原理

[編輯 | 編輯原始碼]

預定義的 Ada 語言環境包含字串處理包,以鼓勵可移植性。它們支援不同類別的字串:固定長度、有界長度和無界長度。它們還支援用於字串構造、連線、複製、選擇、排序、搜尋、模式匹配和字串轉換的子程式。您不再需要定義自己的字串處理包。

帶標記的型別層次結構

[edit | edit source]

指南

[edit | edit source]
  • 考慮使用帶標記的型別層次結構來促進軟體的泛化,以便重複使用。
  • 考慮使用帶標記的型別層次結構將泛化演算法與對特定型別依賴關係的細節分離。

示例

[edit | edit source]
with Wage_Info;
package Personnel is
   type Employee is abstract tagged limited private;
   type Employee_Ptr is access all Employee'Class;
   ...
   procedure Compute_Wage (E : Employee) is abstract;
private
   type Employee is tagged limited record
      Name  : ...;
      SSN   : ... ;
      Rates : Wage_Info.Tax_Info;
      ...
   end record;
end Personnel;
package Personnel.Part_Time is
   type Part_Timer is new Employee with private;
   ...
   procedure Compute_Wage (E : Part_Timer);
private
   ...
end Personnel.Part_Time;
package Personnel.Full_Time is
   type Full_Timer is new Employee with private;
   ...
   procedure Compute_Wage (E : Full_Timer);
private
   ...
end Personnel.Full_Time;

給定以下陣列宣告

type Employee_List is array (Positive range <>) of Personnel.Employee_Ptr;

您可以編寫一個計算每個員工工資的程式,無論您建立了多少種不同的員工型別。Employee_List 包含一個指向各種型別員工的指標陣列,每個員工都有一個單獨的 Compute_Wage 過程。(原始 Compute_Wage 被宣告為一個抽象過程,因此必須由所有後代覆蓋。)您無需修改工資單程式碼,因為您專門化了員工型別

procedure Compute_Payroll (Who : Employee_List) is
begin -- Compute_Payroll
   for E in Who'Range loop
      Compute_Wage (Who(E).all);
   end loop;
end Compute_Payroll;

基本原理

[edit | edit source]

一般演算法可以多型地依賴於根帶標記型別的類寬型別物件,而無需關心從根型別派生的特定型別。如果在型別層次結構中添加了其他型別,則無需更改泛化演算法。另請參見準則 5.4.2。此外,子包層次結構將映象繼承層次結構。

一個通用的根帶標記型別可以定義層次結構中更特定型別的共同屬性並具有共同的操作。僅依賴於此根型別的軟體將是通用的,因為它可以與任何更特定型別的物件一起使用。此外,根型別客戶機的一般演算法不必隨著在型別層次結構中新增更多特定型別而更改。這是一種組織面向物件軟體以進行重複使用的特別有效的方法。

將派生帶標記型別層次結構分離到各個包中,透過減少包介面中的專案數量來增強可重用性。它還允許您只使用所需的 capabilities。

另請參見準則 9.2、9.3.1、9.3.5 和 9.4.1。

摘要

[edit | edit source]

理解和清晰

[edit | edit source]
  • 為可重用部分及其識別符號選擇限制最少的名稱。
  • 選擇通用名稱以避免與通用例項的命名約定衝突。
  • 使用表示可重用部分行為特徵及其抽象的名稱。
  • 在識別符號或單元名稱中不要使用縮寫。
  • 像文件化任何包規範一樣,文件化泛型形式引數的預期行為。

健壯性

[edit | edit source]
  • 使用命名的數字和靜態表示式,可以讓多個依賴關係連結到少量符號。
  • 對於陣列形式引數和陣列返回值,使用無約束陣列型別。
  • 在適當的情況下,使區域性變數的大小取決於實際引數的大小。
  • 最小化單元做出的假設數量。
  • 對於無法避免的假設,使用子型別或約束來自動強制一致性。
  • 對於無法透過子型別自動強制執行的假設,請在程式碼中新增顯式檢查。
  • 記錄所有假設。
  • 如果程式碼依賴於特定特殊需求附件的實現才能正常執行,請在程式碼中記錄此假設。
  • 在宣告模式為 in out 的泛型形式物件時,使用第一個子型別。
  • 注意在宣告泛型形式子程式的引數或返回值時使用子型別作為子型別標記。
  • 使用屬性而不是字面量。
  • 謹慎對待過載同一個泛型包匯出的子程式的名稱。
  • 在規範內,記錄透過使用規範和使用規範的任何部分而啟用的任何任務。
  • 記錄從泛型單元中隱藏的任務中訪問哪些泛型形式引數。
  • 記錄任何多執行緒元件。
  • 將異常從可重用部分傳播出去。僅當您確定處理在所有情況下都合適時,才在可重用部分內處理異常。
  • 在執行泛型形式子程式正確操作的任何必要清理後,傳播泛型形式子程式引發的異常。
  • 在引發異常時,將狀態變數置於有效狀態。
  • 在引發異常時,將引數保持不變。

適應性

[edit | edit source]
  • 在可重用部分或一組部分中提供核心功能,以便可以在此抽象級別中對其進行有意義的擴充套件,使其能夠被重複使用。
  • 更具體地說,為每個可能包含動態資料的的資料結構提供初始化和終結過程。
  • 對於需要初始化和終結的資料結構,請考慮儘可能從型別 Ada.Finalization.Controlled 或 Ada.Finalization.Limited_Controlled 派生它們。
  • 使用泛型單元避免程式碼重複。
  • 為泛型單元引數化以實現最大適應性。
  • 重用泛型單元的常見例項,以及泛型單元本身。
  • 當您在泛型體內部不需要對該型別物件的賦值時,請考慮使用受限私有型別作為泛型形式型別。
  • 當您在泛型體內部需要對該型別物件的正常賦值時,請考慮使用非受限私有型別作為泛型形式型別。
  • 當您需要在泛型體內部對該型別物件強制執行特殊賦值語義時,請考慮使用從 Ada.Finalization.Controlled 派生的形式標記型別。
  • 匯出限制最少的型別,以維護資料和抽象的完整性,同時允許替代實現。
  • 請考慮使用受限私有抽象型別作為擴充套件形式私有標記型別的泛型的泛型形式型別。
  • 使用泛型單元封裝獨立於資料型別的演算法。
  • 考慮使用抽象資料型別(不要與 Ada 的抽象型別混淆)而不是抽象資料物件。
  • 考慮使用泛型單元來實現獨立於其元件資料型別的抽象資料型別。
  • 為可重用部分中複雜資料結構的遍歷提供迭代器。
  • 考慮提供主動和被動迭代器。
  • 保護迭代器免受迭代期間資料結構修改導致的錯誤。
  • 記錄迭代器在遍歷期間資料結構被修改時的行為。
  • 在圖片輸出中本地化貨幣符號、數字分隔符、基數標記和填充字元。
  • 考慮在圖片佈局中使用 # 字元,以便編輯後的數字輸出長度在不同長度的貨幣符號之間保持不變。
  • 考慮使用抽象標記型別和泛型來定義可重用的功能單元,這些單元可以“混合”到核心抽象中(也稱為 Mixin)。
  • 考慮將子系統結構化,以便僅在特定上下文中使用的操作位於與在不同上下文中使用的操作不同的子包中。
  • 考慮在父包中宣告與上下文無關的功能,在子包中宣告與上下文相關的功能。

獨立性

[edit | edit source]
  • 最小化可重用部件上的 with 子句,尤其是在它們的規範上。
  • 考慮使用泛型引數而不是 with 語句來減少可重用部件上的上下文子句數量。
  • 考慮使用泛型形式包引數直接匯入預先存在的泛型例項中定義的所有型別和操作。
  • 在泛型庫單元的規範中,使用 pragma Elaborate_Body。
  • 建立具有類似規範的泛型或其他部分的族。
  • 構建可重用程式碼以利用編譯器進行死程式碼移除。
  • 只要可能且適當,編寫表驅動的可重用部分。
  • 使用預定義的包進行字串處理。
  • 考慮使用帶標記的型別層次結構來促進軟體的泛化,以便重複使用。
  • 考慮使用帶標記的型別層次結構將泛化演算法與對特定型別依賴關係的細節分離。

面向物件特性

華夏公益教科書