接下來第 13 天開始後,會比較往我在探索軟體工程整個過程中,有看到一些我想提出來的一些思考,並且整理一些我自已的想法,有些我已經有在用了,有些沒有 ~ 就自已當參考吧。
然後這篇文章我們將要來談談以下來個東西 :
它是一種的主要核心概念為 :
保護自已,不相信任何人,也就是從外部進來的所有東西,都要檢查
在任何情況下,都有保證 code 的可靠性,先來給個簡單的範例如下,它是一個課程平台,然後如果完全要走這個概念來開發,那大概就會寫的如下一樣,一堆的檢查,事實上如果真的要寫還可以繼續寫下去。
class Product {
constructor(public id: number, public name: string, public price: number) {}
}
class ShoppingCart {
private items: { product: Product, quantity: number }[] = [];
// 防禦性檢查,確保產品和數量的有效性
addItem(product: Product, quantity: number): void {
// 檢查產品是否有效
if (!product) {
throw new Error("Invalid product.");
}
// 檢查產品 ID 是否有效
if (!product.id || product.id <= 0) {
throw new Error("Invalid product ID. It must be a positive number.");
}
const productIdExist = await ProductMode.isExistAsync(product.id)
if(!productIdExist){
throw new Error("The product is nonexistent")
}
// 檢查產品名稱是否有效
if (!product.name || product.name.trim() === '') {
throw new Error("Invalid product name. It cannot be empty.");
}
// 檢查產品價格是否有效
if (!product.price || product.price <= 0) {
throw new Error("Invalid product price. It must be a positive number.");
}
// 檢查數量是否有效
if (!quantity || quantity <= 0) {
throw new Error("Invalid quantity. It must be a positive number.");
}
// 檢查產品是否已經在購物車中
const existingItem = this.items.find(item => item.product.id === product.id);
if (existingItem) {
throw new Error(`Product with ID ${product.id} is already in the cart.`);
}
// 添加產品到購物車
this.items.push({ product, quantity });
}
}
客觀來說,不能說有錯,但是有個東西要想一下 :
如果 product 早就已經被驗證過了呢 ?
然後它還有一些潛在的問題 :
然後在 DBC 的作者寫的一篇論文中《Applying “Design by Contract》也有提到這些問題,所以他提出了 Design By Contract ( DBC ) 來當替代方案。
然後我這裡先說一下我的想法,之前我年輕時的確也一直都有不要相信任何傳進來的東西
,所以寫了一大堆的檢查,一開始寫到只是單純的簡單驗證那還好,但後來有些驗證可能還會去 db 要資料來判斷在不在,然後就有在開始思考,這個事情是對的嗎 ?
如果是 API 我覺得還有理由,因為可能會被用戶玩壞,但是拉回到自已系統內,有時後就在想,真的有需要這樣嗎 ? 所以後來往這個方向來查,就查到這兩個名詞,原來不是只有我有這個煩惱……
所以 code review 時,通常如果是 API 那層我會傾向用防禦式來看,而在內部時就會選擇偏相信帶入的
這個是《Applying “Design by Contract》中的核心想法 :
software elements should be considered as implementations meant to satisfy well-understood specifications, not as arbitrary executable texts. This is where the contract theory comes in.
然後我自已的理解為 :
軟體應該被視為為了滿足
明確規範(Contract)
而設計的實現,而不是任意的可執行代碼
而下面就是 contract 內的三種規範條件:
然後加上這幾個以後,我覺得比較好理解話為 :
一個方法呼叫方有義務帶入符合
Precondition
的資料進來,並且實作方有責任達成後置條件與回傳結果,如果有 error 那就一定是呼叫方的問題
然後下面比較算是參考其它網路資料後,將上面的防禦式程式設計的範例,拿下來改,整體來說這個方法讓我們明確的知道前置條件
那些是呼叫方的義務,那時是這個方法的責任,但我自已寫有個點很 confuse 那就是 :
我好像還是要檢查 Precondition 啊……
奇怪是我理解有問題嗎… 就算看目前最完整支援 DbC 的語言 Eiffel 的範例看起來的確還是會需要檢查 Precondition ……
https://www.eiffel.org/doc/solutions/Design_by_Contract_and_Assertions
make (a_nm: STRING; a_offset: INTEGER)
-- Initalize with name `a_nm' and utcoffset `a_offset'.
require
name_not_empty: a_nm /= Void and then not a_nm.is_empty
offset_valid: a_offset >= -12 and a_offset <= 12
do
name := a_nm.twin
utcoffset := a_offset
ensure
name_initialized: name.is_equal (a_nm)
utcoffset_initialized: utcoffset = a_offset
end
但我在猜,它的核心還在於明確規範(Contract)
,而不是什麼東西都假設別人會亂來 ~
class ShoppingCart {
private items: { product: Product, quantity: number }[] = [];
/**
* 添加產品到購物車
*
* 它期望的是什麼?(前置條件)
* - 產品 ID、名稱和價格必須是有效的(由 Product 類內的檢查保證)
* - 數量必須是正數
* - 產品不能已經存在於購物車中
*
* 它要保證的是什麼?(後置條件)
* - 購物車中的產品數量會增加,且不能有重複的產品
*/
async addItem(product: Product, quantity: number): Promise<void> {
// 前置條件檢查
this.requireProductExists(product); // 產品存在
await this.requireValidProductId(product.id); // 產品 ID 合法
this.requirePositiveQuantity(quantity); // 數量必須為正數
this.requireProductNotInCart(product.id); // 產品不能已存在於購物車
// 添加產品到購物車
this.items.push({ product, quantity });
// 後置條件檢查
this.ensureProductAdded(product.id); // 確保產品已成功加入購物車
this.ensureNoDuplicateProducts(); // 確保購物車中沒有重複產品
}
private requireProductExists(product: Product): void {
if (!product) {
throw new Error("Product is required.");
}
}
private requirePositiveQuantity(quantity: number): void {
if (quantity <= 0) {
throw new Error("Quantity must be a positive number.");
}
}
private requireProductNotInCart(productId: number): void {
const existingItem = this.items.find(item => item.product.id === productId);
if (existingItem) {
throw new Error(`Product with ID ${productId} is already in the cart.`);
}
}
private ensureProductAdded(productId: number): void {
const existingItem = this.items.find(item => item.product.id === productId);
if (!existingItem) {
throw new Error(`Postcondition failed: Product with ID ${productId} was not added to the cart.`);
}
}
private ensureNoDuplicateProducts(): void {
const productIds = this.items.map(item => item.product.id);
const uniqueProductIds = new Set(productIds);
if (productIds.length !== uniqueProductIds.size) {
throw new Error("Postcondition failed: Duplicate products found in the cart.");
}
}
}
整體來說這兩個設計思想的核心分別如下 :
我自已本身年輕時比較算是走防禦式程式設計這一塊的思想,就是每個方法都假設帶入的參數不可以相信,所以全部都要檢查,這個思想簡單,就是 check 到爆,但後來到了一定的經驗後,就在想這樣是合理的嗎 ?
然後接下來查到 DBC 它所提的 :
軟體應該被視為為了滿足
明確規範(Contract)
而設計的實現,而不是任意的可執行代碼
程式應該是我們說好這個方法要如何使用,而不是不相信一切,如果真的不相信一切,那我覺得只有在有錢有閒的公司才做的到。
不過我也承認在這篇文章中的 DBC 範例我自已覺得只是個樣子,是不是對的我也有點不確定,但是我自已覺得至少在開發時,每一次的檢查都可以試這分看看Precondition
、Postcondition
、Invariant
,雖然可能還不能減少太多 check,但至少可以知道那些職責是呼叫方,那些時職責是實作方的。