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

Итак, что же такое инкапсуляция? Что это на самом деле означает и зачем нам это нужно?

Согласно Википедии: «Инкапсуляция используется, чтобы скрыть значения или состояние объекта структурированных данных внутри класса, предотвращая прямой доступ к ним неавторизованных сторон. В классе обычно предоставляются общедоступные методы (так называемые «геттеры» и «сеттеры») для доступа к значениям, а другие клиентские классы вызывают эти методы для извлечения и изменения значений внутри объекта ».

Но что это на самом деле означает? Когда мы скрываем данные \ реализацию, как и с какой целью?

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

1. Скрытие информации:

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

У клиента (разработчика) должен быть понятный и простой API, который позволяет ему использовать библиотеку без необходимости читать внутренний код, разработчик должен видеть только то, что ему нужно знать для работы с этой библиотекой, не менее и не более того.

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

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

String handleResponse(String response) {
   …
}

Можно ли узнать точно, что делает функция, не заглядывая в код?

При просмотре этой функции возникает несколько вопросов: что означает возвращаемое значение String? что мне делать, если функция возвращает null? что именно функция делает с ответом? Есть ли побочные эффекты? …

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

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

На эти вопросы необходимо ответить по сигнатуре метода (имя метода, принимаемые параметры и возвращаемое значение).

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

2. Защита инвариантов:

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

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

Итак, что означает «защита инвариантов», так это то, что наш код должен гарантировать, что инварианты всегда истинны, и очень трудно (даже невозможно) перевести объект в недопустимое состояние.

Давайте рассмотрим простой пример кода Java, чтобы проиллюстрировать эту концепцию, представьте себе класс, представляющий телефонную книгу:

class PhoneBook {
   private List<Person> peopleList;
   public void init(List<Person> peopleList) {
      this.peopleList = peopleList;
   }
   public Person get(int id) {
    ...
   }
   public void add(Person person) {
    ...
   }
   public int getCount() {
      return this.peopleList.size();
   }
}

И рассмотрим это использование:

PhoneBook phoneBook = new phoneBook();
phoneBook.getCount();

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

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

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

Кроме того, если у объекта есть значение, которое является инвариантом, поддерживаемым операциями этого объекта, мы должны предотвратить изменение этого значения непосредственно извне класса, потому что, если мы сделаем это неправильно, мы можем получить недопустимое состояние. для этого объекта, например, предположим, что этот класс PhoneBook отслеживает количество телефонных номеров, которое делится на 3, если в классе есть метод, который возвращает ссылку на «peopleList », пользователи этого класса могут добавлять / удалять элементы из списка без обновления счетчика чисел, которые делятся на 3, что переводит объект в недопустимое состояние, эта проблема приведет к неожиданным ошибкам.

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

Почему важна инкапсуляция

Существует множество ресурсов, которые помогут вам более глубоко изучить инкапсуляцию,

Но почему это так важно?

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

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

Заключение

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

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

И что еще более важно, напишите код, который, даже если он используется неправильно, дает пользователю надлежащую обратную связь об ошибке и способах ее предотвращения.

Так что ты думаешь? Считаете ли вы, что инкапсуляция также очень важна?