Может, я неправильно распорядился наследством?
Я пытаюсь настроить программу, которая может генерировать балансовые отчеты на основе суммирования ряда транзакций и представлять результаты в следующем формате:
Важными атрибутами здесь являются то, что счет верхнего уровня (например, активы) раскладывается на дерево субсчетов, и только счета нижнего уровня ("листья") отслеживают свои собственные балансы (балансы счетов более высокого уровня-это просто суммы балансов их субсчетов).
Мой go-to подход заключается в использовании наследования:
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*
к ChildAccount*
перед вызовом addToBalance()
. Первый вариант кажется немного более элегантным, но тот факт, что он требует от меня определения ParentAccount::addToBalance()
(хотя и как ошибка), кажется мне немного странным.
*Примечание: я признаю, что, вероятно, существует более эффективный способ организации счетов для поиск, но моя основная цель-создать программу, которая интуитивно понятна для интерпретации и отладки. Исходя из моего нынешнего уровня понимания, это происходит за счет вычислительной эффективности (в данном случае, по крайней мере).
3 ответа:
Да, вы правильно угадали, что это не правильный случай наследования.
virtual void addToBalance(int amount) { cout << "Error: Cannot modify balance of a parent account" << endl; exit(1); }
Ясно указывает на то, что
Есть два способа исправить это: один-лишить наследстваclass ParentAccount : public Account
неверно: ParentAccount не имеет отношения к счету.ParentAccount
. Но последовательность показывает, что это может быть чрезмерной реакцией. Таким образом, вы можете просто исключитьaddToBalance()
изAccount
(иParentAccount
), и иерархия будет правильной.Конечно, это будет означать, что вам придется получить указатель
Названия этой неловкости-ломка ЛСП (Принцип подстановки Лискова), или, проще говоря, ломка ИС-отношения.ChildAccount
Перед тем, как звонюaddToBalance()
, но ты все равно должен это сделать. Практические решения многочисленны, например, вы можете просто иметь два вектора вParentAccount
, один для других родительских счетов, другой для дочерних счетов, или использоватьdynamic_cast
, или... (зависит от того, что еще вы должны сделать со счетами).
Таким образом, у вас есть дерево, узлы которого имеют два различных типа, производных от одного и того же основания, и вы хотите выполнить операцию над одним типом, но не над другим ... это похоже на работу для шаблона посетителя. :)
Идея паттерна visitor^ заключается в следующем: он обеспечивает возможность для элементов сложных структур (деревьев, графов) работать по-разному в зависимости от их типа (который известен только во время выполнения), и где сама конкретная операция также может быть известна только во время выполнения. runtime, без необходимости изменять всю иерархию, к которой принадлежат элементы (т. е. избегать таких вещей, как реализация функции "ошибка" addToBalance, о которой вы подумали). (^Он имеет мало общего с посещением, поэтому, вероятно, неправильно назван - это скорее способ достижения двойной отправки для языков, которые изначально его не поддерживают.)Таким образом, вы можете иметь набор операций для выполнения над элементами, и операции могут быть, например, перегружены в зависимости от типа элемента. элемент. Простой способ сделать это-определить базовый класс для всех операций (ниже я называю его классом посетителя). Единственное, что он будет содержать, - это пустые функции - по одной для каждого типа элемента, над которым потенциально может быть выполнена операция. Эти функции будут переопределены конкретными операциями.
class Visitor { virtual void Visit(ParentAccount*) { /* do nothing by default*/ } virtual void Visit(ChildAccount*) { /* do nothing by default */ } };
Теперь мы создаем специальный класс для выполнения AddToBalance только на
ChildAccount
s.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; };
Некоторые изменения в исходном классе учетной записи.
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); } };
Обратите внимание на Функция Accept() в классе Account, которая просто принимает посетителя* в качестве аргумента и вызывает функцию посещения этого посетителя на
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; } };
Если бы
Теперь нам больше не нужно определять функцию addToBalance в ParentAccount:this
вvoid Account::Accept()
было aParentAccount
, то пустая функцияVisitor::Visit(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); };
Самая забавная часть-это определение вашего собственного посетителя, который делает гораздо более сложным операции (например, накопление сумм остатков только по всем дочерним счетам):
class CalculateBalances : public Visitor { void Visit(ChildAccount* _child) { balanceSum += _child->getBalance(); } int CumulativeSum() {return balanceSum; } int balanceSum; }
Концептуально, у вас нет дочерних и родительских учетных записей, но учетные записи и дерево объектов, из которых конечные узлы содержат указатель на фактические учетные записи.
Я бы предложил вам непосредственно представить эту структуру в коде:
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 };
Если теперь вы хотите пересечь дерево для сбора остатков на счетах и т. д., Вы можете сделать это непосредственно в структуре дерева. Это проще и менее запутанно, чем использование маршрута через родительские учетные записи. Кроме того, ясно, что фактическое учетные записи и древовидная структура, содержащая их, - это разные вещи.