Я неправильно применил наследство?

Я пытаюсь настроить программу, которая может генерировать балансы на основе суммирования количества транзакций и представлять результаты в таком формате:

баланс
Важными атрибутами здесь являются то, что учетная запись верхнего уровня (например, активы) раскладывается в дерево субсчетов, и только учетные записи самого низкого уровня («листья») отслеживают свои собственные остатки (остатки учетных записей верхнего уровня только суммы остатков на их субсчетах).

Мой подход заключается в использовании наследования:

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() (хотя и как ошибка) кажется мне немного странным.

У меня вопрос: есть ли название этой неловкости и стандартный подход к ее решению, или я просто полностью неправильно использую наследство?

*Заметка: Я понимаю, что, возможно, существует более эффективный способ организации учетных записей для поиска, но моя основная задача — создать программу, которая будет интуитивно понятна для интерпретации и отладки. Исходя из моего текущего уровня понимания, это достигается ценой вычислительной эффективности (по крайней мере, в этом случае).

2

Решение

Да, вы правильно догадались, что это не правильный случай наследования.

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() последовательность показывает, что это может быть чрезмерной реакцией. Так что вы можете просто исключить addToBalance() от Account (а также ParentAccount) и иерархия будет правильной.

Конечно, это будет означать, что вам придется получить ChildAccount указатель перед вызовом addToBalance(), но ты все равно должен это сделать. Практических решений много, например, Вы могли бы просто иметь два вектора в ParentAccountодин для другого ParentAccounts, другой для ChildAccounts или используйте dynamic_castили … (зависит от того, что еще вы должны делать с учетными записями).

Имена этой неловкости ломают LSP (принцип подстановки Лискова) или, проще говоря, ломают отношения IS-A.

1

Другие решения

Итак, у вас есть дерево, узлы которого имеют два разных типа, происходящих из одной и той же базы, и вы хотите выполнить операцию с одним типом, но не над другим … это звучит как работа для шаблона посетителя. 🙂

Идея шаблона «посетитель ^» заключается в следующем: он предоставляет способ для элементов сложных структур (деревьев, графиков) работать по-разному в зависимости от их типа (который известен только во время выполнения), и где сама конкретная операция может также быть известным только во время выполнения, без необходимости изменять всю иерархию, к которой принадлежат элементы (т.е. избегать таких вещей, как реализация функции addToBalance «ошибка», о которой вы думали). (^ Это не имеет ничего общего с посещением, поэтому его, вероятно, неправильно назвали — это скорее способ достижения двойной диспетчеризации для языков, которые изначально не поддерживают его.)

Таким образом, вы можете иметь набор операций для выполнения над элементами, и операции могут быть, например, перегружен в зависимости от типа элемента. Простой способ сделать это — определить базовый класс для всех операций (ниже я называю его классом Visitor). Единственное, что он будет содержать, — это пустые функции — по одной для каждого типа элемента, над которым потенциально может быть выполнена операция. Эти функции будут переопределены конкретными операциями.

class Visitor {

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

Теперь мы создаем определенный класс для выполнения AddToBalance ChildAccountтолько с

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, которая просто принимает 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;
}
};

Если this в void Account::Accept() был ParentAccountтогда пустая функция
Visitor::Visit(ParentAccount*) был бы вызван, так как эта функция не переопределяется в AddToBalance,

Теперь нам больше не нужно определять функцию addToBalance в ParentAccount:

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;
}
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
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector