Как сделать инкапсуляцию

Добавил пользователь Алексей Ф.
Обновлено: 05.10.2024

Из всего многообразия возможностей ООП, есть одна базовая, которая для большинства программистов ассоциируется с ООП. Она называется инкапсуляция. Инкапсуляция – это объединение функций и данных в рамках одной структуры, внутреннее состояние которой (данные) скрыто от внешнего мира (этот аспект мы разберем позже). Такие функции называют методами. Мы уже встречались с ними много раз и, как вы заметили, в 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). Если вы знакомы с командной строкой, то скорее всего не раз видели такой код:

Открыть доступ

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

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

Я хочу, чтобы Вы кое о чем подумали:

Это два варианта реализации класса TimeInterval:

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

А вот фиг вам! Ни черта это не инкапсуляция!
Вот подумайте. Если для нас важно чтобы start Так что такое инкапсуляция?

Это механизм позволяющий нам обеспечить согласованность данных объекта или модуля, от приведения их в некорректное состояние. Обычно для этого прячут все поля и предоставляют только простой набор методов. Как я уже говорил, нам не обязательно даже прятать всё и вся. Достаточно просто запретить “контрактом” или документацией использовать такую возможность. Существуют языки в которых нет спецификаторов доступа (private, protected, public), но в них успешно используют инкапсуляцию.

Я вижу некоторое сходство инкапсуляции с синхронизацией в много поточных приложениях. В обоих случаях мы стремимся защитить наши данные от повреждений или рассогласованности, вызванных некорректным использованием. Просто когда пишете какой-то компонент, предусмотрите вариант что им будет пользоваться блондинка-программист с синдромом Дауна и чтобы у нее не было шансов привести его в некорректное состояние.

Чем плох данный пример (Код 2)?

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

Я бы сделал так:

Теперь нам гораздо сложнее рассогласовать переменные start и end. Приведу еще один пример из жизни:

В классе Car нельзя делать метод removeParkingBrake публичным, потому что машина покатится и врежется в столб, и выйдет из строя. Нельзя разрешать использовать этим методом. А если у пользователя будет возможность вызывать те методы, что находятся в startMoving, но напрямую, то это тоже плохо. Он может забыть прогреть двигатель и в итоге испортит его. Это все утечка внутренней логики работы класса. Нарушение инкапсуляции.

Смотрите метод startMoving. Первоначально задумывалось, что пользователь может использовать только startMoving. В таком случае ничего плохого не произойдет.

Зачем же нам нужна инкапсуляция?

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

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

Когда Вы работаете один на проекте, то Вы свой код обычно держите в голове. И, скорее всего, знаете все нюансы использования Ваших компонентов. Но в большой команде, где каждый занимается своим модулем, для Вас другие модули как черный ящик. Если на Ваш вопрос о инкапсуляции к таким разработчикам, слышите “Инкапсуляция?! Не не слышал”, то будьте уверены – прикрутив новый функционал, Вы сломаете все приложение.

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

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

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

Почему же задают такой простой вопрос на собеседовании?

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

Это как лакмусовая бумажка. И это лично мое мнение.

Related Posts

Для чего он нужен? Этот шаблон очень популярен потому, что очень часто возникает необходимость выстроить…

В этой статье я попробую простым языком и кратко рассказать, что такое класс в общем…

3 комментариев

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

Каждый объект в системе имеет определенный контракт взаимодействия. Контракт выражается в операциях, которые можно выполнить над объектом:

  • Что нужно предоставить ему для корректной работы;
  • Что в состоянии объекта может изменяться в процессе работы;
  • Что он нам готов предоставить в результате.

Одна из задач проектирования кода – это построение прозрачных контрактов и абстракций. Если контракт понятен, лёгок в использовании и не привносит неожиданностей, то такой контракт хороший.

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

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

  1. Поля
  2. Функциональные члены
  3. Конструкторы

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

Состояние – это совокупность значений всех полей объекта.

Если какое-то значение меняется, то меняется и состояние объекта. Наша задача построить объект так, чтобы его состояние не становилось некорректным. Некорректное состояние – это состояние, работа с которым сопряжена с ошибками и багами.

Первое, что нам нужно сделать, это определить: какую ответственность на себя берёт данный тип?

В нашем случае у нас есть задача:

В нашей игре есть автомат. Автомат заряжен патронами разных типов:

  1. Бронебойный
  2. Трасирующий
  3. Обычный

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

Сейчас наша задача построить такой тип, который делал ровно то, что мы хотим, и нельзя было бы сделать того, что мы не ожидаем. На первый взгляд простая задача, не так ли?
Давайте возьмём такое решение:

Как вы думаете, какие операции мы можем совершать над объектом данного типа?

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

  1. Вызывать метод Shot (фактически стрелять этим оружием)
  2. Вызывать метод Reload (фактически перезаряжать оружие)

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

  1. Произвольное изменение текущего патрона в очереди. Ожидает ли данный тип, что снаружи это значение можно изменить как угодно?
  2. Возможно заменить обойму без сбрасывания указателя на текущий патрон.

И это не всё. Пока что я придержу все карты. Согласитесь, что если мы загрузим обойму через Reload на 10 выстрелов, а потом сделаем 5 выстрелов, то ничего страшного не произойдёт. Но что будет, если мы самостоятельно, без Reload, поставим в поле Bullets обойму, скажем, на 3 патрона, и попробуем сделать выстрел? Внезапно оружие не будет стрелять. А что, если перед этим мы произвели всего два выстрела? Оно выстрелит, но один раз. Не совсем очевидное поведение для того, кто этот код использует не правда ли? Но мы же сами разрешили менять это поле пользователю этим типом, соответственно нет ничего странного в том, что люди пробуют это делать.

Как мы можем защитится? Для начала нам нужно сделать поля private. Это позволяет закрыть доступ к ним вне типа. Добились ли мы защиты внутреннего состояния? Оно стало лучше, но всё ещё есть кое-какие проблемы. Они не критичны в данном типе, но могут вызывать проблемы в других ситуациях.

Обратите внимание на метод Reload. Что он принимает? List . А этот тип ссылочный. Это значит, если нам дадут очередь для стрельбы, то ссылку на эту очередь будет иметь тот, кто нам её дал. И, конечно же, он сможет её произвольно изменять, как ему это хочется. Если бы у нас был чувствительный код к этому, то мы бы получили множество багов.

Что нужно делать в данном случае? Ограничивать абстракцию. Конечно же, нам хватило бы тут и обычного массива. List добавляет операции записи и удаления, и мы, по сути, сами сказали: “дайте нам ссылку на что-то, что без нашего ведома может изменяться, а именно пополняться новыми элементами или стремительно сокращаться”.

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

Если вы нашли ошибку, пожалуйста выделите её и нажмите Ctrl+Enter.

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

Цель инкапсуляции:

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

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

Варианты организации инкапсуляции в JavaScript:

  1. с помощью функции (замыкание);
  2. с помощью фабричной функции;
  3. с помощью модулей:
    • на основе функции-конструктора;
    • на основе немедленно вызываемой функции (IIFE - Immediately (немедленно) Invoked (вызываемое) Function (функциональное) Expression (выражение));
    • на основе блока кода ;
  4. с помощью классов.

Если переменная или объект в JavaScript не помещены внутрь какой-либо функции (блока кода), то они становятся глобальными, т.е. свойствами глобального объекта (для браузера это объект window ).

Создание глобальных переменных, как правило, нежелательно:

  1. оно может привести к трудно обнаружимым ошибкам;
  2. усложняет перенос кода в другие приложения.

Реализация инкапсуляции с помощью функции (замыкания)

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

Все переменные внутри функции в JavaScript - это свойства объекта LexicalEnvironment, так называемое лексическое окружение. Данный объект является внутренним и к нему нет доступа. Таким образом, оборачивание любого куска кода в функцию эффективно "скроет" любые вложенные определения переменных или функций от внешней области видимости во внутренней области видимости этой функции.

Читайте также: