2013-04-19 82 views
2

我試圖建立一個程序,可以基於總結了一些交易的資產負債表,並在這樣的格式呈現結果:我誤用了繼承嗎?

balance sheet 重要屬性這裏是頂級帳戶(如資產)被分解成子賬戶的一棵樹,只有最低級帳戶(「葉」)跟蹤自己的餘額(更高級別的賬戶餘額及其子賬戶的餘額只總和)。

我去到的方法是使用繼承:

class Account{ 
    string name; 
    virtual int getBalance() =0; //generic base class has no implementation 
    virtual void addToBalance(int amount) =0; 
}; 
class ParentAccount : public Account{ 
    vector<Account*> children; 
    virtual int getBalance() { 
     int result = 0; 
     for (int i = 0; i < children.size(); i++) 
      result += children[i]->getBalance(); 
     return result; 
    } 
    virtual void addToBalance(int amount) { 
     cout << "Error: Cannot modify balance of a parent account" << endl; 
     exit(1); 
    } 
}; 
class ChildAccount : public Account{ 
    int balance; 
    virtual int getBalance() { return balance; } 
    virtual void addToBalance(int amount) {balance += amount;} 
}; 

的想法是,該賬戶存在在編譯時是不知道,所以必須動態生成的樹賬戶。繼承是在這裏幫助,因爲它可以很容易地生成任意深度的樹結構(ParentAccounts可以有孩子這是ParentAccounts),因爲它可以很容易地使用遞歸實現像getBalance()功能。

當我嘗試合併派生類所特有的功能時,比如修改餘額(這應該只適用於ChildAccount對象,因爲ParentAccount餘額僅由它們的餘額定義兒童)。我的計劃是,像processTransaction(string accountName, int amount)功能將通過樹狀結構尋找具有正確名稱的帳戶進行搜索,然後在該帳戶調用addToBalance(amount)(*注意:以下)。由於上面的樹結構只允許我找到Account*,因此可能需要執行addToBalance(amount)(如上所述)或dynamic_castAccount*ChildAccount*,然後再調用addToBalance()。第一個選項似乎稍微優雅,但它需要我這樣的事實來定義ParentAccount::addToBalance()(雖然爲錯誤),似乎有點怪我。

我的問題是:是否有一個名字爲這個尷尬,以及解決它的標準方法,還是我只是完全誤用繼承?

*注:我承認有可能是組織的賬戶用於搜索的更有效的方式,但我的首要目標是創建一個程序,它是直觀的解釋和調試。根據我目前的理解水平,這是以計算效率爲代價的(至少在這種情況下)。

+1

調用'addToBalance'時必須出現該錯誤的事實是一個很大的信號,表明您正在使用繼承不正確。 「ParentAccount」顯然不是真正的「賬戶」。 –

回答

1

是的,你猜對了,它不是一個正確的繼承案例。

virtual void addToBalance(int amount) { 
    cout << "Error: Cannot modify balance of a parent account" << endl; 
    exit(1); 
} 

清楚地表明,class ParentAccount : public Account是錯誤的:ParentAccount沒有IS-A與客戶的關係。

有兩種方法來解決這個問題:一是剝奪繼承權ParentAccount。但getBalance()一致性表明它可能是過度反應。所以你可以從Account(和ParentAccount)排除addToBalance(),並且層次結構是正確的。

當然,這意味着你必須在致電addToBalance()之前獲得ChildAccount指針,但無論如何你必須這樣做。實際的解決方案很多,例如您可以簡單地在ParentAccount中有兩個向量,一個用於另一個ParentAccounts,另一個用於ChildAccounts,或者使用dynamic_cast或...(取決於您還需要對帳戶進行哪些操作)。

這種尷尬的名字打破了LSP(Liskov替代原則),或者更簡單地說,打破了IS-A關係。

0

因此,你有一棵樹,其節點有兩個不同的類型派生自同一個基地,並且你想對一個類型執行一個操作,但不是另一個類型......這聽起來像訪問者模式的工作。 :)

訪客^模式背後的想法是這樣的:它爲複雜結構(樹,圖)的元素根據其類型(僅在運行時才知道)進行不同操作提供了一種方式,特定操作本身也可以僅在運行時才知道,而不必更改元素所屬的整個層次結構(即,避免像您想到的那樣執行addToBalance的「錯誤」函數實現)。 (^這與訪問很少有關係,所以它可能被錯誤地命名 - 它更多的是實現對原本不支持它的語言進行雙重調度的方式。)

因此,您可以擁有一組操作來執行在元素上,操作可以是例如基於元素的類型重載。一個簡單的方法是爲所有操作定義一個基類(我稱之爲下面的Visitor類)。它將包含的唯一東西是空函數 - 對於可能在其上執行操作的每種類型的元素都有一個函數。這些功能將被特定的操作覆蓋。

class Visitor { 

    virtual void Visit(ParentAccount*) { /* do nothing by default*/ } 
    virtual void Visit(ChildAccount*) { /* do nothing by default */ } 
}; 

現在我們創建一個特定的類,僅在ChildAccount上執行AddToBalance。

class AddToBalance : public Visitor { 

    public: 
    AddBalance(string _nameOfTarget, int _balanceToAdd) : 
    nameOfTarget(_nameOfTarget), balanceToAdd(_balanceToAdd) {} 

    void Visit(ChildAccount* _child) { //overrides Visit only for ChildAccount nodes 
    if(child->name == _name) 
     child->addToBalance(_balance); //calls a function SPECIFIC TO THE CHILD 
    } 

    private: 
    string nameOfTarget; 
    int _balanceToAdd; 
}; 

對原來的Account類進行了一些更改。

class Account{ 
    vector<Account*> children; //assume ALL Account objects could have children; \ 
           //for leaf nodes (ChildAccount), this vector will be empty 
    string name; 
    virtual int getBalance() =0; //generic base class has no implementation 

    //no addToBalance function! 

    virtual void Accept(Visitor* _visitor) { 
    _visitor->Visit(this); 
    } 
}; 

注意在帳戶類,它只是需要觀衆*作爲參數,並呼籲this訪問者的訪問功能的接受()函數。 這就是魔術發生的地方。此時,this的類型以及_visitor的類型將被解決。如果this是ChildAccount類型,並且_visitor的類型是AddToBalance,則將在_visitor->Visit(this);中調用的Visit函數將爲void AddToBalance::Visit(ChildAccount* _child)

這恰好叫_child->addToBalance(...);

class ChildAccount : public Account{ 
    int balance; 
    virtual int getBalance() { return balance; } 
    virtual void addToBalance(int amount) { 
    balance += amount; 
    } 
}; 

如果thisvoid Account::Accept()當過ParentAccount,那麼空函數 Visitor::Visit(ParentAccount*)會被稱爲,因爲該功能不AddToBalance覆蓋。

現在,我們不再需要在ParentAccount定義一個addToBalance功能:

class ParentAccount : public Account{ 
    virtual int getBalance() { 
     int result = 0; 
     for (int i = 0; i < children.size(); i++) 
      result += children[i]->getBalance(); 
     return result; 
    } 
    //no addToBalance function 
}; 

第二個最有趣的部分是這樣的:因爲我們有一棵樹,我們可以定義一個訪問序列的通用功能,它決定以何種順序訪問樹的節點:

void VisitWithPreOrderTraversal(Account* _node, Visitor* _visitor) { 
    _node->Accept(_visitor); 
    for(size_t i = 0; i < _node->children.size(); ++i) 
    _node->children[i]->Accept(_visitor); 

} 

int main() { 
    ParentAccount* root = GetRootOfAccount(...); 

    AddToBalance* atb = new AddToBalance("pensky_account", 500); 
    VisitWithPreOrderTraversal(atb, root); 

}; 

最有趣的部分是定義你自己的觀衆,做更復雜的操作(如累計只有所有ChildAccounts的餘額的總和):

class CalculateBalances : public Visitor { 

    void Visit(ChildAccount* _child) { 

    balanceSum += _child->getBalance(); 

    } 
    int CumulativeSum() {return balanceSum; } 
    int balanceSum; 
} 
+0

感謝您的徹底迴應!我不確定這會不會解決我的尷尬。在我的實現中,編譯器在調用'ParentAccount'上的AddToBalance()方法時沒有問題,儘管這實際上是一個無效操作;在你的實現中,一個AddToBalance對象可以訪問一個ParentAccount。無論採用哪種方法,我都需要決定我是否足夠信任自己,不會意外地調用無效函數,如果不是,我需要編寫一個特殊函數來跳出錯誤。當然,什麼都不做就好辦了:) – user1476176

+0

不,謝謝!我實際上有我自己的[關於「瞭解訪問者地址的問題」)(http://www.artima.com/cppsource/top_cpp_aha_moments.html)中的自己的[哈哈薩特時刻],這激發了答案。 :) – maditya

+0

至於讓編譯器告訴你什麼是錯的,你可以通過使訪問者基類具有純虛擬功能(以重新實現每個派生訪問者類中的每一個函數爲代價)來修改上述內容。另外(或另外),你可以只有兩個派生訪問者類 - 一個用於收集每個ParentAccount的指針,另一個用於收集ChildAccount的指針。一旦收集到指針,你就可以通過調用相關函數來處理它們。該模式的要點是保持_structure_(樹)的層次結構不變,但您將新的_operations_添加爲新訪問者。 – maditya

0

從概念上講,您沒有子帳戶和父帳戶,但帳戶和對象樹的葉節點包含指向實際帳戶的指針。

我會建議你在代碼中直接表示這種結構:

class Account 
{ 
public: 
    int getBalance(); 
    void addToBalance(int amount); 
// privates and implementation not shown for brevity 
}; 


class TreeNode 
{ 
public: 
    // contains account instance on leaf nodes, and nullptr otherwise. 
    Account* getAccount(); 

    // tree node members for iteration over children, adding/removing children etc 

private: 
    Account* _account; 
    SomeContainer _children 
}; 

如果你現在要遍歷樹收集賬戶餘額等,可以直接做到這一點的樹結構。這是更簡單和更少混淆,採取路線通過父母帳戶。另外,很明顯,實際帳戶和包含它們的樹結構是不同的東西。

+0

我喜歡這個,但是在我的腦海裏,有一個'TreeNode',它的子節點是'TreeNode's和子節點'Account',而不是每個'Node'只允許一個'Account'。 – user1476176

+0

事實上,你也可以爲每個節點創建多個帳戶 - 只需用容器,std :: vector等替換帳戶指針,然後轉發迭代器函數即可。 – Wilbert