W3docs

Инкапсуляция в 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(...).
  • Не возвращайте изменяемые внутренние объекты напрямую. Оборачивайте, копируйте или используйте иммутабельное представление.
  • Валидируйте на входе. Отклоняйте недопустимое состояние на границе, чтобы внутренний код мог считать данные корректными.

Рабочий пример

java— editable, runs on the server

Что дальше

Механическая часть инкапсуляции — private-поля с public-методами для чтения и записи — имеет собственные соглашения, которые стоит знать. Переходите к главе о геттерах и сеттерах.

Практика

Практика
Почему механическая генерация публичного геттера и публичного сеттера для каждого private-поля нивелирует смысл инкапсуляции?
Почему механическая генерация публичного геттера и публичного сеттера для каждого private-поля нивелирует смысл инкапсуляции?
Was this page helpful?