2016-10-31 99 views
0

我想擴展一個類以包含額外的數據和功能(我想要多態行爲)。使用繼承和多繼承似乎很明顯。爲擴展類功能提供最佳方法的建議

看了各種職位,繼承(尤其是多重繼承),是有問題的,我已經開始尋找其他的選擇:

  • 把所有的數據和功能於一體的類,而不是使用繼承
  • Composite模式
  • 混入

是否有下面的繼承示例建議的方法呢?這是繼承是否合理的情況? (但我不喜歡把默認功能的基類)

#include <iostream> 

//================================ 
class B { 

public: 
    virtual ~B() { } 

    void setVal(int val) { val_ = val; } 

    // I'd rather not have these at base class level but want to use 
    // polymorphism on type B: 

    virtual void setColor(int val) { std::cout << "setColor not implemented" << std::endl; } 
    virtual void setLength(int val) { std::cout << "setLength not implemented" << std::endl; } 

private: 
    int val_; 

}; 

//================================ 
class D1 : virtual public B { 

public: 
    void setColor(int color) { 
     std::cout << "D1::setColor to " << color << std::endl; 
     color_ = color; 
    } 

private: 
    int color_; 

}; 

//================================ 
class D2 : virtual public B { 

public: 
    void setLength(int length) { 
     std::cout << "D2::setLength to " << length << std::endl; 
     length_ = length; 
    } 

private: 
    int length_; 

}; 

//================================ 
// multi-inheritance diamond - have fiddled with mixin 
// but haven't solved using type B polymorphically with mixins 
class M1 : public D1, public D2 { 

}; 


//================================ 
int main() { 

    B* d1 = new D1; 

    d1->setVal(3); 
    d1->setColor(1); 


    B* m1 = new M1; 

    m1->setVal(4); 
    m1->setLength(2); 
    m1->setColor(4); 

    return 0; 
} 
+0

沒有具體的例子就很難說出任何東西。繼承代表的是一種關係。通常,當你開始談論向對象添加「行爲和更多數據」時,這是否意味着它總是使用行爲和數據,或者它是可選的?如果這樣查看組件和組合而不是繼承和運行時查詢附加行爲。如果你真的覺得你會繼續擴展你的基本類型,這種方法更加模塊化。 –

+0

您可能想了解[純虛方法](http://stackoverflow.com/questions/1306778/c-virtual-pure-virtual-explained) – wasthishelpful

+0

單繼承是相當自然的,以確保整個層次結構將尊重共享概念;多重繼承(沒有虛擬)對於爲同一個對象提供不同的視圖可能很有用。應謹慎使用多個虛擬繼承(dynamic_cast從基礎派生到派生需要),正如您所提到的,複合或裝飾器模式可能是有趣的替代方案。 – Franck

回答

1

與原來的示例代碼

有許多與你的榜樣問題懷疑的問題。

首先,您不必在基類中提供函數體。改用純虛函數。其次,你的類D1和D2都沒有功能,所以它們應該是抽象的(這會阻止你從它們中創建被剝奪的對象)。如果你真的爲你的基類使用純虛擬函數,這第二個問題將變得清晰。編譯器將開始發出警告。

與D134一樣,將D1例化爲new D1,這是糟糕的設計,因爲D1沒有setLength方法的真正功能實現,即使您給它一個「虛擬」主體。給它一個'虛擬'身體(一個沒有做任何有用的東西),所以掩蓋了你的設計錯誤。

所以你的評論(但我不喜歡不得不把默認函數放在基類中)證明了一個適當的直覺。必須這樣做的信號有缺陷的設計。 D1對象無法理解setLength,而其繼承的公共接口承諾它可以。

並且:如果使用正確,多繼承沒有任何問題。它非常強大和優雅。但你必須在適當的地方使用它。 D1和D2是B的部分實現,所以抽象,並從兩者繼承將確實給你一個完整的實現,非常具體。

也許一個很好的規則是:只有在看到引人注目的需求時才使用多重繼承。但是,如果你這樣做,這是非常有用的。與例如相比,它可以防止相當醜陋的不對稱和代碼重複。像Java這樣的語言已經禁止了它。

我不是樹醫生。當我使用鏈鋸時,會危害我的腿。但這並不是說電鋸沒有用處。

在哪裏放假人:無處請,不要取消...

[編輯OP的第一個註釋後]

如果你得到的B類D1,將打印的「未實現setLength」如果你調用它的setLength方法,應該如何調用者作何反應?它不應該首先調用它,如果D1不是從具有這種方法的B派生的,那麼調用者可能已經知道這個方法,純粹是虛擬的。那麼很明顯,它不支持這種方法。擁有B基類讓D1感到賓至如歸的元素類型,B *或B &的多態數據結構承諾其用戶正確支持getLength,而不支持getLength。

儘管在你的例子中情況並非如此(但也許你遺漏了一些東西),但當然可以有一個很好的理由從B中推導出D1和D2。B可以保存最終接口的一部分或實現它的派生類D1和D2都需要。

假設B有一個方法setAny(key,value)(設置字典中的一個值),D1和D2都使用,D1在setColor中調用它,D2在setLength中調用它。 在這種情況下,使用通用基類是有道理的。在這種情況下,B不應該有虛擬方法setColor或setLength,既不是虛擬方法也不是純粹方法。你應該只在你的D1類中有一個setColor,在你的D2類中有一個setLength,但在你的B類中都沒有。

有一個在面向對象設計的基本原則:

不要剝奪繼承權

通過引入「方法,這是不適用」的具體類的概念,這正是你正在做。現在像這樣的規則不是教條。但違反這條規則幾乎總是指向一個設計缺陷。

所有B的一個數據結構是唯一有用的有他們這樣做一招,他們都明白...

[EDIT2 OP的第二COMENT後]

OP希望有一個地圖,可以容納來自B的任何類別的對象。

這正是問題出現的地方。要了解如何存儲指向我們對象的指針和引用,我們必須問:用於存儲的是什麼。如果一個地圖,比如說mapB被用來存儲指向B的指針,那麼必須有一個點。數據存儲的樂趣在於檢索數據並做一些有用的事情。

讓我們通過使用日常生活中的清單來簡化這一點。假設我有一個personList 1000人,每個人都有他們的全名和電話號碼。現在說我的廚房水槽有問題。我實際上可以通讀清單,打電話給每個人,問:你能修理我的廚房水槽嗎?換句話說:你是否支持repairKitchenSink方法。或者:你有沒有可能成爲水管工人的一個例子(你是水管工)。但是後來我花了很長時間打電話,或許經過500次電話會議後,我很幸運。

現在我personList上的所有1000人都支持talkToMe方法。所以,每當我感到孤獨時,我都可以打電話給任何人,並調用該人的talkToMe方法。但他們不應該都有修理KidchenSink方法,即使不是純粹的虛擬或虛擬變體也會做別的事情,因爲如果我將這種方法稱爲類Burglar的人,他可能會響應這個呼叫,但是在一個意外的方式。

所以類人不應該包含一個方法repairKitchenSink,即使不是一個純粹的虛擬人。因爲它不應該被稱爲personList迭代的一部分。在迭代plumberList時應該調用它。該列表僅保存支持repairKitchenSink方法的對象。

使用純虛函數只有在適當

他們可能會支持它以不同的方式,但。換句話說,在Plumber類中,方法repairKitchenSink可以例如是純粹的虛擬。有可能例如是2派生類,PvcPlumber和CopperPlumber。 CopperPlumber將通過調用lightFlame實現(代碼)repairKitchenSink方法,然後調用solderDrainToSink,而PvcPlumber將實現它作爲applyGlueToPvcTube和glueTubeToSinkOutlet的連續調用。但是這兩個水管工子類僅以不同的方式實施repairKitchenSink。這並且唯一證明在他們的基類水暖工中擁有純虛函數repairKitchenSink。當然,一個班級可能來自水管工,不會實施該方法,比如說WannabePlumber類。但是由於它是抽象的,你不能從它實例化對象,這很好,除非你想溼腳。

Person可能有很多不同的子類。他們例如代表不同的職業,或不同的政治偏好或不同的宗教。如果一個人是民主黨的Budhist Plumber,那麼他(M/F)可能在繼承民主黨,Budhist和水管工階級的衍生階級。使用繼承或者甚至爲政治偏好或宗教信仰,甚至職業以及這些組合的無盡組合這樣動盪的東西打字,在實踐中並不方便,但這僅僅是一個例子。在現實中,職業,宗教和政治偏好可能是屬性。但這並沒有改變這裏重要的一點。如果某個類是不支持某種操作的,那麼它不應該在數據結構中暗示它的作用。

除了personList,擁有plumberList,animistList和democratList之外,您一定會打電話給理解您的方法inviteBillToPlayInMyJazzBand或worshipTheTreeInMyBackyard的人。

列表不包含對象,它們只包含指向對象的指針或引用。所以我們的民主Budhist管道工被包含在personList,democratList,budhistList和plumberList中沒有任何問題。列表就像數據庫索引。不包含記錄,他們只是指它們。你可以在一個表上有許多索引,而且你應該這麼做,因爲索引很小,並且使你的數據庫更快。

對於多態數據結構也是如此。目前,即使personList,democratList,budhistList和plumberList變得如此之大以至於內存不足,解決方案通常不會只有一個personList。因爲那樣你就會遇到一個性能問題和一個代碼複雜性問題,而這個問題通常會變得更糟。

所以,回到你的評論:你說你想讓你所有的派生類都在B的列表中。很好,但是B的接口應該只包含爲列表中的所有內容實現的方法,所以沒有虛擬方法。這就像是通過圖書館並瀏覽所有書籍,尋找一種支持教學關於浮萍的方法。老實說,告訴你所有這些,我一直犯下一個資本罪。我一直在推銷一般真理。但在軟件設計中這些不存在。我一直在試圖將它們出售給你,因爲我已經教了30年的面向對象設計,而且我認爲我認識到了你卡在哪裏。但是對於每一條規則,都有很多例外。儘管如此,如果我已經正確地解決了你的問題,在這種情況下,我認爲你應該選擇單獨的數據結構,每個數據結構只保存對象的引用或指針,這些對象確實可以在你迭代特定數據結構時進行欺騙。

點是方形圓

部分的混亂中適當地使用多態數據結構(數據結構保持指針或引用不同的對象類型)來對關係數據庫的世界。 RDB與平面記錄表一起工作,每個記錄具有相同的字段。由於某些領域可能不適用,所以發明了一種叫做「約束」的東西。在C++類中,Point將包含字段x和y。 Class Circle可以繼承它,並且還包含字段'radius'。類Square也可以繼承Point,但除x和y之外還包含字段「side」。在RDB世界約束中,不是字段,是繼承的。因此,一個圓的約束半徑將爲== 0,而一個Square的約束點的值爲== 0.一個點將繼承兩個約束,所以它將滿足既是正方形又是圓的條件:一個點是一個正方形這在數學上確實是這樣。請注意,與C++相比,約束繼承層次結構是'顛倒'的。這可能會讓人困惑。

無論是普遍認爲繼承與專業化並駕齊驅,兩者都無濟於事。雖然通常情況下並不總是如此。在許多情況下,C++繼承是擴展而不是專業化。這兩者往往是一致的,但點,方,圓的例子表明,這不是一個普遍的事實。

如果使用繼承,在C++中Circle應該從Point派生,因爲它有額外的字段。但是Circle肯定不是一種特殊類型的Point,反之亦然。在許多實際的圖書館中,Circle將包含一個類Point的對象,持有x和y,而不是從它繼承,繞過了整個問題。

歡迎的設計選擇

你碰到了什麼,世界是一個真正的設計選擇,一個重要問題。仔細思考這樣的事情,就像你在做的一樣,在實踐中嘗試所有這些事情,包括所謂的「錯誤」,都會使你成爲程序員,而不是編碼員。

+0

謝謝。在這種情況下,我確實想要實例化D1,D2或M1中的任何一個。如果我使用抽象基類,那麼是否在派生類中爲那些不適用的函數放置了一個「虛擬」佔位符?我在想,如果我將這些虛擬例程放在頂層類中,我不必將它們放在派生類中。 – ark262

+0

我將編輯我的答案 –

+0

謝謝雅克。我將這些方法放在基類中的原因是因爲我想使用一個可以容納任何B或從B派生的類的映射。如果我然後嘗試使用映射迭代器調用setLength,它會失敗,因爲setLength不是基類中的方法。我是C++和OOD的新手,知道這有味道,但不知道如何解決它。我一直試圖使用裝飾模式(每個人似乎都說繼承是壞的),但我似乎遇到了同樣的問題。如果你有更多的時間來看這個,我可以發佈更多的代碼來包含容器的使用。 – ark262

0

讓我先狀態你正在嘗試做的是一個設計的氣味:最有可能你是什麼實際上試圖實現可以以更好的方式來實現。不幸的是,我們無法知道您實際上想要達到的目標,因爲您只告訴我們您想如何達到目標

但無論如何,你的實現是壞的,因爲這些方法報告「未實現」以用戶程序的,而不是向呼叫者。調用者無法對方法做出反應,而沒有按照預期做出反應。更糟的是,你甚至不會將它輸出到錯誤流中,而是輸出到常規輸出流中,所以如果你在任何產生常規輸出的程序中使用這個類,那輸出將會被你的錯誤信息中斷,可能會使程序混淆進一步在管道中)。

這裏有一個更好的方式來做到這一點:

#include <iostream> 
#include <cstdlib> // for EXIT_FAILURE 

//================================ 
class B { 

public: 
    virtual ~B() { } 

    void setVal(int val) { val_ = val; } 

    // note: No implementation of methods not making sense to a B  
private: 
    int val_; 

}; 

//================================ 
class D1 : virtual public B { 

public: 
    void setColor(int color) { 
     std::cout << "D1::setColor to " << color << std::endl; 
     color_ = color; 
    } 

private: 
    int color_; 

}; 

//================================ 
class D2 : virtual public B { 

public: 
    void setLength(int length) { 
     std::cout << "D2::setLength to " << length << std::endl; 
     length_ = length; 
    } 

private: 
    int length_; 

}; 

class M1 : public virtual D1, public virtual D2 { 

}; 


//================================ 
int main() { 

    B* d1 = new D1; 

    p->setVal(3); 
    if (D1* p = dynamic_cast<D1*>(d1)) 
    { 
    p->setColor(1); 
    } 
    else 
    { 
    // note: Use std::cerr, not std::cout, for error messages 
    std::cerr << "Oops, this wasn't a D1!\n"; 
    // Since this should not have happened to begin with, 
    // better exit immediately; *reporting* the failure 
    return EXIT_FAILURE; 
    } 

    B* m1 = new M1; 

    m1->setVal(4); 
    if (D2* p = dynamic_cast<D2*>(m1)) 
    { 
    p->setLength(2); 
    } 
    else 
    { 
    // note: Use std::cerr, not std::cout, for error messages 
    std::cerr << "Oops, this wasn't a D1!\n"; 
    // Since this should not have happened to begin with, 
    // better exit immediately; *reporting* the failure 
    return EXIT_FAILURE; 
    } 
    if (D1* p = dynamic_cast<D1*>(m1)) 
    { 
    p->setColor(4); 
    } 
    else 
    { 
    // note: Use std::cerr, not std::cout, for error messages 
    std::cerr << "Oops, this wasn't a D1!\n"; 
    // Since this should not have happened to begin with, 
    // better exit immediately; *reporting* the failure 
    return EXIT_FAILURE; 
    } 

    return 0; 
} 

或者,你可以利用的事實,你的方法共享一些一致性,並使用一個通用的方法來設置所有:

#include <iostream> 
#include <stdexcept> // for std::logic_error 
#include <cstdlib> 
#include <string> 

enum properties { propValue, propColour, propLength }; 

std::string property_name(property p) 
{ 
    switch(p) 
    { 
    case propValue: return "Value"; 
    case propColour: return "Colour"; 
    case propLength: return "Length"; 
    default: return "<invalid property>"; 
    } 
} 

class B 
{ 
public: 
    virtual ~B() {} 

    // allow the caller to determine which properties are supported 
    virtual bool supportsProperty(property p) 
    { 
    return p == propValue; 
    } 
    void setProperty(property p, int v) 
    { 
    bool succeeded = do_set_property(p,v); 
    // report problems to the _caller_ 
    if (!succeeded) 
     throw std::logic_error(property_name(p)+" not supported."); 
    } 
private: 
    virtual bool do_set_property(property p) 
    { 
    if (p == propValue) 
    { 
     value = v; 
     return true; 
    } 
    else 
     return false; 
    } 

    int value; 
}; 

class D1: public virtual B 
{ 
public: 
    virtual bool supportsProperty(property p) 
    { 
    return p == propColour || B::supportsProperty(p); 
    } 
private: 
    virtual bool do_set_property(property p, int v) 
    { 
    if (p == propColour) 
    { 
     colour = v; 
     return true; 
    } 
    else 
     return B::do_set_property(p, v); 
    } 

    int colour; 
}; 

class D2: public virtual B 
{ 
public: 
    virtual bool supportsProperty(property p) 
    { 
    return p == propLength || B::supportsProperty(p); 
    } 
private: 
    virtual bool do_set_property(property p, int v) 
    { 
    if (p == propLength) 
    { 
     length = v; 
     return true; 
    } 
    else 
     return B::do_set_property(p, v); 
    } 

    int length; 
}; 

class M1: public virtual D1, public virtual D2 
{ 
public: 
    virtual bool supportsProperty(property p) 
    { 
    return D1::supportsProperty(p) || D2::supportsProperty(p); 
    } 
private: 
    bool do_set_property(property p, int v) 
    { 
    return D1::do_set_property(p, v) || D2::do_set_property(p, v); 
    } 
}; 
+0

謝謝你的回覆。你知道在基類中是否有一個特定的成語名稱嗎? (所以我可以進一步研究這個想法) – ark262

+0

@ ark262:我不知道一個成語的名字,對不起。 – celtschk

+0

Dewhurst的「C++ Common Knowledge」似乎稱之爲「能力查詢」 – ark262

相關問題