Привет еще раз! Позвольте мне начать с нескольких простых вопросов - нужно ли вам покупать новый телефон каждый раз, когда вы хотите загрузить и попробовать новое приложение? Или вам нужно пойти купить новую антенну для вашего телевизора, если вы хотите изменить свой подписанный пакет каналов? Или вам нужно повторно подключить домашнее электроснабжение, когда вы покупаете новый прибор? Нет почему? Потому что, хотя каждый новый прибор может иметь разные значения напряжения и силы тока, сам прибор поставляется с электрическим адаптером, отвечающим за взаимодействие прибора со стандартным источником электропитания в вашем доме. Другими словами, изменения, происходящие в требованиях к электропитанию бытовой техники, инкапсулируются из стандартного источника питания в вашем доме, таким образом гарантируя, что

what stays the same is isolated from what changes often.

Давайте сначала посмотрим на простой пример разработки программного обеспечения. Допустим, у нас есть экземпляр класса Animal. Животное может иметь распорядок движения, который может варьироваться в зависимости от типа животного, например:

if (animal.type == dog)
    print("animal is walking");
else if (animal.type == bird)
    print("animal is flying");
else if (animal.type == snake)
    print("animal is crawling");

Теперь, когда мы реализуем подпрограмму move таким образом, где бы в нашем проекте мы ни нуждались в перемещении Animal, нам нужно написать это if-else или сказать блок типа switch-case. Если завтра мы хотим добавить новый тип животного Кенгуру, который печатает «животное прыгает», нам, возможно, придется добавить новый регистр во все места в нашем приложении, где используется процедура перемещения.

ООП спешат на помощь?

Я уверен, что вы уже создали в своей голове класс Animal с подпрограммой move () и его подклассы Dog, Snake, Bird, Kangaroo, все с их собственной переопределенной версией move () и вуаля! И вы будете абсолютно правы, думая об инкапсуляции таким образом (хотя, честно говоря, здесь задействованы как инкапсуляция, так и абстракция, но это история для другого раза), но давайте сделаем шаг назад. Инкапсуляция - это не просто принцип объектно-ориентированного программирования, это скорее общий принцип проектирования программного обеспечения, который существует даже в функциональном программировании.

Как мы инкапсулируем изменяющиеся части нашего кода при отсутствии объектов и наследования? Достаточно просто, если мы вернемся и спросим, ​​какова была основная цель - инкапсулировать, что меняется от того, чего нет. Итак, если у нас есть десять мест в нашем коде, где у нас есть блок switch-case для процедуры перемещения животных, мы можем переместить это в метод, как показано ниже:

void move(animal) {
    if (animal.type == dog)
        print("animal is walking");
    else if (animal.type == bird)
        print("animal is flying");
    else if (animal.type == snake)
        print("animal is crawling");
}

Итак, теперь при добавлении нового типа животных нам просто нужно добавить строку кода в одно место в нашем проекте. Как бы банально это ни звучало - это тоже инкапсуляция! Я хотел здесь подчеркнуть, что инкапсуляция, хотя она гораздо более эффективна в объектно-ориентированной среде, существует даже за пределами ООП и не менее полезна и важна, если мы понимаем ее основные цели и преимущества.

Инкапсуляция везде!

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

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

Инкапсуляция в разработке программного обеспечения

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

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

Пример дизайна

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

Допустим, мы создаем приложение для обмена сообщениями, такое как WhatsApp, и наша первая цель - поддерживать текстовые сообщения между двумя клиентскими устройствами. В нашем первом дизайне объекта Message может быть только одно поле «text» или «String»:

class Message {
    String text;
}

Основываясь на этом «предположении», что в сообщении есть только текстовое поле (и ничего больше), мы можем написать код генерации сообщения и код обработки сообщения:

class MessageDisplay {
    public void displayMessage(Message message) {
        System.out.println(message.text);
    }
}

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

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

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

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

enum MessageType {
    TEXT,
    PHOTO,
    VIDEO;
}
class Message {
    MessageType type;
    String messageData;    //text, or URL
}

В приведенном выше примере наш объект сообщения имеет поле типа, которое в настоящее время имеет только одно возможное значение «ТЕКСТ», но позже оно может иметь новые типы, такие как «ФОТО» или «ВИДЕО» в качестве типа сообщения. Теперь, в дополнение к полю типа, нам может понадобиться поле данных, которое будет иметь другое значение в случае разных типов сообщений - в случае текстовых сообщений эти данные могут быть обычным текстом, в случае фотографии это может быть URL-адресом фотографии. По мере развития программного обеспечения нам может потребоваться больше данных для типа сообщения, а не только одно поле String - возможно, мы хотим отправить время начала и время окончания при совместном использовании URL-адреса видео YouTube в виде сообщения. В таких случаях имеет смысл снова инкапсулировать эти данные и, возможно, назвать их новым типом объекта - MessageData или чем-то в этом роде:

class Message {
    Long messageId;
    MessageType messageType;
    String senderId;
    String receivedId;
    MessageData messageData;    //text, or URL
}
abstract class MessageData {
}
class TextMessageData extends MessageData {
    String textMessage;
}
class PhotoMessageData extends MessageData {
    String imageDownloadUrl;
    Integer width;
    Integer height;
    String imageCaption;
    //add more params as needed
}
class VideoMessageData extends MessageData {
    String videoDownloadUrl;
    Long videoStartOffsetMillis;
    //more such params
}

Таким образом, ваш объект сообщения может иметь некоторые поля, общие для всех типов сообщений - например, messageID, messageType, senderID, ReceiverID и т. Д., И данные, которые варьируются в зависимости от типа сообщения, будут инкапсулированы в объект (или интерфейс) MessageData, который позже может быть быть расширенным до PhotoMessageData или VideoMessageData и т. д. Для любопытных это шаблон стратегии.

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

Щепотка соли

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

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

PS: Если вам понравилась статья, поддержите ее аплодисментами 👏 ниже. Ура!