Инкапсуляция в Java
Объединяйте данные и методы в классах Java, скрывая детали реализации с помощью private-полей и публичных методов доступа.
Инкапсуляция — первый из четырёх принципов ООП и самый простой в реализации: данные класса остаются private, а поведение открывается через методы. Класс сам отвечает за своё состояние — внешний код не может привести его в недопустимый вид, — а остальная программа взаимодействует с ним через управляемый вами интерфейс.
Механизм — это private-поля плюс public-методы. Принцип — «не открывай состояние, которое не можешь контролировать».
Паттерн
Неинкапсулированный класс — просто набор публичных полей:
public class Account {
public int balance;
}
Account a = new Account();
a.balance = 100;
a.balance = -50; // nothing stops thisИнкапсулированная версия скрывает поле и предоставляет явные операции:
public class Account {
private int balance;
public int balance() { return balance; }
public void deposit(int amount) {
if (amount <= 0) throw new IllegalArgumentException();
balance += amount;
}
public boolean withdraw(int amount) {
if (amount <= 0) throw new IllegalArgumentException();
if (amount > balance) return false;
balance -= amount;
return true;
}
}Теперь внешний код никак не может установить balance в отрицательное значение, перезаписать его в обход валидации или прочитать, не вызвав balance().
Что вы получаете
Инварианты, которым можно доверять. Класс Account гарантирует «balance ≥ 0 после каждой публичной операции». Поскольку снаружи никто не может обратиться к balance, правило обеспечивается из одного файла, а не из всей кодовой базы.
Свободу изменений. При private-поле balance вы можете впоследствии сменить его тип с int на long, изменить единицу с долларов на центы, хранить в BigDecimal или перенести в базу данных — не трогая ни одного вызывающего кода. С публичным полем тип и хранение жёстко закреплены в каждом месте использования.
Меньший и понятный API. Вызывающий код видит только deposit, withdraw, balance — но не дюжину private-вспомогательных методов, которые за этим стоят. Класс объявляет что он делает, а не как.
Локальность рассуждений. Когда с balance что-то идёт не так, ошибка находится в одном из трёх методов, а не в любом из тысяч мест, которые иначе могли бы записать это поле.
Инкапсуляция ≠ геттеры и сеттеры для всего
Распространённый антипаттерн — механически генерировать геттер и сеттер для каждого поля:
public class Account {
private int balance;
public int getBalance() { return balance; }
public void setBalance(int v) { this.balance = v; } // same as public field, with extra steps
}Это технически «инкапсулировано» в учебном смысле, но не даёт ни одного реального преимущества инкапсуляции — любой по-прежнему может привести объект в произвольное состояние. Настоящая инкапсуляция выражает операции, а не сырой доступ к полям:
deposit(amount)вместоsetBalance(balance + amount)withdraw(amount)с возвратом успеха/неудачи вместоsetBalance(balance - amount)balance()(аксессор только для чтения) без соответствующегоsetBalance
В следующей главе о геттерах и сеттерах подробно рассмотрены соглашения о том, когда каждый из них уместен.
Защитные копии
Если тип поля является изменяемым (массив, список, Date), прямой возврат открывает внешний доступ:
public class Order {
private final List<String> items = new ArrayList<>();
public List<String> items() { return items; } // leak!
}
Order o = new Order();
o.items().add("apple"); // outside code mutated the orderРешение — возвращать немодифицируемое представление или защитную копию:
public List<String> items() {
return List.copyOf(items); // immutable snapshot
}Та же осторожность применяется к сеттерам, принимающим изменяемые значения, — копируйте их на входе:
public Order(List<String> items) {
this.items = new ArrayList<>(items);
}Глава об иммутабельных классах рассматривает это подробнее.
Выбор правильного уровня доступа
private и public используются чаще всего, но в Java есть четыре уровня, и промежуточные имеют значение для инкапсуляции:
private— виден только внутри того же класса. По умолчанию применяется к полям и вспомогательным методам.- package-private (без ключевого слова) — виден другим классам того же пакета. Полезен, когда несколько взаимодействующих классов образуют единый модуль и должны видеть внутренние детали друг друга, но снаружи это не нужно.
protected— package-private плюс виден подклассам. Используйте для членов, которые подкласс действительно должен переопределить или на которых строится.public— виден везде. Это ваш опубликованный API; как только внешний код начинает зависеть от него, изменение нарушает всех вызывающих.
Эмпирическое правило — принцип минимального раскрытия: давайте каждому члену наименьший доступ, при котором код всё ещё работает, и расширяйте его только при конкретной необходимости. Публичное поле или метод — это обещание, которое нужно держать. Полную таблицу см. в главе о модификаторах доступа.
Инкапсуляция в проектировании
При определённом масштабе инкапсуляция становится инструментом проектирования, а не просто правилом написания кода. Каждый класс обозначает границу вокруг части состояния и операций над ним; остальная система взаимодействует с ним через эту границу. Большинство архитектурных паттернов — слоистая архитектура, гексагональная, MVC, Clean — суть аргументы о том, где проводить эти границы.
Практические рекомендации:
- Поля private, пока не доказано обратное. Начинайте с этого; расширяйте доступ только по причине.
- Открывайте глаголы, а не существительные.
cancel(),pay(),ship()лучше, чемsetStatus(...). - Не возвращайте изменяемые внутренние объекты напрямую. Оборачивайте, копируйте или используйте иммутабельное представление.
- Валидируйте на входе. Отклоняйте недопустимое состояние на границе, чтобы внутренний код мог считать данные корректными.
Рабочий пример
Что дальше
Механическая часть инкапсуляции — private-поля с public-методами для чтения и записи — имеет собственные соглашения, которые стоит знать. Переходите к главе о геттерах и сеттерах.