Правильно ли реализован паттерн?
В коде ниже я попытался реализовать паттерн "команда" на примере банковской системы.
Основные вопросы:
- Правильно ли хранить операции и аккаунты в объекте банка? Кажется, что так, но это не совсем соответствует паттерну на мой взгляд
- Соответствует ли в целом код данному паттерну (и в общем здравому смыслу)? Что можно улучшить?
Весь код сразу (и с поправками)
В банковской системе есть:
- Интерфейс банковской операции (интерфейс команды в паттерне)
interface IAccountCommand
{
void Execute();
void Undo();
void SetStatus(BankTransactionStatus status);
BankTransactionStatus GetStatus();
string GetOperationInfo();
int GetOperationId();
}
- Класс клиента банка (получатель или исполнитель команды)
class BankAccount
{
public string? FullName;
public int balance = 0;
public int id;
public string GetAccountInfo()
{
return $"Account name: {FullName}, balance: {balance}, id: {id}";
}
}
- Класс операции списания/зачисления на баланс, реализующий интерфейс банковской операции (конкретная команда)
class ExecutePayment : IAccountCommand
{
public int amount;
public BankTransactionStatus status = BankTransactionStatus.Waiting;
public BankAccount? targetAccount;
public int OperationId;
public void SetTransactionAmount(int amount)
{
this.amount = amount;
}
public void SetTargetAccount(BankAccount account)
{
targetAccount = account;
}
public void Execute()
{
if (targetAccount != null)
{
int resultAmount = targetAccount.balance + amount;
if (resultAmount < 0)
{
throw new InsufficientFundsException();
}
else
{
targetAccount.balance = resultAmount;
}
}
else
{
throw new Exception("Account was not set. ");
}
}
public void Undo()
{
if (targetAccount != null)
{
targetAccount.balance -= amount;
}
else
{
throw new Exception("Account was not set. ");
}
}
public void SetStatus(BankTransactionStatus status)
{
this.status = status;
}
BankTransactionStatus IAccountCommand.GetStatus()
{
return status;
}
string GetOperationType()
{
if (amount > 0) return "Income";
else return "Outcome";
}
public string GetOperationInfo()
{
return $"\tOperationType: Payment\n\tPayment type: {GetOperationType()}\n\tOperation Amount: {Math.Abs(amount)}\n\tStatus: {status}\n\tid: {OperationId}\n";
}
int IAccountCommand.GetOperationId()
{
return OperationId;
}
}
Соответственно, в методе Execute()
проверяется, можно ли выполнить такую операцию, при возможности она выполняется, а метод Undo()
эту операцию отменяет (как и должна команда в соответствии с паттерном)
- Банковская операция по начислению процентов на баланс счета (с последующим сохранением начисленных процентов для отмены) (тоже конкретная команда в паттерне)
class AddPercent : IAccountCommand
{
public int percent;
public int? percentAmount;
public BankAccount? targetAccount;
BankTransactionStatus status;
public int OperationId;
public void SetAccrualProcent(int procent)
{
percent = procent;
}
public void SetTargetAccount(BankAccount account)
{
targetAccount = account;
}
public void Execute()
{
if (targetAccount != null)
{
int new_balance = (int)(targetAccount.balance * (percent + 100f) / 100f);
percentAmount = new_balance - targetAccount.balance;
targetAccount.balance = new_balance;
}
else
{
throw new Exception("Account was not set. ");
}
}
public void Undo()
{
if (targetAccount is not null)
{
if (percentAmount is null)
throw new Exception("Percent was not accrued. ");
else targetAccount.balance -= percentAmount.Value;
}
else
{
throw new Exception("Account was not set. ");
}
}
public void SetStatus(BankTransactionStatus status)
{
this.status = status;
}
BankTransactionStatus IAccountCommand.GetStatus()
{
return status;
}
public string GetOperationInfo()
{
return $"\tOperationType: Percent accrual\n\tAccrual procent: {percent}%\n\tStatus: {status}\n\tid: {OperationId}\n";
}
int IAccountCommand.GetOperationId()
{
return OperationId;
}
}
percent
хранит в себе процент для начисления (например, 5%), а percentAmount
хранит сумму, начисленную в рублях в результате операции, чтобы затем ее снять со счета в случае отмены. Остальное также, как и выше.
И теперь класс исполнителя в паттерне - банка.
class Bank
{
string? BankName;
IAccountCommand? Cmd;
readonly List<IAccountCommand> commands = [];
readonly List<BankAccount> accounts = [];
public void SetCommand(IAccountCommand cmd)
{
Cmd = cmd;
}
public void ExecuteCommand()
{
if (Cmd is null)
{
throw new Exception("Command was not set. ");
}
else if (CheckOperationExists(Cmd.GetOperationId()))
{
throw new Exception("Operation already executed. ");
}
else
{
int CurrentOperationIndex = commands.Count;
commands.Add(Cmd);
commands.ElementAt(CurrentOperationIndex).SetStatus(BankTransactionStatus.Processing);
try
{
commands.ElementAt(CurrentOperationIndex).Execute();
commands.ElementAt(CurrentOperationIndex).SetStatus(BankTransactionStatus.Accepted);
}
catch
{
commands.ElementAt(CurrentOperationIndex).SetStatus(BankTransactionStatus.Cancelled);
}
}
}
public void RevertOperation(int operationId)
{
foreach (var operation in commands)
{
if (operation.GetOperationId() == operationId)
{
operation.Undo();
operation.SetStatus(BankTransactionStatus.Reverted);
return;
}
}
throw new Exception("Operation does not exists. ");
}
public void SetName(string name)
{
BankName = name;
}
public void RegisterAccount(BankAccount account)
{
foreach (var account_ in accounts)
{
if (account_.id == account.id) throw new Exception("Id already exists.");
}
accounts.Add(account);
}
public BankAccount GetAccountById(int id)
{
foreach (var account in accounts)
{
if (account.id == id) return account;
}
throw new Exception($"Account with id={id} was not found");
}
public bool CheckOperationExists(int id)
{
foreach (var operation in commands)
{
if (operation.GetOperationId() == id) return true;
}
return false;
}
public int FindAccount(string accountName)
{
for (int i = 0; i < accounts.Count; i++)
{
BankAccount bankAccount = accounts[i];
if (bankAccount.FullName == accountName) return i;
}
return -1;
}
public string GetBankInfo()
{
string result = "";
result += $"Bank name: {BankName}, Total transactions: {commands.Count}, Total accounts: {accounts.Count}\n\n";
foreach (var Operation in commands)
{
result += Operation.GetOperationInfo() + "\n";
}
foreach (var account in accounts)
{
result += account.GetAccountInfo() + "\n";
}
return result;
}
}
В Cmd
хранится текущая операция для исполнения. Все предыдущие операции вместе с их результатами (отменена, отклонена, исполнена и тд) хранятся в List<IAccountCommand> commands
.
ExecuteCommand()
проверяет наличие всех данных, и при правильности отправляет команду на исполнение (меняя ее статус в ходе).
RevertOperation()
таким образом отменяет операцию по её id. Вообще, по паттерну нужно было бы отменять операцию, содержащуюся в Cmd
, но на мой взгляд лучше отменять её по айдишнику (я этот вопрос и задаю, чтобы узнать, "на мой взгляд" - это правильно?). Остальные функции вроде как понятны по названию.