Доступ из класса состояния к частным полям класса контекста
Меня смущает реализация паттерна состояний. В соответствии с этим шаблоном мы должны выделить управление состоянием в отдельные классы. На первый взгляд, это позволяет избежать больших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
методы. Сделайте так, чтобы ваши методы возвращали соответствующие значения, и вам будет легче создавать классы, которые взаимодействуют естественно.