2

我正在尝试建立一个程序,该程序可以根据对许多交易的总和生成资产负债表,并以如下格式显示结果:

资产负债表 这里的重要属性是顶级账户(如 Assets)被分解为子账户树,只有最低级账户(“叶子”)跟踪自己的余额(更高级别账户的余额为只是他们子账户余额的总和)。

我的首选方法是使用继承:

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_cast实现Account*to a 。第一个选项似乎稍微优雅一些​​,但事实上它需要我定义ChildAccount*addToBalance()ParentAccount::addToBalance()(尽管是一个错误)对我来说似乎有点奇怪。

我的问题是:这种尴尬是否有一个名称,以及解决它的标准方法,还是我完全误用了继承?

*注意:我认识到可能有一种更有效的方法来组织帐户以进行搜索,但我的主要目标是创建一个易于解释和调试的程序。根据我目前的理解水平,这是以计算效率为代价的(至少在这种情况下)。

4

3 回答 3

1

是的,你猜对了,这不是一个正确的继承案例。

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

清楚地表明这class ParentAccount : public Account是错误的:ParentAccount 与 Account 没有 IS-A 关系。

有两种方法可以解决:一种是取消继承ParentAccount。但getBalance()一致性表明这可能是过度反应。所以你可以addToBalance()Account(和ParentAccount)中排除,层次结构是正确的。

当然,这意味着您必须ChildAccount在调用 之前获取指针addToBalance(),但无论如何您都必须这样做。实用的解决方案有很多,例如,您可以简单地将两个向量 in ParentAccount,一个用于另一个 ParentAccounts,另一个用于 ChildAccounts,或者使用dynamic_cast,或者...(取决于您对帐户的其他操作)。

这种尴尬的名称是打破 LSP(Liskov 替换原则),或者更简单地说,打破 IS-A 关系。

于 2013-04-19T07:45:30.377 回答
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
};

如果你现在想遍历树来收集账户余额等,你可以直接在树结构上进行。这比通过父帐户路由更简单,更不容易混淆。此外,很明显,实际帐户和包含它们的树结构是不同的东西。

于 2013-04-19T08:35:16.177 回答
0

所以你有一棵树,它的节点有两种不同类型的节点,它们来自同一个基,你想对一种类型而不是另一种类型执行操作……这听起来像是访问者模式的工作。:)

访问者模式背后的想法是这样的:它为复杂结构(树、图)的元素提供了一种方法,可以根据它们的类型(仅在运行时知道)进行不同的操作,并且特定操作本身也可以仅在运行时才知道,而不必更改元素所属的整个层次结构(即避免您想到的 addToBalance 的“错误”函数实现之类的事情)。(^它与访问无关,所以它可能是错误的名称——它更多的是为不支持它的语言实现双重调度的一种方式。)

因此,您可以对元素执行一组操作,并且可以根据元素的类型对这些操作进行重载。一个简单的方法是为所有操作定义一个基类(我在下面将其称为访问者类)。它将包含的唯一内容是空函数 - 每种类型的元素都有一个可能执行操作的元素。这些函数将被特定操作覆盖。

class Visitor {

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

ChildAccount现在我们创建一个特定的类来仅在s上执行 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);
   }
};

请注意 Account 类中的 Accept() 函数,它只是将 Visitor* 作为参数并在 上调用该访问者的 Visit 函数this这就是魔法发生的地方。至此,类型和this类型_visitor都会被解析。如果this是 ChildAccount 类型并且_visitor是 类型AddToBalance,那么Visit将被调用的函数_visitor->Visit(this);将是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;
   } 
};

如果thisinvoid Account::Accept()是 a ParentAccount,则将调用空函数 Visitor::Visit(ParentAccount*),因为该函数未被 in 覆盖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;
}
于 2013-04-19T07:21:07.583 回答