第3章:抽象資料型別(ADT)和麵向物件程式設計(OOP) 3.2設計規約

啥也博士發表於2019-01-19

大綱

1.程式語言中的功能/方法
2.規約:便於交流的程式設計,為什麼需要規約
行為等同規約結構:前提條件和後條件測試和驗證規約
3.設計規約分類規約圖表規約質量規約
4.總結

程式語言的功能和方法

方法:構建模組
大型專案由小型方法構建
方法可以單獨開發,測試和重複使用
方法的使用者不需要知道它是如何工作的 – 這被稱為“抽象”

注意:呼叫方法時引數型別不匹配 – 靜態檢查
返回值型別是否匹配,也在靜態型別檢查階段完成

規約:便於交流的程式設計

(1)程式設計中的文件

Java API文件:一個例子
類層次結構和實現的介面列表。
直接子類,併為介面實現類。
類的描述
構建思想
方法摘要列出了我們可以呼叫的所有方法
每個方法和建構函式的詳細描述

  • 方法簽名:我們看到返回型別,方法名稱和引數。 我們也看到例外。 目前,這些通常意味著該方法可能遇到的錯誤。
  • 完整的描述。
  • 引數:方法引數的描述。
  • 以及該方法返回的描述。

記錄假設

向下寫入變數的型別記錄了一個關於它的假設:例如,此變數將始終引用一個整數。

  • Java實際上在編譯時檢查了這個假設,並保證在你的程式中沒有地方違反了這個假設。

宣告一個變數final也是一種形式的文件,宣告該變數在初始賦值後永遠不會改變。

  • Java也會靜態地檢查它。

如何函式/方法的假設?

便於交流的程式設計

為什麼我們需要寫下我們的假設?

  • 因為程式設計充滿了它們,如果我們不寫下它們,我們將不會記住它們,而其他需要閱讀或更改我們程式的人將不會知道它們。 他們必須猜測。

程式必須記住兩個目標:

  • 與電腦交流。 首先說服編譯器你的程式是合理的 – 語法正確和型別正確。 然後讓邏輯正確,以便在執行時提供正確的結果。
  • 與其他人溝通。 使程式易於理解,以便在有人修復,改進或未來適應時,他們可以這樣做。

黑客與工程

黑客往往以肆無忌憚的樂觀為標誌

  • 不好:在測試任何程式碼之前先寫很多程式碼
  • 不好:把所有的細節都留在腦海中,假設你會永遠記住它們,而不是寫在你的程式碼中
  • 不好:假設錯誤不存在或者很容易找到並修復

但軟體工程不是黑客行為。 工程師是悲觀主義者:

  • 好:一次寫一點,隨時測試(第7章中的測試優先程式設計)。
  • 好:記錄你的程式碼依賴的假設
  • 好:捍衛你的程式碼免受愚蠢 – 尤其是你自己的!

靜態檢查有助於此

(2)規約和契約(方法)

規約(或稱為契約)
規約是團隊合作的關鍵。 沒有規約就不可能委託實施方法的責任。
規約作為一種契約:實施者負責滿足契約,而使用該方法的客戶可以依賴契約。

  • 說明方法和呼叫者的責任
  • 定義實現的正確含義

規約對雙方都有要求:當規約有先決條件時,客戶也有責任。

  • 如果你在這個時間表上支付了這筆款項……
  • 我將用下面的詳細規約來構建一個
  • 有些契約有不履行的補救措施

為什麼規約?

現實:

  • 程式中最常見的錯誤是由於對兩段程式碼之間的介面行為的誤解而產生的。
  • 儘管每個程式設計師都有規約說明,但並不是所有的程式設計師都把它們寫下來。 因此,團隊中的不同程式設計師有不同的規約。
  • 程式失敗時,很難確定錯誤的位置。

優點:

  • 程式碼中的精確規約讓您分攤程式碼片段的責備,並且可以免除您在修復應該去的地方令人費解的痛苦。
  • 規約對於一個方法的客戶來說是很好的,因為他們不需要閱讀程式碼。

規約對於方法的實現者來說是很好的,因為他們給了實現者自由地改變實現而不告訴客戶。
規約也可以使碼程式碼更快。
契約充當客戶和實施者之間的防火牆。

  • 它保護客戶免受單位工作細節的影響。
  • 它將執行器從單元使用的細節中遮蔽掉。
  • 這種防火牆會導致解耦,允許單元的程式碼和客戶端的程式碼獨立更改,只要這些更改符合規約。
  • 解耦,不需要了解具體實現

物件與其使用者之間的協議

  • 方法簽名(型號規約)
  • 功能和正確性預期
  • 效能預期效能

該方法做了什麼,而不是如何做

  • 介面(API),不是實現

(3)行為等價性

要確定行為等同性,問題是我們是否可以用另一個實現替代另一個實現
等價的概念在客戶眼中。

為了使一個實現替代另一個實現成為可能,並且知道何時可以接受,我們需要一個規約來說明客戶端依賴於什麼
注意:規約不應該談論方法類的區域性變數或方法類的私有欄位。

(4)規約結構:前提條件和後置條件

一個方法的規約由幾個子句組成:

  • 先決條件,由關鍵字require表示
  • 後置條件,由關鍵字效果表示
  • 特殊行為:如果違反先決條件,會發生什麼?

先決條件是客戶(即方法的呼叫者)的義務。 這是呼叫方法的狀態。
後置條件是該方法實施者的義務。
如果前提條件適用於呼叫狀態,則該方法必須遵守後置條件,方法是返回適當的值,丟擲指定的異常,修改或不修改物件等等。

整體結構是一個合乎邏輯的含義:如果在呼叫方法時前提條件成立,則在方法完成時必須保持後置條件。
如果在呼叫方法時前提條件不成立,則實現不受後置條件的限制。

  • 可以自由地做任何事情,包括不終止,丟擲異常,返回任意結果,進行任意修改等。

Java中的規約

Java的靜態型別宣告實際上是方法的前提條件和後置條件的一部分,該方法是編譯器自動檢查和執行的一部分。
靜態檢查
契約的其餘部分必須在該方法之前的評論中進行描述,並且通常取決於人類對其進行檢查並予以保證。
引數由@param子句描述,結果由@return和@throws子句描述。
將前提條件放在@param中,並將後置條件放入@return和@throws。

可變方法的規約

如果效應沒有明確說明輸入可以被突變,那麼我們假設輸入的突變是隱式地被禁止的。
幾乎所有的程式設計師都會承擔同樣的事情。 驚喜突變導致可怕的錯誤。
慣例:

  • 除非另有說明,否則不允許突變。
  • 沒有突變的投入

可變物件可以使簡單的規約/合約非常複雜
可變物件降低了可變性

可變物件使簡單的合約變得複雜

對同一個可變物件(物件的別名)的多次引用可能意味著程式中的多個地方 – 可能相當分散 – 依靠該物件保持一致。
按照規約說明,契約不能再在一個地方執行,例如, 一個類的客戶和一個類的實施者之間。
涉及可變物件的契約現在取決於每個引用可變物件的每個人的良好行為。

作為這種非本地契約現象的一個症狀,考慮Java集合類,這些類通常記錄在客戶端和實現者之間的非常明確的契約中。

  • 嘗試找到它在客戶端記錄關鍵要求的位置,以便在迭代時無法修改集合。

對這樣的全域性屬性進行推理的需要使得理解難度更大,並且對可變資料結構的程式的正確性有信心。
我們仍然必須這樣做 – 為了效能和便利性 – 但是為了這樣做,我們在bug安全方面付出了巨大的代價。

可變物件降低了可變性

可變物件使得客戶端和實現者之間的契約更加複雜,並且減少了客戶端和實現者改變的自由。
換句話說,使用允許更改的物件會使程式碼難以改變。

(5)*測試和驗證規約

正式契約規約

Java建模語言(JML)
這是一個有優勢的理論方法

  • 執行時檢查自動生成
  • 正式驗證的依據
  • 自動分析工具

缺點

  • 需要很多工作
  • 在大的不切實際
  • 行為的某些方面不符合正式規約

文字說明 – Javadoc

實用方法
記錄每個引數,返回值,每個異常(選中和未選中),該方法執行的操作,包括目的,副作用,任何執行緒安全問題,任何效能問題。
不要記錄實施細節

語義正確性遵守契約

編譯器確保型別正確(靜態型別檢查)

  • 防止許多執行時錯誤,例如“未找到方法”和“無法將布林值新增到int”

靜態分析工具(如FindBugs)可以識別許多常見問題(錯誤模式)

  • 例如:覆蓋equals而不覆蓋hashCode

但是,如何確保語義的正確性?

正式驗證

使用數學方法證明正式規約的正確性
正式證明一個實現的所有可能的執行符合規約
手動努力; 部分自動化; 不能自動確定

測試

使用受控環境中的選定輸入執行程式
目標

  • 顯示錯誤,因此可以修復(主要目標)
  • 評估質量
  • 明確說明書,檔案

黑盒測試:以獨立於實現的方式檢查測試的程式是否遵循指定的規約。

設計規約

(1)按規約分類

比較規約

它是如何確定性的。 該規約是否僅為給定輸入定義了單個可能的輸出,或允許實現者從一組合法輸出中進行選擇?
它是如何宣告的。 規約是否只是表徵輸出的結果,還是明確說明如何計算輸出?
它有多強大。 規約是否只有一小部分法律實施或一大套?
“什麼使一些規約比其他規約更好?”

如何比較兩種規約的行為來決定用新規約替換舊規約是否安全?

規約S2強於或等於規約S1如果

  • S2的先決條件弱於或等於S1
  • 對於滿足S1的先決條件的狀態,S2的後置條件強於或等於S1。

那麼滿足S2的實現也可以用來滿足S1,在程式中用S2代替S1是安全的。

規則:

  • 削弱先決條件:減少對客戶的要求永遠不會讓他們感到不安。
  • 加強後續條件,這意味著做出更多的承諾。

如果S3既不強於也不弱於S1,則規約可能會重疊(因此存在僅滿足S1,僅S3,以及S1和S3的實現)或者可能不相交。
在這兩種情況下,S1和S3都是無法比較的。

(2)圖表規約

這個空間中的每個點代表一個方法實現。
規約在所有可能的實現的空間中定義了一個區域。
一個給定的實現要麼按照規約行事,要滿足前置條件 – 隱含 – 後置契約(它在區域內),或者不(在區域外)。
實現者可以自由地在規約中移動,更改程式碼而不用擔心會破壞客戶端。
這對於實現者能夠提高其演算法的效能,程式碼的清晰度或者在發現錯誤時改變他們的方法等而言是至關重要的。
客戶不知道他們會得到哪些實現。

  • 他們必須尊重規約,但也有自由改變他們如何使用實現而不用擔心會突然中斷。

當S2比S1強時,它在此圖中定義了一個較小的區域。
較弱的規約定義了一個更大的區域。
強化實施者的後置條件意味著他們自由度較低,對產出的要求更強。
弱化前提意味著:實現必須處理先前被規約排除的新輸入。

(3)設計好的規約
規約的質量

什麼是一個好方法? 設計方法意味著主要編寫一個規約。
關於規約的形式:它顯然應該簡潔,清晰,結構良好,以便閱讀。
然而,規約的內容很難規定。 沒有一個可靠的規則,但有一些有用的指導方針。

規約應該是連貫的(內聚的)

該規約不應該有很多不同的情況。 冗長的引數列表,深層巢狀的if語句和布林型標誌都是麻煩的跡象。
除了可怕地使用全域性變數和列印而不是返回之外,規約不是一致的 – 它執行兩個不同的事情,計算單詞並找出最長的單詞。
呼叫的結果應該是資訊豐富的
如果返回null,則無法確定金鑰是否先前未繫結,或者實際上是否繫結為null。這不是一個很好的設計,因為返回值是無用的,除非您確定沒有插入null。

規約應該足夠強大

規約應給予客戶在一般情況下足夠強大的保證 – 它需要滿足其基本要求。 – 在規定特殊情況時,我們必須格外小心,確保它們不會破壞本來是有用的方法。例如,對於一個不合理的論證丟擲異常,但允許任意的突變是沒有意義的,因為客戶端將無法確定實際發生了什麼樣的突變。

規約也應該足夠薄弱

這是一個不好的規約。

  • 它缺少重要的細節:開啟閱讀或寫作檔案? 它是否已經存在或被建立?
  • 它太強大了,因為它無法保證開啟檔案。 它執行的過程可能缺少開啟檔案的許可權,或者檔案系統可能存在一些超出程式控制範圍的問題。相反,說明書應該說更弱一些:它試圖開啟一個檔案,如果成功,檔案具有某些屬性。

規約應該使用抽象型別

用抽象型別編寫我們的規約為客戶和實現者提供了更多的自由。
在Java中,這通常意味著使用介面型別,如Map或Reader,而不是像HashMap或FileReader這樣的特定實現型別。

  • 像列表或集合這樣的抽象概念
  • 特定的實現像ArrayList或HashSet。

這強制客戶端傳入一個ArrayList,並強制實現返回一個ArrayList,即使可能存在他們希望使用的替代List實現。

先決條件還是後置條件?

是否使用前提條件,如果是,則在繼續之前,方法程式碼是否應該嘗試確保先決條件已滿足?
對於程式設計師:

  • 前提條件最常見的用法是要求提供一個屬性,因為該方法檢查該屬性會很困難或昂貴。

如果檢查一個條件會使方法變得難以接受,那麼通常需要一個先決條件。

對使用者而言:

  • 一個不平凡的先決條件會給客戶帶來不便,因為他們必須確保他們不會以不良狀態呼叫該方法(違反前提條件); 如果他們這樣做,沒有可預測的方法來從錯誤中恢復。

所以方法的使用者不喜歡先決條件。

  • 因此,Java API類傾向於指定(作為後置條件),當引數不合適時,它們會丟擲未經檢查的異常。
  • 這使得在呼叫者程式碼中找到導致傳遞錯誤引數的錯誤或不正確的假設更容易。
  • 通常情況下,儘可能靠近錯誤的地點快速失敗,而不是讓糟糕的價值觀通過遠離其原始原因的程式傳播。

關鍵因素是檢查的費用(編寫和執行程式碼)以及方法的範圍。

如果只在類本地呼叫,則可以通過仔細檢查呼叫該方法的所有類來解決前提條件。
如果該方法是公開的,並且被其他開發人員使用,那麼使用前提條件將不太明智。 相反,像Java API類一樣,您應該丟擲一個異常。

總結
規約作為程式實現者與其客戶之間的關鍵防火牆。
它使得單獨的開發成為可能:客戶端可以自由地編寫使用該過程的程式碼,而無需檢視其原始碼,並且實現者可以自由地編寫實現該過程的程式碼而不知道它將如何使用。

減少錯誤保證安全

  • 一個好的規約清楚地記錄了客戶和實施者依賴的相互假設。錯誤通常來自介面上的分歧,並且規約的存在會降低這一點。
  • 在你的規約中使用機器檢查的語言特性,比如靜態型別和異常,而不僅僅是一個人類可讀的評論,可以更多地減少錯誤。容易理解
  • 一個簡短的規約比實現本身更容易理解,並且使其他人不必閱讀程式碼。

準備好改變

  • 規約在程式碼的不同部分之間建立契約,允許這些部分獨立更改,只要它們繼續滿足合同的要求。

宣告性規約在實踐中是最有用的。
先決條件(削弱了規約)使客戶的生活更加艱難,但明智地應用它們是軟體設計師的重要工具,允許實施者做出必要的假設。

減少錯誤保證安全

  • 沒有規約,即使是我們程式中任何部分的細微變化,都可能成為敲打整個事情的尖端多米諾骨牌。
  • 良好的結構,一致的規約最大限度地減少了誤解,並最大限度地提高了我們在靜態檢查,謹慎推理,測試和程式碼審查的幫助下編寫正確程式碼的能力。

容易理解

  • 寫得很好的宣告性規約意味著客戶端不必閱讀或理解程式碼。

準備好改變

  • 適當的規約賦予實現者自由,適當的強壯規約賦予客戶自由。
  • 我們甚至可以自己改變規約,而不必重新審視每個地方的使用情況,只要我們只是加強它們:削弱先決條件並加強後置條件。

相關文章