預(yù)防勝于治療:MAKE POSSIBLE IMPOSSIBLE

錯(cuò)誤/異常處理闸盔,一直是程序員痛恨,卻無法擺脫的夢(mèng)魘橙凳。如果一個(gè)系統(tǒng)中僅包含happy path的實(shí)現(xiàn)蕾殴,那么這個(gè)系統(tǒng)的代碼規(guī)模會(huì)顯著縮小笑撞,而邏輯清晰度則大大增加。

C++ 以及更加現(xiàn)代的語言钓觉,都提供了概念:異常茴肥。異常機(jī)制大大的簡(jiǎn)化了程序員的工作,并且讓實(shí)現(xiàn)代碼能夠更加關(guān)注真正的業(yè)務(wù)邏輯荡灾。

但異常機(jī)制并不是免費(fèi)的瓤狐,尤其是C++的異常機(jī)制,對(duì)于空間和時(shí)間都有較大的消耗批幌。所以础锐,大多數(shù)實(shí)時(shí)性嵌入式系統(tǒng)都會(huì)禁止使用異常。于是荧缘,嵌入式程序員只好重新回到“一步一崗皆警,嚴(yán)密防衛(wèi)”的編程模式下。

即便允許使用異常截粗,只要一個(gè)類可能出現(xiàn)異常的狀況信姓,那么這個(gè)類的所有客戶都必須編寫相應(yīng)的異常處理代碼,這仍然是一個(gè)讓人不悅的工作绸罗。

錯(cuò)誤/異常就像強(qiáng)盜意推,一旦讓其闖入,你就需要不斷與其周旋:編寫防御代碼珊蟀,打印錯(cuò)誤日志菊值,DEBUG,等等……耗去你的時(shí)間和精力育灸,還把你原本漂亮的代碼搞的一團(tuán)糟腻窒。

所以,如果能夠在事先就杜絕一些錯(cuò)誤發(fā)生的可能描扯,那么程序員就無須為之付出相應(yīng)的精力定页,在保持系統(tǒng)簡(jiǎn)潔的同時(shí),也會(huì)讓系統(tǒng)更加健壯 (你能確保程序員的錯(cuò)誤處理代碼沒有錯(cuò)嗎绽诚?)典徊。

C++作為一門強(qiáng)類型的靜態(tài)語言,我們要充分利用它的優(yōu)勢(shì)恩够。讓它的嚴(yán)格類型檢查作為我們忠實(shí)的看門狗卒落,在編譯時(shí)就可以辨認(rèn)出任何可能溜進(jìn)來的bug。從而不讓用戶存在犯錯(cuò)的可能(惡意用戶刻意的hack除外)蜂桶。

錯(cuò)誤處理

基于防御式編程的思想儡毕,程序員需要在各處對(duì)可能的錯(cuò)誤進(jìn)行判斷和處理,從而為系統(tǒng)實(shí)現(xiàn)引入額外的復(fù)雜度,而這部分代碼有時(shí)候在系統(tǒng)中的占比是驚人的腰湾。很多系統(tǒng)都有測(cè)試覆蓋率的要求雷恃,但當(dāng)團(tuán)隊(duì)進(jìn)行系統(tǒng)測(cè)試時(shí),卻發(fā)現(xiàn)測(cè)試覆蓋率一旦達(dá)到某個(gè)比例時(shí)(我最近聽到的一個(gè)案例费坊,此比例是30%)倒槐,再想提高這個(gè)比例就變得非常困難。通過工具一查看附井,發(fā)現(xiàn)大部分無法覆蓋的是錯(cuò)誤處理相關(guān)代碼讨越。而其中,有些代碼永遠(yuǎn)也無法得到運(yùn)行永毅,即我們常說的死代碼把跨。 讓我們看一個(gè)簡(jiǎn)單的例子:

struct Object 
{ 
  Status f1(Foo* foo)
  {
    if(foo == NULL) 
    { 
     // 或許能得到運(yùn)行的代碼(但也只是或許而已) 
     // 錯(cuò)誤處理:blah...blah...     
     return FAILED; 
    }
     
    if(f2(foo) != SUCCESS) 
    { 
      // 死代碼 
      // 錯(cuò)誤處理:blah...blah... 
    } 
    // 其它代碼 
    // ... 
    return SUCCESS; 
  } 
  
private:  
  Status f2(Foo* foo) 
  { 
    if(foo == 0) 
    { 
      // 死代碼      
      // blah...blah... 
      return FAILED; 
    } 
    value = foo->getValue() + 5;
    return SUCCESS; 
  } 
private:  
  int value; 
  // ... 
}; 

出現(xiàn)這種問題的原因,一則是防衛(wèi)過度:即私有函數(shù)是否需要對(duì)傳入的空指針進(jìn)行防衛(wèi)沼死?事實(shí)上着逐,將那些死代碼刪除,在任何情況下意蛀,都不會(huì)讓系統(tǒng)更不安全滨嘱。 而清理后的結(jié)果,卻會(huì)比原來要清晰和簡(jiǎn)潔許多浸间。

但那些遵從懷疑一切哲學(xué)的謹(jǐn)慎編程者,對(duì)這種做法持反對(duì)態(tài)度吟榴。他們強(qiáng)烈認(rèn)同必須通過考驗(yàn)才可獲得信任的價(jià)值觀魁蒜。他們認(rèn)為:私有函數(shù)也是函數(shù)。而作為函數(shù)吩翻,它不應(yīng)該知道究竟誰是它的客戶兜看,因此也不應(yīng)該對(duì)客戶給予信任。更何況狭瞎,代碼是隨時(shí)可以改變的细移,當(dāng)前的用戶能夠保證其安全性,并不意味這隨后的新增客戶也會(huì)同樣遵守熊锭。

我們先不去討論這樣的觀點(diǎn)是否正確弧轧。重要的是,無論你屬于那一派碗殷,你都肯定會(huì)同意:如果一件事不可能出錯(cuò)精绎,你就不會(huì)再為之焦慮

指針 vs. 引用

C時(shí)代锌妻,指針是間接訪問對(duì)象的唯一方式代乃。由于任何一個(gè)指針都可是是空指針,謹(jǐn)慎的程序員會(huì)在獲取到一個(gè)指針時(shí)仿粹,都會(huì)先判斷其是否為空搁吓。這種代碼在任何一個(gè)正式的C項(xiàng)目中比比皆是原茅。

到了C++時(shí)代,為了避免這類問題堕仔,一種新的對(duì)象訪問方式誕生了:引用擂橘。不幸的是,由于很多C++程序員都是從C工程師轉(zhuǎn)化而來的贮预,所以贝室,很多項(xiàng)目仍然在固執(zhí)的大量使用指針。比如:

Status f(Object* object)
{
  if(object == 0) {
    // 錯(cuò)誤處理代碼:記日志,發(fā)告警等等仿吞。
    return FAILED; 
  }
  // 對(duì) object 所指對(duì)象進(jìn)行操作 
  object->f();
  // ...
}

指針不同滑频,引用是對(duì)象的別名。因此唤冈,引用具有如下性質(zhì):

  • 引用在定義時(shí)即需要賦值;
  • 一旦賦值峡迷,你不可能讓其引用其它對(duì)象;
  • 對(duì)引用進(jìn)行取地址操作,就是對(duì)對(duì)象進(jìn)行取地址操作你虹。

所以绘搞,一旦將參數(shù)改為引用方式,你就得到了傳入對(duì)象非空的承諾傅物,代碼會(huì)一下子簡(jiǎn)潔許多:

Status f(Object& object)
{
  object.f();
  // ...
}

空引用

引用的非空特性給我們帶來很大的便利夯辖。不幸的是,現(xiàn)實(shí)在理想面前總是很骨感:空引用的的確確存在董饰。比如:

void f(Object& object);

Object* object = 0;

// 空引用! 
f(*object);

這個(gè)殘酷的現(xiàn)實(shí)蒿褂,動(dòng)搖了我們的信念:使用指針還是引用,似乎并沒有本質(zhì)差別卒暂,而引用還有那么一堆約束和限制啄栓,還是使用指針更好。

事前條件

但是也祠,從契約式編程(Design By Contract)的角度看昙楚,一旦一個(gè)函數(shù)的某個(gè)參數(shù)聲明為引用類型,那就意味著它明確的定義了一個(gè)事前條件(Precondition):函數(shù)的調(diào)用者有義務(wù)確保傳入的引用非空诈嘿。否則堪旧,空引用引起的崩潰應(yīng)該由調(diào)用者負(fù)責(zé)。

如果你仍然對(duì)此持有異議奖亚,那么不妨設(shè)想一下:讓我們重新回到指針時(shí)代崎场,看看會(huì)發(fā)生什么。

指針事實(shí)上有三種狀態(tài):

  1. 空:空指針
  2. 有效:指向一個(gè)合法對(duì)象
  3. 非法:指向一個(gè)未知世界

而引用卻只有兩種狀態(tài):

  1. 有效:引用了一個(gè)合法對(duì)象
  2. 非法:引用了一個(gè)非法對(duì)象(包括空對(duì)象

如果一個(gè)函數(shù)的某個(gè)參數(shù)為指針類型遂蛀,那么作為函數(shù)的實(shí)現(xiàn)者谭跨,你也只能檢測(cè)指針是否為空,卻永遠(yuǎn)無法保證客戶是否會(huì)傳入一個(gè)非法指針。所以螃宙,保證指針的合法性(無論是否為空)蛮瞄,當(dāng)然是調(diào)用者的責(zé)任。

由此不難得出結(jié)論:保證一個(gè)引用不會(huì)是非法引用也應(yīng)該是調(diào)用者的責(zé)任谆扎。

事后條件

而當(dāng)一個(gè)函數(shù)的返回值是一個(gè)引用時(shí)挂捅,這樣的聲明則明確定義了一個(gè)事后條件(Post Condition)。而保證引用的合法性堂湖,就成了函數(shù)實(shí)現(xiàn)者的義務(wù)闲先。

// 函數(shù)實(shí)現(xiàn)者必須保證返回值的合法性
Object& f();

所以,實(shí)現(xiàn)者必須保證返回的對(duì)象不能是個(gè)臨時(shí)對(duì)象无蜂。比如:

Object& getObject()
{
  // 不要這樣做 
  Object object; 
  // ...
  return object;
}

所以伺糠,也要避免可能造成內(nèi)存泄漏。比如:

Object& getObject()
{
   return *(new Object); 
}

而返回引用的優(yōu)勢(shì)則在于:它讓所有的客戶代碼無須再進(jìn)行相關(guān)的錯(cuò)誤處理斥季。

// 無須判斷空指針 
getObject().f();

何時(shí)必須使用指針

由于引用比指針更加嚴(yán)格训桶,所以,并非在所有場(chǎng)景下引用都可以代替指針酣倾。下面列出了幾種常見理由(但并非全部)舵揭。

空指針是一種合法狀態(tài)

指針引用貌似很像,但事實(shí)上存在在本質(zhì)的語義差別:

  • 指針是MaybeOptional語義躁锡;
  • 引用是Value語義午绳。

因而,一些時(shí)候映之,空指針也是一種合法狀態(tài)(正如Maybe語義的Nothing一樣)箱叁,比如,單向鏈表中最后一個(gè)節(jié)點(diǎn)的next指針惕医;一顆樹Root節(jié)點(diǎn)的 parent指針。比如:

struct Node 
{
  // ...
private:
  // 作為 Root 節(jié)點(diǎn),parent 可能為空 Node* parent;
  // 作為 Leaf 節(jié)點(diǎn),children 可能為空 Node* leftChild;
  Node* rightChild;
};

這種情況下算色,我們只能使用指針抬伺,而不能使用引用。

需變更所指對(duì)象

如果一個(gè)指針在其生命周期內(nèi)需要改變所指向的對(duì)象灾梦。比如:

struct Foo 
{
  Foo(Object* object) : object(object) {}
  // 變更接口
  void setObject(Object* object) 
  {
    this->object = object; 
  }
  // ...
private:
  // 只能使用指針 
  Object* object;
};

作為輸出參數(shù)峡钓,需要得到一個(gè)對(duì)象

函數(shù)希望通過某個(gè)輸出參數(shù)以取得一個(gè)對(duì)象,比如:

Status getObject(int key, Object** object) 
{
  // ...
  (*object) = findObjectByKey(key); 
  if(*object == 0)
  {
    return NOTFOUND; 
  }
  // ...
  return SUCCESS; 
}

當(dāng)然若河,當(dāng)通過返回值來返回對(duì)象時(shí)能岩,如果存在查找不到的可能性,也應(yīng)該使用指針:

Object* getObject(int key) 
{
  Object* object = findObjectByKey(key); 
  if(object == 0)
  {
    return 0; 
  }
  // ...
  return object; 
}

除非你通過空對(duì)象模式萧福,避免空指針的可能性:

Object& getObject(int key) 
{
  Object* object = findObjectByKey(key); 
  if(object == 0)
  {
    return NullObject::getInstance(); 
  }
  // ...
  return *object; 
}

動(dòng)態(tài)分配內(nèi)存 vs. 靜態(tài)分配

單例模式Singleton)估計(jì)是OO程序員使用最為廣泛的設(shè)計(jì)模式拉鹃。一般而言,從標(biāo)準(zhǔn)的教科書上,Singleton的實(shí)現(xiàn)模式如下:

struct Foo
{
  static Foo* getInstance();

  // public functions

private:
   Foo();
   
private:
   static Foo* foo;
};

Foo* Foo:foo = 0;

Foo* Foo::getInstance()
{
   if(foo != 0) return foo;
   
   foo = new Foo();
   
   return foo;
}

這樣的實(shí)現(xiàn)方式至少有兩個(gè)問題:

  1. 內(nèi)存分配失敗的可能膏燕;
  2. 由于內(nèi)存分配可能失敗钥屈,所有的客戶代碼為了安全,都必須檢查getInstance()是否返回空指針坝辫。

這無疑會(huì)增加客戶代碼的復(fù)雜度篷就。為了避免這類問題,我們必須從源頭消滅這樣的可能性:將內(nèi)存分配從動(dòng)態(tài)改為靜態(tài)分配:

struct Foo
{
  static Foo& getInstance();

  // public functions

private:
   Foo();   
};

Foo& Foo::getInstance()
{
   static Foo foo;
   return foo;
}

這樣的實(shí)現(xiàn)方式可以帶來諸多好處:

  • 由于內(nèi)存是靜態(tài)分配的近忙,一旦系統(tǒng)成功加載竭业,就永遠(yuǎn)也不可能失敗,因而可以將返回值類型改為引用及舍;
  • 這種局部靜態(tài)分配的方式未辆,讓實(shí)例的構(gòu)造是lazy的,這可以有效避免C++靜態(tài)成員初始化順序不確定帶來的問題击纬。
  • 對(duì)于getInstance函數(shù)的實(shí)現(xiàn)鼎姐,由于不需要每次都判斷指針是否為空,甚至?xí)硇阅苌系囊恍┖锰幐瘛#呐聦?duì)于性能影響微乎其微炕桨,但也要遵守不做不必要的劣化原則);
  • 對(duì)于客戶肯腕,杜絕了指針為空的可能性献宫,可以編寫更加簡(jiǎn)潔的代碼,避免思考空指針處理的邏輯实撒,甚至也會(huì)對(duì)性能有些好處(邏輯同上)姊途。

枚舉 vs. 投幣模式

假設(shè)現(xiàn)在有個(gè)需求,要求你實(shí)現(xiàn)一個(gè)長(zhǎng)度類知态。類的使用者捷兰,可以使用英里 (Mile),Yard)和英尺Feet)為單位來表現(xiàn)一個(gè)長(zhǎng)度负敏。并且可以比較任意兩個(gè)長(zhǎng)度是否相等贡茅。

typedef unsigned int Amount;

enum Unit {
         FEET = 1,
         YARD = 3    * FEET,
         MILE = 1760 * YARD
};

struct Length 
{
  Length(const Amount& amount, const Unit unit) : amountInBaseUnit(unit * amount)
  {}
  
  bool operator==(const Length& another) const 
  {
    return amountOfBaseUnit == another.amountOfBaseUnit; 
  }

private:
  Amount amountOfBaseUnit;
};

這個(gè)實(shí)現(xiàn)非常簡(jiǎn)潔。它使用枚舉來紀(jì)錄長(zhǎng)度單位之間的轉(zhuǎn)換系數(shù)其做,無論使用者以何單位來紀(jì)錄一個(gè)長(zhǎng)度顶考,Length總是會(huì)在構(gòu)造時(shí)將其轉(zhuǎn)化為以基準(zhǔn)單位為單位的數(shù)量,而基準(zhǔn)單位在這里很明顯是FEET妖泄。

但這個(gè)實(shí)現(xiàn)的一個(gè)重大問題是:由于在C++ 98里驹沿,枚舉并非強(qiáng)類型。事實(shí)上使用者可以傳入任何整數(shù)作為 unit參數(shù)蹈胡。

基于防御式編程的哲學(xué)渊季,我們需要處理這種情況朋蔫。所以,一種做法是在構(gòu)造函數(shù)里進(jìn)行檢查梭域,如果傳入一個(gè)非法的unit斑举,就拋出異常。

struct Length 
{
  Length(const Amount& amount, const Unit unit) : amountInBaseUnit(unit * amount)
  {
    if(unit != MILE && unit != YARD && unit != FEET) 
    {
      throw InvalidUnitException; 
    }
  }
// ...
private:
  Amount amountOfBaseUnit;
};

這個(gè)實(shí)現(xiàn)的主要問題是:構(gòu)造函數(shù)可能會(huì)拋出異常病涨,因而客戶代碼必須處理此異常富玷。更何況,在嵌入式系統(tǒng)上既穆,異常往往是不允許使用的赎懦。因而必須額外提供一個(gè)額外的init函數(shù)以允許返回失敗。

當(dāng)然幻工,你還可以進(jìn)行其它方式的實(shí)現(xiàn)励两,來應(yīng)對(duì)上述問題。但無論你怎樣做囊颅,只要Unit是個(gè)枚舉当悔,你總是逃不脫要對(duì)錯(cuò)誤進(jìn)行檢測(cè)和處理。然后把你原有的簡(jiǎn)潔設(shè)計(jì)搞的一團(tuán)糟踢代。

避免處理錯(cuò)誤的最好方式是不要讓錯(cuò)誤有發(fā)生的可能性盲憎。所以我們將實(shí)現(xiàn)方式改為:

struct Unit 
{
  // 允許用戶使用的 Unit 對(duì)象
  static Unit getMile() { return MILE_FACTOR; } 
  static Unit getYard() { return YARD_FACTOR; } 
  static Unit getFeet() { return FEET_FACTOR; }

  Amount toAmountInBaseUnit(const Amount& amount) const 
  {
    return conversionFactor * amount; 
  }

private:
  // 構(gòu)造函數(shù)私有以避免用戶實(shí)例化 Unit 對(duì)象 
  Unit(unsigned int conversionFactor)
    : conversionFactor(conversionFactor)
  {}

private: 
  enum
  {
    FEET_FACTOR = 1,
    YARD_FACTOR = 3 * FEET_FACTOR,
    MILE_FACTOR = 1760 * YARD_FACTOR
  };
  
private:
  unsigned int conversionFactor;
};

// 通過宏定義,提供和原來一樣的 Unit 使用方式。 
 #define MILE Unit::getMile()
 #define YARD Unit::getYard()
 #define FEET Unit::getFeet()

struct Length 
{
  Length(const Amount& amount, const Unit& unit)
    : amountInBaseUnit(unit.toAmountInBaseUnit(amount))
  {}
  // ...
private:
  Amount amountOfBaseUnit;
};

這種處理問題的手段胳挎,被稱為投幣模式(Slug Pattern)饼疙。

類復(fù)用 vs. 模板復(fù)用

現(xiàn)在,我們?cè)谏厦娴睦踊A(chǔ)上增加一個(gè)新需求:實(shí)現(xiàn)一個(gè)容量類慕爬。此類的使用者窑眯,可以使用盎司(OZ)和湯匙(TBSP)為單位來表現(xiàn)一個(gè)容量。并且可以比較任意兩個(gè)容量是否相等医窿。

不難看出磅甩,容量長(zhǎng)度的需求非常相似。所以姥卢,它們的代碼是可以復(fù)用的卷要。于是,我們將這兩個(gè)體系合二為一隔显,起一個(gè)更通用的名字:Quantity

struct Quantity 
{
  Quantity(const Amount& amount, const Unit& unit)
    : amountInBaseUnit(unit.toAmountInBaseUnit(amount))
  {}

  bool operator==(const Quantity& another) const 
  {
    return amountOfBaseUnit == another.amountOfBaseUnit; 
  }
  
private:
  Amount amountOfBaseUnit;
};

struct Unit 
{
  // 允許用戶使用的長(zhǎng)度 Unit 對(duì)象
  static Unit getMile() { return MILE_FACTOR; } 
  static Unit getYard() { return YARD_FACTOR; } 
  static Unit getFeet() { return FEET_FACTOR; }

  // 允許用戶使用的容量 Unit 對(duì)象
  static Unit getTbsp() { return TBSP_FACTOR; }
  static Unit getOz()   { return OZ_FACTOR; }
  
  // ...
private: 
  enum
  {
     FEET_FACTOR = 1,
     YARD_FACTOR = 3
     MILE_FACTOR = 1760 * YARD_FACTOR
  };
  
  enum
  {
    TBSP_FACTOR = 1,
    OZ_FACTOR   = 2  * TBSP_FACTOR,
  };
  
private:
  unsigned int conversionFactor;
};

#define MILE Unit::getMile() 
#define YARD Unit::getYard() 
#define FEET Unit::getFeet()

#define OZ Unit::getOz() 
#define TBSP Unit::getTbsp()

我們現(xiàn)在通過一套類就解決了兩個(gè)體系的問題饵逐,我們不僅為自己對(duì)DRY原則的堅(jiān)持和強(qiáng)大的抽象能力感到驕傲括眠。

但沒過多久,你就會(huì)收到一個(gè)來自于用戶的Bug報(bào)告: 1 FEET 怎能等于 1 TBST 呢?

哦倍权,長(zhǎng)度容量是兩種不同的體系掷豺。我們?cè)瓉硗嗽谂袛嘞嗟葧r(shí)對(duì)兩種體系進(jìn)行區(qū)分捞烟。

作為富有經(jīng)驗(yàn)的程序員,這個(gè)難不倒我們:只需要為Unit添加一個(gè)類型当船,對(duì)兩種不同的Unit進(jìn)行區(qū)分即可题画。

struct Unit 
{
  static Unit getMile() { return Unit(MILE_FACTOR, LENGTH_TYPE); } 
  // ...
  static Unit getOz() { return Unit(OZ_FACTOR, VOLUME_TYPE); }
  
  // 增加一個(gè)判斷 UnitType 是否一致的借口
  bool hasSameType(const Unit& another) const 
  {
    return unitType == another.unitType; 
  }
  // ...

private:
  Unit(unsigned int conversionFactor, UnitType unitType) 
    : conversionFactor(conversionFactor)
    , unitType(unitType)
  {}
  
  // 增加枚舉類型:UnitType 
  enum UnitType
  {
    LENGTH_TYPE,
    VOLUME_TYPE
  };
  
  // ...
private:
  unsigned int conversionFactor; UnitType unitType;
};

然后,我們將Quantity的實(shí)現(xiàn)改為下面的樣子:

struct Quantity 
{
  Quantity(const Amount& amount, const Unit& unit) 
    : amount(amount)
    , unit(unit)
  {}

  bool operator==(const Quantity& another) const 
  {
    // 先判斷兩個(gè) Unit 是否屬于同一類型
    return unit.hasSameType(another.unit) &&
           getAmountInBaseUnit() == another.getAmountInBaseUnit();
  } 

private:
  Amount getAmountInBaseUnit() const 
  {
    return unit.toAmountInBaseUnit(amount); 
  }

private:
  Amount amount; Unit unit;
};

這樣做總算滿足了要求德频,盡管其背后的語義仍然略顯怪異(為什么允許一個(gè)長(zhǎng)度和一個(gè)容量對(duì)比相等性?)苍息。但如果我們現(xiàn)在增加一個(gè)新的需求:加法運(yùn)算,其實(shí)現(xiàn)方式將會(huì)變得非常棘手壹置。

Quantity Quantity::operator+(const Quantity& another) 
{
  if(not unit.hasSameType(another)) {
    // 該如何做? 拋出異常? 還是返回一個(gè)非法的 Quantity? 
  }
  // ...
}

而避免這種問題的方法是:禁止兩個(gè)不同體系的Quantity之間進(jìn)行任何互操作竞思。而模板可以幫助我們達(dá)到目標(biāo):

template <typename UNIT> 
struct Quantity
{
  Quantity(const Amount& amount, const UNIT& unit)
    : amountInBaseUnit(UNIT.toAmountInBaseUnit(amount))
  {}
  
  bool operator==(const Quantity& another) const 
  {
    return getAmountInBaseUnit() == another.getAmountInBaseUnit(); 
  }

  Quantity operator+(const Quantity& another) 
  {
    return amountInBaseUnit + another.amountInBaseUnit; 
  }
private:
  Quantity(const Amount& amountInBaseUnit)
    : amountInBaseUnit(amountInBaseUnit)
  {}
private:
  Amount amountInBaseUnit;
};

由于兩個(gè)體系的Unit的算法和數(shù)據(jù)都完全一樣,所以這里我們?yōu)樗鼈兲崛∫粋€(gè)基類钞护。其中盖喷,我們將Unit 的構(gòu)造函數(shù)設(shè)為protected的,以避免用戶直接創(chuàng)建Unit實(shí)例难咕。

struct Unit 
{
  Amount toAmountInBaseUnit(const Amount& amount) const;
protected:
  Unit(unsigned int conversionFactor)
    : conversionFactor(conversionFactor)
  {}
private:
  unsigned int conversionFactor;
};

然后课梳,我們讓LengthUnitVolumeUnit分別繼承自Unit:

struct LengthUnit : Unit
{
  // 允許用戶使用的長(zhǎng)度 Unit 對(duì)象
  static LengthUnit getMile() { return MILE_FACTOR; } 
  static LengthUnit getYard() { return YARD_FACTOR; } 
  static LengthUnit getFeet() { return FEET_FACTOR; }

private: 
  enum
  {
    FEET_FACTOR = 1,
    YARD_FACTOR = 3    * FEET_FACTOR,
    MILE_FACTOR = 1760 * YARD_FACTOR
  }; 
};

struct VolumeUnit : Unit 
{
  static Unit getTbsp() { return TBSP_FACTOR; }
  static Unit getOz()   { return OZ_FACTOR; }

private: 
  enum
  {
     TBSP_FACTOR = 1,
     OZ_FACTOR   = 2 * TBSP_FACTOR
  }; 
};

然后我們用LengthUnitVolumeUnit分別實(shí)例化Quantity模板,從而得到兩個(gè)僅僅共享代碼余佃,卻無法相互操作的兩個(gè)類:LengthVolume暮刃。

typedef Quantity<LengthUnit> Length;
typedef Quantity<VolumeUnit> Volume;

另外,通過這樣的方法咙冗,我們不僅避免了兩種體系之間互操作的問題沾歪,還將兩個(gè)體系從代碼上也清晰的分割為相互獨(dú)立的兩個(gè)部分。

總結(jié)

本文通過幾個(gè)不同的C++語言的例子,來說明預(yù)防勝于治療思想對(duì)于提高系統(tǒng)健壯性的重要性。針對(duì)不同的語言敢订,解決方案或許不同铆遭,但本文在各個(gè)例子中思考過程才是本文更想給讀者帶來的價(jià)值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末走越,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泉哈,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件破讨,死亡現(xiàn)場(chǎng)離奇詭異丛晦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)提陶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門烫沙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人隙笆,你說我怎么就攤上這事锌蓄∩ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵瘸爽,是天一觀的道長(zhǎng)您访。 經(jīng)常有香客問我,道長(zhǎng)剪决,這世上最難降的妖魔是什么灵汪? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮昼捍,結(jié)果婚禮上识虚,老公的妹妹穿的比我還像新娘。我一直安慰自己妒茬,他們只是感情好担锤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著乍钻,像睡著了一般肛循。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上银择,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天多糠,我揣著相機(jī)與錄音,去河邊找鬼浩考。 笑死夹孔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的析孽。 我是一名探鬼主播搭伤,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼袜瞬!你這毒婦竟也來了怜俐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤邓尤,失蹤者是張志新(化名)和其女友劉穎拍鲤,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體汞扎,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡季稳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了澈魄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片景鼠。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖一忱,靈堂內(nèi)的尸體忽然破棺而出莲蜘,到底是詐尸還是另有隱情,我是刑警寧澤帘营,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布票渠,位于F島的核電站,受9級(jí)特大地震影響芬迄,放射性物質(zhì)發(fā)生泄漏问顷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一禀梳、第九天 我趴在偏房一處隱蔽的房頂上張望杜窄。 院中可真熱鬧,春花似錦算途、人聲如沸塞耕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扫外。三九已至,卻和暖如春廓脆,著一層夾襖步出監(jiān)牢的瞬間筛谚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工停忿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留驾讲,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓席赂,卻偏偏與公主長(zhǎng)得像吮铭,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子氧枣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容