2013-04-19 54 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