Правильно ли реализован паттерн?

В коде ниже я попытался реализовать паттерн "команда" на примере банковской системы.

Основные вопросы:

  1. Правильно ли хранить операции и аккаунты в объекте банка? Кажется, что так, но это не совсем соответствует паттерну на мой взгляд
  2. Соответствует ли в целом код данному паттерну (и в общем здравому смыслу)? Что можно улучшить?

Весь код сразу (и с поправками)

В банковской системе есть:

  • Интерфейс банковской операции (интерфейс команды в паттерне)
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, но на мой взгляд лучше отменять её по айдишнику (я этот вопрос и задаю, чтобы узнать, "на мой взгляд" - это правильно?). Остальные функции вроде как понятны по названию.


Ответы (0 шт):