Для чего нужна инкапсуляция
Что значит инкапсулировать?
Начал читать книгу о паттернах. В ней постоянно встречаются предложения типа
инкапсулируйте то, что изменяется
4 ответа 4
В разработке ПО есть два схожих понятия – инкапсуляция и сокрытия информации. Кто-то считает, что это синонимы, кто-то нет, но это не так и важно.
Немного истории: Дэвид Парнас в году эдак 70-м в статье “On the Criteria To Be Used in Decomposing Systems into Modules” впервые ввел понятие сокрытия информации, как ключевого инструмента проектирования. Звучал этот принцип примерно так: декомпозиция системы на модули не должна основываться на анализе блок схем или потоков исполнения. Вместо этого, каждый модуль должен прятать внутри некоторое решение (design decision), предоставляя минимальное количество информации о нем своим клиентам.
Вот небольшой пример.
Допустим, вы разрабатываете ынтырпрайз приложение, которое делает что-то важное. Любое нормальное приложение обычно требует некоторой конфигурации: ну, параметры подключения к БД или серверу, и другую ценную информацию. И вот, матерый артихектор (*) вместе с не менее матерым клиентом просят прочитать конфигурацию из конфигурационного файла.
(*) это не опечатка, пожалуйста, не правьте это!
В процессе разговора с ними вы понимаете, что никто толком не знает, почему читать конфигурацию нужно именно из файла, какой должен быть у него формат и что именно там должно храниться.
Этот пример кажется надуманным, но это не так! Мы довольно часто сталкиваемся с изменчивостью требований, но используем, к сожалению, один из двух подходов:
• Полностью игнорируем возможность изменения требований и делаем все в лоб или
• Создаем супер сложное решение с десятком уровней косвенности, которое должно выдержать изменения требований в любом направлении без изменения кода вообще.
Более же разумный подход находится где-то по середине. Каждый раз, когда я начинаю разработку некоторой фичи, я думаю, сколько кусков в коде придется поменять, если требования или детали реализации существенно изменятся. При этом я не стараюсь свести количество изменений к 1 (ну, типа, если мы следуем SRP, то должно быть только одно место, в случае изменения требованй). Я стараюсь, чтобы этих мест было мало, а изменения были простыми.
Собственно, это и есть суть information hiding и его младшей сестры – инкапсуляции.
Инкапсуляция — JS: Введение в ООП
Из всего многообразия возможностей ООП, есть одна базовая, которая для большинства программистов ассоциируется с ООП. Она называется инкапсуляция. Инкапсуляция – это объединение функций и данных в рамках одной структуры, внутреннее состояние которой (данные) скрыто от внешнего мира (этот аспект мы разберем позже). Такие функции называют методами. Мы уже встречались с ними много раз и, как вы заметили, в JavaScript они используются повсеместно.
Сложно
Перед тем как мы начнем рассматривать инкапсуляцию подробнее, надо сделать небольшое отступление по поводу терминологии и путаницы в среде разработчиков. Это особенно важно, учитывая, что многие студенты уже приходят, начитавшись разной литературы. Если для вас пока сложно понять, что написано в следующем абзаце, то просто не обращайте внимания, вернитесь к нему в конце курса.
В большом числе источников под инкапсуляцией понимают сокрытие данных (data hiding) от прямого внешнего обращения (обычно с помощью ключевых слов private, protected). Более того, именно это определение захотят от вас услышать на собеседовании, но оно правильно лишь частично. Несмотря на то, что это распространенное определение, стоит разделять объединение данных с методами и сокрытие этих данных. Есть языки, например JavaScript и Python, в которых есть объединение данных, но нет сокрытия данных. Причем если в этих языках ввести сокрытие данных, то архитектура программ не изменится, а вот если разъединить данные и методы, то придется переписать практически весь код. Примерно такая же картина и с языками в которых есть сокрытие данных. Если его убрать, то мало что поменяется, кроме того, что разработчикам придется быть чуть аккуратнее при работе с объектами.
Подводя итог: инкапсуляция это и объединение, и сокрытие там, где оно есть. Там где его нет, это просто объединение. В этом курсе мы будем разделять инкапсуляцию (понимая под ней только объединение данных и функций) и сокрытие данных, чтобы иметь возможность обсуждать эти особенности независимо. Иначе бы возникла путаница с тем, что имеется в виду, когда упоминается термин инкапсуляция.
Зачем нужно сокрытие данных, разбирается в уроке про инварианты
Конец Сложно 🙂
О том, как работают методы внутри, мы поговорим в следующем уроке. А сейчас рассмотрим внешние особенности методов.
Работа с методами вместо функций приводит к одному неожиданному эффекту – появляется возможность реализовать автодополнение методов в редакторах. Это снижает ментальную нагрузку и очень радует программистов. Существует теория, что именно эта особенность методов стала причиной такой популярности ООП (не подтвержденная, но вполне вероятная).
В языках с развитой системой модулей автодополнение есть и при работе с обычными функциями. Но там в любом случае надо сначала написать правильное имя модуля. Пример из эликсира: User.getName(user). С другой стороны, существуют языки с Unified Function Call (например Nim), там обычные функции можно вызывать как методы и получать автодополнение.
Другая особенность достаточно противоречивая. Для многих разработчиков код с методами выглядит «естественнее». С их точки зрения, абстракции с помощью данных можно строить только на базе методов. Если не объединять данные и функции в одном месте, то абстракция невозможна. Такое восприятие возникает из-за ограниченного опыта. Как правило такой разработчик никогда не работал за пределами популярных ООП-языков и в его языке абстракции на функциях противоестественны и даже невозможны.
Это, конечно, не так. Достаточно пройти курс JS: Абстракции с помощью данных, чтобы убедиться в этом. Абстракции и моделирование реального мира существуют не только в ООП. Они существовали до и будут существовать после.
Попробуйте представить себе добавление в друзья в ООП-стиле. Кто кого должен добавить (первый друг второго или второй первого) и как не допустить рекурсии при взаимном добавлении?
Третья особенность методов уже интереснее. Она действительно помогает сделать работу с кодом проще, а сам код короче. При работе с объектами нам не надо ничего дополнительно импортировать, как в случае с функциями. Любая функция, в которую был передан объект, может вызывать его методы так, как она хочет. Если бы мы работали с функциями, то нам бы пришлось дополнительно импортировать нужные функции. Эта особенность не дается бесплатно, она ограничивает расширение объектов (об этом в следующих уроках).
А что делать в том случае, когда объекта нет, как в примере выше? Разработчики языков и библиотек поступают по-разному. В JavaScript обычные функции и методы спокойно уживаются вместе. Примерно то же самое происходит в Python. В Ruby и PHP (в современных фреймворках) обычные функции выглядят уже не так естественно, хотя их по-прежнему можно создавать. В Java вообще нет возможности создавать обычные функции. Любая функция будет методом. Поэтому в Java объекты создают практически на каждый чих. Это значительно раздувает программу и усложняет реализацию простых вещей. Но есть и другие языки. В Elixir и Clojure методов в текущем понимании просто нет и самое главное, они там просто не нужны, а код при этом лаконичный, простой и расширяемый.
Для имитации обычных функций в Java используют статические методы. Они позволяют работать без создания объектов.
Четвертая особенность – цепочки. Вспомните такой вызов:
Этот метод возвращает новую строку, у которой тоже есть методы, а значит их можно вызвать. Например:
А теперь немного магии. Что если не создавать промежуточные переменные, а делать вызовы сразу? Пробуем:
Код получился компактнее и в некоторых случаях он будет понятнее. Но не увлекайтесь, очень легко перейти границу. Этот код всегда можно разбить на несколько строк:
Подобные цепочки можно строить, даже если возвращается значение другого типа. В таком случае можно применять методы соответствующего типа:
У таких цепочек есть специальное имя: fluent interface
Как и практически все остальное в современном понимании ООП, цепочки не являются чем-то эксклюзивным. Более того, они повторяют такую вещь, как пайплайн (pipeline). Если вы знакомы с командной строкой, то скорее всего не раз видели такой код:
Эта цепочка команд последовательно передает данные слева направо, пропуская их сквозь разные обработчики. Сама концепция пришла из математики и появилась задолго до программирования. Во многих языках пайплайн реализован как языковая конструкция (либо макрос в Lisp). Это настолько удачная концепция, что сейчас её стараются интегрировать во многие языки. Например, она есть в F#, OCaml, Elixir, Elm, Julia, Hack. Прямо сейчас пайплайн находится в стадии рассмотрения в JavaScript. Посмотрите пример:
12.4 – Функции доступа и инкапсуляция
Зачем делать переменные-члены закрытыми?
В предыдущем уроке мы упоминали, что переменные-члены класса обычно делаются закрытыми. Разработчикам, изучающим объектно-ориентированное программирование, часто бывает трудно понять, зачем это нужно. Чтобы ответить на этот вопрос, давайте начнем с аналогии.
В современной жизни у нас есть доступ ко многим электронным устройствам. У вашего телевизора есть пульт дистанционного управления, который можно использовать для включения/выключения телевизора. Вы едете на работу на машине (или скутере). Вы делаете фотографию на свой смартфон. Все эти три вещи используют общий шаблон: они предоставляют простой интерфейс (кнопка, руль и т.д.), который вы можете использовать для выполнения действия. Однако то, как на самом деле работают эти устройства, от вас скрыто. Когда вы нажимаете кнопку на пульте дистанционного управления, вам не нужно знать, что он делает для связи с телевизором. Когда вы нажимаете педаль газа в автомобиле, вам не нужно знать, как двигатель внутреннего сгорания заставляет колеса вращаться. Когда вы делаете фотографию, вам не нужно знать, как датчики собирают свет в пиксельное изображение. Такое разделение интерфейса и реализации чрезвычайно полезно, поскольку позволяет нам использовать объекты, не понимая, как они работают. Это значительно упрощает использование этих объектов и увеличивает количество объектов, с которыми мы можем взаимодействовать.
По тем же причинам разделение реализации и интерфейса полезно и в программировании.
Инкапсуляция
В объектно-ориентированном программировании инкапсуляция (также называемая сокрытием информации) – это процесс скрытия деталей о том, как реализован объект, от пользователей этого объекта. Вместо этого пользователи получают доступ к объекту через открытый, общедоступный интерфейс. Таким образом, пользователи могут использовать объект, не понимая, как он реализован.
В C++ мы реализуем инкапсуляцию через спецификаторы доступа. Как правило, все переменные-члены класса делаются закрытыми (скрывая детали реализации), а большинство функций-членов делаются открытыми (открывая интерфейс для пользователя). Хотя требование, чтобы пользователи класса использовали открытый интерфейс, может показаться более обременительным, чем предоставление открытого доступа к переменным-членам напрямую, это на самом деле дает большое количество полезных преимуществ, которые помогают стимулировать повторное использование класса и его поддерживаемость.
Примечание. Слово инкапсуляция также иногда используется для обозначения упаковки данных и функций, которые работают с этими данными. Мы предпочитаем называть это объектно-ориентированным программированием.
Преимущество: инкапсулированные классы проще в использовании и уменьшают сложность ваших программ
Преимущество: инкапсулированные классы помогают защитить ваши данные и предотвратить неправильное использование
Глобальные переменные опасны тем, что у вас нет строгого контроля над тем, кто имеет доступ к этим глобальным переменным, или как они используются. Классы с открытыми членами страдают от той же проблемы, только в меньшем масштабе.
Например, предположим, что мы пишем строковый класс. Мы могли бы начать так:
Эти две переменные имеют внутреннюю связь: m_length всегда должна быть равна длине строки, содержащейся в m_string (это соединение называется инвариантом). Если бы m_length была открытой, любой мог бы изменить длину строки, не изменяя m_string (или наоборот). Это поставило бы класс в несогласованное состояние, что могло бы вызвать всевозможные странные проблемы. Делая m_length и m_string закрытыми, пользователи вынуждены для работы с этим классом использовать любые доступные открытые функции-члены (а эти функции-члены могут гарантировать, что m_length и m_string всегда устанавливаются правильно).
Мы также можем помочь защитить пользователя от ошибок при использовании нашего класса. Рассмотрим класс с открытой переменной-членом массива:
Если пользователи могут получить доступ к массиву напрямую, они могут попытаться использовать его с недопустимым индексом, что приведет к неопределенным результатам:
Однако если мы сделаем массив закрытым, то сможем заставить пользователя использовать функцию, которая сначала проверяет, что индекс корректен:
Таким образом, мы защитили целостность нашей программы. Кстати, функции at() классов std::array и std::vector делают нечто очень похожее!
Преимущество: инкапсулированные классы легче изменять
Рассмотрим простой пример:
Инкапсуляция дает нам возможность изменить способ реализации классов, не нарушая работу всех программ, которые их используют.
Вот инкапсулированная версия этого класса, который использует функции для доступа к m_value1 :
Теперь давайте изменим реализацию класса:
Обратите внимание: поскольку мы не изменяли прототипы каких-либо функций в открытом интерфейсе нашего класса, наша программа, использующая данный класс, продолжает работать без каких-либо изменений.
Точно так же, если бы гномы пробрались ночью в ваш дом и заменили внутренности вашего пульта от телевизора на другую (но совместимую) технологию, вы, вероятно, даже не заметили бы этого!
Преимущество: инкапсулированные классы легче отлаживать
И, наконец, инкапсуляция помогает вам отлаживать программу, когда что-то идет не так. Часто, когда программа работает некорректно, это происходит потому, что одна из наших переменных-членов имеет неправильное значение. Если получить доступ к переменной напрямую могут все, то отследить, какой фрагмент кода изменил эту переменную, может быть трудно (это может быть любой, и вам нужно будет установить точки останова везде, чтобы выяснить, где же ее изменили). Однако если для изменения значения все должны вызывать одну и ту же открытую функцию, вы можете просто установить точку останова на этой функции и наблюдать, как каждый вызывающий меняет значение, пока не увидите, где что-то пошло не так.
Функции доступа
В зависимости от класса нам может быть целесообразно (в контексте того, что делает класс) иметь возможность напрямую получать или устанавливать значение закрытой переменной-члена.
Функция доступа – это короткая открытая функция, задачей которой является получение или изменение значения закрытой переменной-члена. Например, в классе MyString вы можете увидеть что-то вроде этого:
Функции доступа обычно бывают двух видов: геттеры и сеттеры. Геттеры (англ. «getter», также иногда называемые аксессорами) – это функции, возвращающие значение закрытой переменной-члена. Сеттеры (англ. «setter», также иногда называемые мутаторами) – это функции, которые устанавливают значение закрытой переменной-члена.
Вот пример класса, у которого есть геттеры и сеттеры для всех его членов:
Приведенный выше класс Date – это, по сути, инкапсулированная структура данных с тривиальной реализацией, и пользователь этого класса может разумно ожидать, что сможет получить или установить значение дня, месяца или года.
Приведенный выше класс MyString используется не только для передачи данных – он имеет более сложную функциональность и инвариант, который необходимо поддерживать. Для переменной m_length не было предусмотрено никакого сеттера, потому что мы не хотим, чтобы пользователь мог устанавливать длину напрямую (длина должна устанавливаться только при изменении строки). В этом классе имеет смысл разрешить пользователю напрямую получать длину строки, поэтому был предоставлен метод-геттер длины.
Геттеры должны предоставлять доступ к данным «только для чтения». Поэтому лучше всего возвращать их по значению или по константной ссылке (а не по неконстантной ссылке). Геттер, который возвращает неконстантную ссылку, позволит вызывающему изменить реальный объект, на который та ссылается, что нарушает суть геттера «только для чтения» (и нарушает инкапсуляцию).
Лучшая практика
Геттеры должны возвращать результат по значению или константной ссылке.
Проблемы с функциями доступа
Существует немало дискуссий о том, в каких случаях следует использовать функции доступа или избегать их. Хотя они не нарушают инкапсуляцию, некоторые разработчики утверждают, что использование функций доступа нарушает хороший дизайн классов ООП (тема, которая может легко заполнить всю книгу).
А пока мы рекомендуем прагматичный подход. При создании классов учитывайте следующее:
Резюме
Как видите, инкапсуляция при небольших дополнительных затратах усилий дает множество преимуществ. Основное преимущество заключается в том, что инкапсуляция позволяет нам использовать класс, не зная, как он был реализован. Это значительно упрощает использование незнакомых нам классов.
Что такое инкапсуляция в Java?
Инкапсуляция в Java – это принцип объединения данных (переменных) и кода в единое целое. Это одна из четырех концепций ООП. Другие три – это Наследование, Полиморфизм и Абстракция.
Инкапсуляция на примере
Чтобы понять, что такое инкапсуляция, рассмотрим следующий класс банковских счетов с методами депозита и отображения баланса.
Обычно переменные в классе устанавливаются как «частные», как показано ниже. Доступ к нему можно получить только с помощью методов, определенных в классе. Никакой другой класс или объект не может получить к ним доступ.
Если элемент данных является закрытым, это означает, что доступ к нему возможен только в пределах одного класса. Никакой внешний класс не может получить доступ к частному члену данных или переменной другого класса.
Но реализация метода имеет проверку на отрицательные значения. Таким образом, второй подход также терпит неудачу.
Таким образом, вы никогда не подвергаете свои данные внешней стороне. Что делает ваше приложение безопасным.
Весь код можно представить как капсулу, и вы можете общаться только через сообщения. Отсюда и название инкапсуляции.
Сокрытие данных в Java
Часто инкапсуляция Java называется скрытием данных. Но концепция инкапсуляции не просто скрывает данные, она предназначена для лучшего управления или группировки связанных данных.
Чтобы добиться меньшей степени инкапсуляции в Java, вы можете использовать такие модификаторы, как «protected» или «public». Благодаря инкапсуляции разработчики могут легко изменять одну часть кода, не затрагивая другую.
Методы получения и установки в Java
Если элемент данных объявлен как «закрытый», то доступ к нему возможен только в пределах одного класса. Никакой внешний класс не может получить доступ к данным члена этого класса. Если вам нужен доступ к этим переменным, вы должны использовать публичные методы «getter» и «setter».
Методы Getter и Setter используются для создания, изменения, удаления и просмотра значений переменных.
Следующий код является примером методов получения и установки:
В приведенном выше примере метод getBalance() – это метод получения, который считывает значение переменной account_balance, а метод setNumber() – это метод установки, который устанавливает или обновляет значение для переменной account_number.
Абстракция и Инкапсуляция – разница
Часто инкапсуляция неправильно понимается как абстракция. Давай учиться-
Простым примером, чтобы понять эту разницу, является мобильный телефон. Где сложная логика в печатной плате инкапсулирована в сенсорном экране, и интерфейс предоставлен, чтобы абстрагировать ее.
Для чего нужна инкапсуляция
Инкапсуляция является одним из ключевых понятий ООП.Для начала приведу формальное определение этого понятия:
Можно сказать, что инкапсуляция подразумевает под собой скрытие данных (data hiding), что позволяет защитить эти данные.
А теперь определение, которое, как мне кажется, наиболее точно определяет суть инкапсуляции:
Переменные состояния объекта скрыты от внешнего мира. Изменение состояния объекта (его переменных) возможно ТОЛЬКО с помощью его методов(операций).
Это существенно ограничивает возможность введения объекта в недопустимое состояние и/или несанкционированное разрушение этого объекта.
Для иллюстрации приведенного выше постулата рассмотрим простой жизненный пример.
Аналогично и с нашими объектами, которые могут быть чрезвычайно сложными, а Вы пытаетесь что-то в них подправить, не представляя их внутреннюю организацию.
Вы можете сказать, что этот принцип далеко не новость в программировании, и будете правы. Подобного рода подход для доступа к данным далеко не нов и используется на каждом шагу(во всяком случае в компьютерной сфере).
Хорошим примером применения принципа инкапсуляции являются команды доступа к файлам. Обычно доступ к данным на диске можно осуществить только через специальные функции. Вы не имеете прямой доступ к данным, размещенным на диске. Таким образом, данные, размещенные на диске, можно рассматривать скрытыми от прямого Вашего вмешательства. Доступ к ним можно получить с помощью специальных функций, которые по своей роли схожи с методами объектов. При этом, хотелось бы отметить два момента, которые важны при применении этого подхода. Во-первых, Вы можете получить все данные, которые Вам нужны за счет законченного интерфейса доступа к данным. И, во-вторых, Вы не можете получить доступ к тем данным, которые Вам не нужны. Это предотвращает случайную порчу данных, которая возможна при прямом обращении к файловой системе. Кроме того, это предотвращает получение неверных данных, т.к. специальные функции обычно используют последовательный доступ к данным.
Как известно, ни что в этом мире не дается даром. Применение этого метода ведет к снижению эффективности доступа к элементам объекта. Это обусловлено необходимостью вызова методов для изменения внутренних элементов(переменных) объекта. Однако, при современном уровне развития вычислительной техники, эти потери в эффективности не играют существенной роли.