5. Основные принципы ООП. Инкапсуляция.

Инкапсуляция

Инкапсуляция – один из основополагающих принципов ООП. Инкапсуляция – это одна из причин, почему так широко используется ООП.

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

Инкапсуляция – это процесс отделения друг от друга элементов объекта, определяющих его устройство и поведения; инкапсуляция служит для того, чтобы изолировать контрактные обязательства от их реализации.

Программистов можно разделить на создателей классов (те, кто создает новые типы данных) и на потребителей классов (они используют уже кем-то ранее созданные классы для своих целей).

Цель потребителей классов – как можно быстрее написать программу, используя уже кем-то ранее созданные классы (как кубики в конструкторе).

Цель создателей классов – построить класс, открывающий только то, что необходимо программисту-клиенту, и скрывающий все остальное.

Почему так? Программист-клиент не сможет получить доступ к скрытым частям, а значит, создатель классов оставляет за собой возможность произвольно их изменять, не опасаясь, что это кому-то повредит. Скрытая часть обычно и самая «хрупкая» часть объекта, которую легко можно испортить неосторожный или несведущий программист-клиент, поэтому сокрытие сокращает количество ошибок в программах.

Создавая классы, вы устанавливаете отношения с программистом-клиентом. Если предоставить доступ ко всем членам класса кому угодно, программист-клиент сможет сделать с классом и нарушить логику его работы. Таким образом, первой причиной ограничения доступа является необходимость уберечь «хрупкие» детали от программиста-клиента – части внутренней кухни, не являющиеся составляющими интерфейса, при помощи которого пользователи решают свои задачи. На самом деле это полезно и пользователям – они сразу увидят, что для них важно, а на что можно не обращать внимания.

Вторая причина ограничения доступа – стремление позволить разработчику классов изменять внутренние механизмы класса, не беспокоясь о том, как это отразится на программисте-клиенте. Например, вы можете реализовать класс «на скорую руку», а затем переписать его, чтобы повысить скорость работы. При правильном разделении скрытой и открытой части, сделать это будет совсем несложно.

Никакая часть сложной системы не должна зависеть от внутреннего устройства какой-либо другой части.

Разумная инкапсуляция должна локализовать проектные решения, которые могут измениться. По мере эволюции системы, ее разработчики могут обнаружить, что какие-то операции выполняются недопустимо долго, а какие-то объекты занимают слишком много памяти. В таких ситуациях внутреннее представление объекта, как правило, изменяется, чтобы реализовать более эффективные алгоритмы или оптимизировать использование памяти, заменяя хранение данных их вычислением.

Интерфейс и реализация

В любом классе присутствуют две части: интерфейс и реализация.

Интерфейс отражает внешнее поведение объектов этого класса. Внутренняя реализация описывает представления и механизмы достижения желаемого поведения объекта.

В интерфейсе собрано все, что касается взаимодействия данного объекта с другими объектами, а реализация скрывает от других объектов все детали, не имеющие отношения к процессу взаимодействия объектов.

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

Инкапсуляция в Java

Инкапсуляция в Java реализована с помощью использования модификаторов доступа.

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

  • private– члены класса доступны только членам данного класса. Всё что объявлено private, доступно только конструкторам и методам внутри класса и нигде больше. Они выполняют служебную или вспомогательную роль в пределах класса и их функциональность не предназначена для внешнего пользования. Закрытие (private) полей обеспечивает инкапсуляцию;

  • по умолчанию (package-private) – члены класса доступны классам, которые находятся в этом же пакете;

  • protected– члены класса доступны классам, находящимся в том же пакете, и подклассам – в других пакетах;

  • public– члены класса доступны для всех классов в этом и других пакетах.

Модификатор класса указывается перед остальной частью описания типа отдельного члена класса. Это означает, что именно с него должен начинаться оператор объявления класса.

public String errMessage;
private AccountBalance balance;

private boolean isError(byte status) {}
public class Account {}

Когда член класса обозначается модификатором доступа public, он становится доступным для любого другого кода в программе, включая и методы, определенные в других классах.

Когда член класса обозначается модификатором private, он может быть доступен только другим членам этого класса. Следовательно, методы из других классов не имеют доступа к закрытому члену класса.

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

Модификатор доступа protected связан с использованием механизма наследования и будет рассмотрен позже.

Модификатор доступа указывается перед остальной частью описания типа отдельного члена класса (то есть, именно с модификатора доступа начинается объявление члена класса).

Член класса (переменная, конструктор, методы), объявленный public, доступен из любого метода вне класса.

Всё что объявлено private, доступно только конструкторам и методам внутри класса и нигде больше. Они выполняют служебную или вспомогательную роль в пределах класса и их функциональность не предназначена для внешнего пользования. Закрытие (private) полей обеспечивает инкапсуляцию.

Доступ к полям через геттеры и сеттеры.

В подавляющем большинстве случаев, поля класса объявляются как private (это не касается статических переменных и констант, там ситуация может быть другая). Должны быть веские основания объявить поле класса общедоступным. Манипулирование данными должно осуществляться только с помощью методов.

Для того чтобы дать возможность получить доступ к переменной или дать возможность изменить ее значение, объявляют специальные методы, которые называются "геттерами" и "сеттерами".

Геттер возвращает значение приватного поля, тогда как сеттер меняет значение приватного поля (новое значение передается в качестве аргумента метода).

Хотя сигнатура и имена геттеров и сеттеров могут быть любыми, приучите себя соблюдать строгий шаблон для объявления геттеров и сеттеров.

Геттер должен иметь префикс get, после которого идет название поля с большой буквы. Геттер, как правило, не имеет входных аргументов.

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

Account.java
public class Account {

    private double balance;

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
}

Большинство IDE для Java имеют механизм для генерации геттеров и сеттеров. В IntelliJ IDEA нажмите комбинацию Alt+Insert находясь в окне редактирования java-файла. Откроется контекстное меню Generate, где вы можете выбрать генерацию геттера и сеттера, после чего указать поля, для которых необходимо сгенерировать методы.

Пример использования инкапсуляции

Представим, что нам необходимо создать класс «Корзина» (Cart), который хранит в себе набор объектов класса «Товар» (Item).

Какие методы «Корзина» должна предоставлять для внешнего использования? Это могут быть, например, методы «Добавить товар», «Убрать последний добавленный товар», «Подсчет суммы цен товаров в корзине», «Повышение цен в корзине на N процентов» и «Снижение цен в корзине на N процентов».

Название метода

Описание

public Cart(int capacity)

Конструктор с 1 параметром – максимальным количеством товаров в корзине.

public boolean addItem(Item item)

Добавление товара в корзину. Возвращает успешность операции.

public Item deleteLastAddedItem()

Удаление последнего добавленного товара в корзину. Возвращает удаленный товар.

public double calculateItemPrices()

Подсчет суммы цен всех товаров в корзине.

public void raiseItemPrices(double percent)

Поднять цены товаров в корзине на определенный процент (значение процента передается как аргумент метода).

public void cutItemPrices(double percent)

Снизить цены товаров в корзине на определенный процент (значение процента передается как аргумент метода).

Как вы можете заметить, это публичные методы, а значит, их можно вызвать через оператор-точку имея ссылку ну объект.

Cart cart = new Cart();
cart.addItem(new Item("Клавиатура", 2000));

Перечень этих публичных методов и составляет интерфейс класса – то есть, с помощью этих методов объект класса будет взаимодействовать с внешним миром.

Эти методы имеют вполне четко определенные входные аргументы и могут возвращать значения четко определенных типов, и никак иначе. По аналогии с этим, поворот колес автомобиля осуществляется четко определенным образом – поворотом руля, и бензин надо заливать в четко определенное отверстие крышки бензобака, а не как-то еще.

То – как будет реализовано хранение товаров в корзине – это внутренняя логика класса и она не должна быть доступна внешнему миру, она должна быть скрыта от внешнего вмешательства. Другие классы, которые будут использовать объекты класса Cartне должны знать и не должны иметь доступ к тому – как там «внутри» реализовано хранение товаров, подсчет цен и изменение цены на определенный процент и так далее, они могут только лишь использовать предоставленные им публичные методы. Давайте реализуем «Корзину» с помощью структуры «стек», которая, в свою очередь, реализована обычным массивом.

Cart.java
public class Cart {

    private Item[] stack; // массив для реализации стека
    private int topIndex; // указатель на вершину стека

    // При создании корзины мы должны
    // указать максимальное количество элементов
    // в корзине
    public Cart(int capacity) {
        stack = new Item[capacity];
        topIndex = -1;
    }

    // Добавление нового товара в корзину
    public boolean addItem(Item item) {
        return push(item);
    }

    // Приватный метод, который реализует добавление в стек
    private boolean push (Item item) {
        // Добавляем товар в стек
        return true; // или false если не стек переполнен
    }
    
    // Удаление последнего добавленного товара в корзину
    public Item deleteLastAddedItem() {
        return pop();
    }

    // Приватный метод, который реализует извлечение из стека
    private Item pop() {
        return new Item(); // Извлеченный из стека товар
    }
}

Как мы видим, массив с товарами, указать на вершину стек объявлены как privateчлены класса. Это значит, что мы не можем получить к ним доступ извне – они доступны только внутри данного класса.

Cart cart = new Cart();
cart.addItem(new Item("Клавиатура", 2000));

// Данная инструкция вызовет ошибку компиляции
cart.topIndex = 4;

Программиста, который будет использовать класс Cart, не должна волновать ситуация с переполнением стека, с попыткой извлечь элемент из пустого стека, он не должен следить за указателем на вершину стека, он даже не должен знать что это стек.

Для него объект класса Cart это некоторый объект, который предоставляет «услугу» в виде корзины товаров и с этой корзиной можно работать с помощью определенных публичных методов.

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

Инкапсуляция при проектировании классов

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

Один из важнейших аспектов проектирования класса - принять решение о том, какие свойства сделать доступными вне класса, а какие оставить секретными.

Класс может включать 25 методов, предоставляя доступ только к пяти из них и используя остальные 20 внутренне. Класс может использовать несколько типов данных, не раскрывая сведений о них. Этот аспект проектирования классов называют "видимостью", так как он определяет, какие свойства класса "видимы" или "доступны" извне.

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

Как и любой другой аспект проектирования, разработка интерфейса класса - итеративный процесс. Если приемлемый интерфейс класса не удается создать с первого раза, сделайте еще несколько попыток, пока он не стабилизируется.

Приведем несколько правил использования инкапсуляции при проектировании классов:

Минимизируйте доступность классов и их членов.

Минимизация доступности - одно из нескольких правил, поддерживающих инкапсуляцию. Если вы не можете понять, каким делать конкретный метод: открытым, закрытым или защищенным - некоторые авторы советуют выбирать самый строгий уровень защиты, который работает.

Не делайте данные-члены открытыми.

Предоставление доступа в данным-членам нарушает инкапсуляцию. Например, класс Point (точка), который предоставляет доступ к данным:

class Point {

    public float x;
    public float y;
    public float z;
}

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

class Point {

    private float x;
    private float y;
    private float z;

    public void setX(float x) {
        this.x = x;
    }

    public void setY(float y) {
        this.y = y;
    }

    public void setZ(float z) {
        this.z = z;
    }
}

поддерживает прекрасную инкапсуляцию. Вы не имеете понятия о том, реализованы ли данные как float x, y и z, хранит ли класс Point эти элементы как double, преобразуя их во float, или же он хранит их на Луне и получает через спутник.

Не включайте в интерфейс класса закрытые детали реализации.

Истинная инкапсуляция не позволяла бы узнать детали реализации вообще. Они были бы скрыты и в прямом, и в переносном смыслах.

Не делайте предположений о клиентах класса.

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

Не делайте метод открытым лишь потому, что он использует только открытые методы.

То, что метод использует только открытые методы, не играет особой роли. Лучше спросите себя, согласуется ли предоставление доступа к данному методу с абстракцией, формируемой интерфейсом.

Last updated