簡體   English   中英

防御性編程是否違反DRY原則?

[英]Does defensive programming violate the DRY principle?

免責聲明:我是一名正在學習編程的非專業人士。 從未參與過項目,也沒有寫過超過500行的文章。

我的問題是:防御性編程是否違反了“不要重復自己”的原則? 假設我對防御性編程的定義是正確的(讓調用函數驗證輸入而不是相反),這對你的代碼不會有害嗎?

例如,這很糟糕:

int foo(int bar)
{
    if (bar != /*condition*/)
    {
        //code, assert, return, etc.
    }
}

int main()
{
    int input = 10;
    foo(input); //doesn't the extra logic
    foo(input); //and potentially extra calls
    foo(input); //work against you?
}   

與此相比:

int main()
{
    if (input == /*condition*/)
    {
        foo(input);
        foo(input);
        foo(input);
    }
}

再說一次,作為一個外行人,我不知道有多少簡單的邏輯陳述對你來說就性能而言是多少,但對於程序或靈魂而言,防御性編程肯定不好。

違反DRY原則看起來像這樣:

int foo(int bar)
{
    if (bar != /*condition*/)
    {
        //code, assert, return, etc.
    }
}

int main()
{
    int input = 10;
    if (input == /*condition*/)
    {
       foo(input);
       foo(input);
       foo(input);
    }
}

正如你所看到的,問題是我們在程序中有兩次相同的檢查,所以如果條件改變,我們必須在兩個地方修改它,很可能我們忘記了其中一個,導致奇怪的行為。 DRY並不意味着“不要兩次執行相同的代碼”,而是“不要兩次寫相同的代碼”

這一切都歸結為界面提供的合同 有兩種不同的情況:輸入和輸出。

輸入 - 我基本上是指函數的參數 - 應該作為一般規則由實現來檢查。

輸出 - 返回結果 - 應該基本上由調用者信任,至少在我看來。

所有這一切都受到這個問題的影響:如果一方違約,會發生什么? 例如,假設您有一個界面:

class A {
  public:
    const char *get_stuff();
}

並且該合約指定永遠不會返回空字符串(最壞的情況下它將是一個空字符串)然后執行此操作是安全的:

A a = ...
char buf[1000];
strcpy(buf, a.get_stuff());

為什么? 好吧,如果你錯了,並且被調用者返回null,那么程序將崩潰。 真的很好 如果某個對象違反了合同,那么一般來說結果應該是災難性的。

你過度防守所面臨的風險是你寫了很多不必要的代碼(可能會引入更多的錯誤),或者你實際上可能通過吞下一個你不應該做的異常來掩蓋一個嚴重的問題。

當然情況可以改變這一點。

首先我要說明,盲目遵循一個原則是理想主義和錯誤的。 您需要實現您想要實現的目標(例如,您的應用程序的安全性),這通常比違反DRY更重要。 在GOOD編程中,最常需要故意違反原則。

一個例子:我在重要階段進行雙重檢查(例如LoginService - 在調用LoginService.Login之前首先驗證輸入一次,然后再在里面再次驗證輸入),但有時候我確定在確保一切正常工作之后再次刪除外部,通常使用單元測試。 這取決於。

我不會在雙重條件檢查中解決問題。 另一方面,完全忘記他們通常是多個級別更糟:)

我認為防御性編程會成為一種糟糕的說法,因為它會做一些不受歡迎的事情,其中​​包括羅嗦的代碼,更重要的是,應用錯誤。

大多數人似乎同意程序在遇到錯誤時應該快速失敗,但是關鍵任務系統應該最好永遠不會失敗,而是在面對錯誤狀態時不斷努力。

當然,這個陳述有一個問題,一個程序,即使是關鍵任務,如果它處於一個不一致的狀態,它怎么能繼續。 當然它不能,真的。

你想要的是讓程序采取一切合理的步驟來做正確的事情,即使有一些奇怪的事情發生。 與此同時,每當遇到如此奇怪的狀態時,程序就應該大聲抱怨。 如果遇到無法恢復的錯誤,通常應該避免發出HLT指令,而應該正常失敗,安全地關閉系統或激活某些備份系統(如果有的話)。

在您的簡化示例中,是的,第二種格式可能更可取。

但是,這並不適用於更大,更復雜,更現實的程序。

因為您事先不知道“foo”將在何處或如何使用,您需要通過驗證輸入來保護foo。 如果輸入由調用者驗證(例如,示例中為“main”),則“main”需要知道驗證規則並應用它們。

在實際編程中,輸入驗證規則可能相當復雜。 使調用者知道所有驗證規則並正確應用它們是不合適的。 某些調用者會在某處忘記驗證規則,或者做錯誤的規則。 所以最好將驗證放在“foo”中,即使它會被重復調用。 這將負責從調用者轉移到被調用者,這使得調用者可以更少地思考“foo”的細節,並將其更多地用作抽象的可靠接口。

如果你真的有一個模式,其中“foo”將使用相同的輸入多次調用,我建議一個包裝函數執行一次驗證,並且一個不受保護的版本支持驗證:

void RepeatFoo(int bar, int repeatCount)
{
   /* Validate bar */
   if (bar != /*condition*/)
   {
       //code, assert, return, etc.
   }

   for(int i=0; i<repeatCount; ++i)
   {
       UnprotectedFoo(bar);
   }
}

void UnprotectedFoo(int bar)
{
    /* Note: no validation */

    /* do something with bar */
}

void Foo(int bar)
{
   /* Validate bar */
   /* either do the work, or call UnprotectedFoo */
}

像Alex所說,這取決於具體情況,例如,我幾乎總是在登錄過程的每個階段驗證輸入。

在其他地方,你不需要這一切。

但是,在你給出的例子中,我假設,在第二個例子中,你有多個輸入,'因為否則它將是多余的,為相同的輸入調用相同的函數3次,這意味着你將擁有寫條件3次。 現在這是多余的。

如果必須檢查輸入ALWAYS,只需將其包含在函數中。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM