Доступ из класса состояния к частным полям класса контекста

Меня смущает реализация паттерна состояний. В соответствии с этим шаблоном мы должны выделить управление состоянием в отдельные классы. На первый взгляд, это позволяет избежать большихif ... else ...конструкции внутри предметной области и это действительно мощное преимущество. Мы можем переместить все проверки условий в классы состояний и очистить класс сущности предметной области.

Но как изменить данные, инкапсулированные в доменном объекте, без нарушения принципа инкапсуляции?

Например, рассмотрим Accountорганизация. Упрощенно, у него есть два возможных состояния:Active а также Blocked и способы ввода и вывода денег.

Согласно модели состояния, мы должны делегировать ответственность за внесение и снятие средств государственным классам. Схема UML здесь.

Но как мы можем изменить money а также state поля из AccountStateреализации? Я вижу только путь там, где у меня есть публичные сеттеры. Но это нарушает принцип инкапсуляции. При таком подходе я также могу сделать частные поля общедоступными.

Пример кода:

class Account {
  private int money;
  private AccountState state;

  public Account() {
    this.money = 0;
    this.state = new Active();
  }

  public void deposit(int amount) {
    this.state.deposit(this, amount);
  }

  public void withdraw(int amount) {
    this.state.withdraw(this, amount);
  }

  public int getMoney() {
    return this.money;
  }

  public AccountState getState() {
    return this.state;
  }
}
interface AccountState {
  public void deposit(Account account, int amount);
  public void withdraw(Account account, int amount);
}
class Active implements AccountState {
  public void deposit(Account account, int amount) {
    // How to change account money and state without setters and public fields usage?
  }
  
  public void withdraw(Account account, int amount) {
    if (account.getState() instanceof Blocked) {
      throw new RuntimeException("Money could not be withdrawn. Account is blocked.");
    }

    if (account.getMoney() - amount <= 0) {
      throw new RuntimeException("Money could not be withdrawn. Insufficient funds.");
    }

    // How to change account money and state without setters and public fields usage?
  }
}

class Blocked implements AccountState {
  public void deposit(Account account, int amount) {
    // How to change account money and state without setters and public fields usage?
  }

  public void withdraw(Account account, int amount) {
    if (account.getMoney() - amount <= 0) {
      throw new RuntimeException("Money could not be withdrawn. Insufficient funds.");
    }

    // How to change account money and state without setters and public fields usage?
  }
}

Это очень упрощенный пример, но он хорошо отражает мою проблему. К сожалению, мне не удалось найти для этого подходящего решения. Все примеры, которые я видел, используют либо публичные сеттеры, либо публичные поля. Также я видел пример изRefactoring to Patternsкнига Джошуа Кериевского. Он предлагает использовать сеттеры с доступом на уровне пакета (без модификаторов доступа, таких какprivate, public, или protected). Таким образом, мы можем изменять данные сущности из классов состояния, находящихся в одном пакете с сущностью домена, и не можем делать это из других пакетов. Но этот подход использует специфичную для языка функцию - доступ на уровне пакета. На других языках, таких как PHP, это не сработает. Ищу концептуальное решение.

Может ли кто-нибудь показать реальный производственный пример решения этой проблемы? Я был бы очень признателен.

4 ответа

Чтобы разрешить вызовы только из определенных классов, вы можете использовать отражение.

Пример на Java: как получить класс вызывающего абонента в Java

Пример в PHP: /questions/30863307/kak-poluchit-imya-vyizyivayuschego-klassa-v-php/30863323#30863323

Я бы либо:

  • Переехать money в AccountState (как AccountState в основном работает с money в этом примере)
  • Предоставьте метод, который манипулирует Accountтак, как вы предписываете. Это может быть с помощью такого метода, какAccount#transact(String label, double amount), позволяя манипулировать балансом, не открывая стержень.
  • Удалите AccountState как избыточный класс, поскольку поля класса должны представлять состояние объекта.

Второе можно сделать и через API функции, но не путайте изменчивость члена класса с нарушением инкапсуляции; цель инкапсуляции - запретить нежелательное поведение (например, произвольные вычисления или доступ к внутренним коллекциям). Это предотвращает переход класса в ошибочное состояние.

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

Есть много способов решить эту проблему, в зависимости от того, что вам нужно делать с каждым экземпляром состояния. В этом конкретном примере я бы передал значение поляmoney в AccountState а не весь Account объект.

Вот пример использования перечисления, но, очевидно, это могут быть два отдельных класса с интерфейсом.

public class Account {
    private int balance = 0;
    private AccountState currentState = AccountState.ACTIVE;

    public int deposit(int amount) {
        balance = currentState.deposit(balance, amount);
        return balance;
    }

    public int withdraw(int amount) {
        balance = currentState.withdraw(balance, amount);
        return balance;
    }

    public AccountState activate() {
        this.currentState = AccountState.ACTIVE;
        return currentState;
    }

    public AccountState block() {
        this.currentState = AccountState.BLOCKED;
        return currentState;
    }

    enum AccountState {
        ACTIVE {
            @Override int deposit(int balance, int amount) {
                return balance + amount;
            }
            @Override int withdraw(int balance, int amount) {
                int newBalance = balance - amount;
                if (newBalance >= 0) {
                    return newBalance;
                }
                throw new IllegalArgumentException("Withdrawal amount is greater than balance.");
            }
        },
        BLOCKED {
            @Override int deposit(int balance, int amount) {
                throw new UnsupportedOperationException("Account is blocked.");
            }
            @Override int withdraw(int balance, int amount) {
                throw new UnsupportedOperationException("Account is blocked.");
            }
        };
        abstract int deposit(int balance, int amount);
        abstract int withdraw(int balance, int amount);
    }
}

Одним из признаков того, что код в OP будет сложно применить шаблоны ООП, является то, что методы бизнес-логики (deposit а также withdraw) возвращение void. Сложно делать что-либо кроме процедурного программирования сvoidметоды. Сделайте так, чтобы ваши методы возвращали соответствующие значения, и вам будет легче создавать классы, которые взаимодействуют естественно.

Другие вопросы по тегам