Инкапсуляция в JavaScript
Перевод статьи Eric Elliott — Encapsulation in JavaScript
Инкапсуляция — это объединение данных и методов, которые воздействуют на эти данные, так что доступ к этим данным ограничен извне. Или как описывает это Алан Кей, «инкапсуляция — это локальное хранение и защита и скрытие процесса состояния». В ООП это означает, что объект хранит свое состояние в приватном порядке, и только методы объекта имеют доступ для его изменения.
Если вы хотите изменить инкапсулированное состояние, вы не обращаетесь к нему напрямую и не изменяете реквизиты некоторых объектов. Вместо этого вы вызываете метод объекта, и, возможно, объект ответит обновлением своего состояния. Например, если вы создаете приложение с использованием Redux, вместо непосредственного управления моделью данных представления, вы отправляете сообщение, называемое действием, в хранилище данных. Redux контролирует когда и как обрабатывается это сообщение. Время и обработка этого сообщения контролируются для создания предсказуемых, детерминированных обновлений состояния. При наличии одинаковых сообщений в одинаковом порядке Redux всегда будет отображать одно и то же состояние.
Аналогично, когда вы хотите обновить состояние компонента React с помощью useState или setState, эти изменения не влияют непосредственно на состояние компонента. Вместо этого они ставятся в очередь потенциальные изменения состояния, которые применяются после завершения цикла рендеринга. Вы напрямую не устанавливаете состояние компонента React, это делает сам React;
Почему инкапсуляция?
В 1960-х и 1970-х программисты боролись с временными зависимостями и конфликтами ресурсов, вызванными попыткой совместного использовать одних и те же ресурсов памяти между различными операциями, выполняемыми в недетерминированных последовательностях. Они также были разочарованы необходимостью привязки кода к конкретному представлению структуры данных о состоянии программы.
В 1970-х годах Алан Кей был вдохновлен составными структурами данных из диссертации Ивана Сазерленда Sketchpad, разработанной в 1961–1963 годах в Массачусетском технологическом институте и Симуле, разработанной в 1960-х годах Оле-Йоханом Далем и Кристен Найгаард из Норвежского вычислительного центра в Осло. Алан Кей принимал участие в исследованиях и разработках ARPAnet, имел опыт работы в науке и математике и был особенно вдохновлен тем, как клетки инкапсулируются мембраной и передаются через передачу сообщений.
Все эти идеи собрались вместе, чтобы сформировать основы ООП: инкапсуляция и передача сообщений.
Проблема с разделяемым изменяемым состоянием состоит в том, что если ваше состояние ввода зависит от состояния вывода какой-либо другой инструкции и любых видов параллелизма, это создает условия гонки. Если вы измените порядок вызова инструкций, это может изменить результаты. Смешайте любой недетерминизм в последовательности, и в результате получится хаос: непредсказуемое, недоказуемое, на первый взгляд случайное состояние приложения. Иногда это работает. Иногда это не так.
Инкапсуляция является одним из подходов к решению этой проблемы.
Инкапсуляция также решает еще одну интересную проблему. Представьте, что у вас есть совокупность данных, которые нужно обработать. Один из способов сделать это — сначала выбрать структуру данных для представления. Если вы начнете с реализации (скажем, используете массив), и все, кто его использует, узнает о его структуре то это может создать тесную связь со структурой данных, что может затруднить изменение этой реализации позже. Что, если вы в конечном итоге захотите поменять массив на поток (stream), дерево или какую-то другую структуру данных? Если все узнают реализацию, это может быть слишком поздно.
Но когда мы инкапсулируем эти детали реализации за общедоступным интерфейсом, а затем все, кто использует объект, делают это только через его общедоступный интерфейс, позже легче будет изменить внутреннюю реализации. Для иллюстрации представьте, что у вас есть структура данных, в которой хранятся числа, и вам нужен способ умножения сохраненных значений на два:
// Эта реализация работает только с массивами const doubleAllImperative = data => { const doubled = []; for (let i = 0, length = data.length; i < length; i++) { doubled[i] = data[i] * 2; } return doubled; }; // То же самое что и выше но работает с чем угодно через оператор map const doubleAllInterface = data => data.map(x => x * 2); const box = value => ({ map: f => box(f(value)), toString: () => `box(${ value })` }); console.log( doubleAllInterface([2,3]), // [4, 6] doubleAllInterface(box(2)).toString(), // box(4) );
Инкапсуляция может быть мощным инструментом, способным помочь вам предотвратить ошибки, возникающие из-за общего изменчивого состояния, а также уменьшить жесткую связь между компонентами и структурами данных, на которые они опираются. Это поможет вам соблюдать как минимум три ключевых принципа разработки программного обеспечения:
- Избегайте общего изменяемого состояния. «Недетерминизм = параллельная обработка + изменяемое состояние» — Мартин Одерски, дизайнер языка программирования Scala
- Программируйте интерфейс, а не реализацию. — Банда четырех, «Design Patterns: Elements of Reusable Object Oriented Software”»
- Небольшое изменение требований должно приводить к соответственно небольшому изменению приложения. — N. D. Birrell, M. A. Ould, “A Practical Handbook for Software Development”
Идиоматическая инкапсуляция в JavaScript
Когда Брендан Айх создал JavaScript в эти роковые 10 дней, кульминацией которых стал выпуск в 1995 году, он задумал две идеи:
- Использования языка программирования Scheme в браузере
- Все должно выглядеть как в языке Java
Scheme — это функциональный язык программирования — диалект LISP, который был элегантным небольшим языком в 1958 году. Поскольку Scheme поддерживала очень гибкие функции высшего порядка и замыкания, у нее был очень большой потенциал.
Java — это объектно-ориентированный язык на основе классов. От Java JavaScript получил понятие функций конструктора, (в конце концов) классы и ключевого слова new (среди прочего).
Так же Брендан Айх черпал свое вдохновение от языка программирования Self. От него он взял прототипное наследование, которые делают JavaScript гораздо более мощным и гибким, чем его аналогично названный, но в остальном дальний родственник, Java, но это уже другая история.
Спустя 23 года этот парадигмальный плавильный котел все еще немного неправильно понят. Одно из таких распространенных недоразумений связано с инкапсуляцией.
Прежде чем перейти к идиоматической инкапсуляции для объектов в JavaScript, я сначала хочу обратиться к общему понятию инкапсуляции. Давным-давно, до того, как многие разработчики JavaScript узнали о замыканиях, некоторые разработчики JavaScript заметили, что объекты JavaScript (и позже классы) не включают механизм для закрытых свойств.
До появления предложения о приватных полях (private fields proposal) в ECMAScript не было никакого способа создать честное приватное свойство для объектов в JavaScript. Вместо того, чтобы использовать замыкания, с помощью которых обычно создают приватные данные для объектов в JavaScript, некоторые разработчики решили обозначить частные свойства и методы, добавив к ним префикс подчеркивания, и это стало общепризнанным (хотя и неуклюжим и спорным) соглашением.
Это спорно по нескольким причинам:
Критические изменения: внутренние свойства и методы имеют тенденцию меняться чаще, чем общедоступные свойства и методы Для многих префиксных методов подчеркивания, например, myComponent._handleClick, ссылаются на методы, которые не предназначены для непосредственного вызова пользователями API. Вместо этого они используются только для внутреннего использования, и, как таковые, если их реализации меняются или полностью удаляются, эти разработчики не считают это серьезным изменением.
К сожалению для пользователей, многие новые разработчики не знают о соглашении префиксов подчеркивания, поэтому могут использовать свойства когда захотят. Так же опытные разработчики часто знают, что это значит, но думают: «Я знаю, что я делаю», и поэтому все равно буду их использовать, особенно если это обеспечивает очевидное решение текущей проблемы. Другими словами, большое количество людей игнорирует соглашение, и это приводит к более серьезным изменениям, чем необходимо.
Утечка деталей реализации: Помните пример выше, где мы изначально поддерживали только массивы, но теперь мы хотим расширить нашу поддержку для потоков? Что ж, если ваши пользователи имеют прямой доступ к базовым структурам данных, они могут создавать зависимости от этих структур данных, поэтому при первом обнаружении потока они будут очень удивлены, когда их код сломается.
Расширенная поверхность атаки для хакеров: в частности, для общедоступных API-интерфейсов добавление еще большего количества API, чем необходимо для использования вашего кода, расширяет доступные данные для использования злоумышленниками. Один из важнейших принципов обеспечения безопасности программного обеспечения ограничивает поверхность атаки только тем, что абсолютно необходимо. Если вы действительно намереваетесь использовать что-то только для внутреннего использования, оно не должно подвергаться внешнему воздействию.
Самодокументируемый код: Ваш общедоступный API должен быть максимально самодокументируемым. Один из способов сделать это — показать только те методы и свойства, которые вы собираетесь предоставить своим пользователям. Таким образом, пользователи не склонны использовать неподдерживаемые и недокументированные методы. Если вы используете соглашение о подчеркивании, вы полагаетесь на то, что пользователь знает, что это значит, и понимает, что вы не хотите что бы это было ими использовано. Если вы используете инкапсуляцию, вам не нужно беспокоиться об этом. Они не смогут использовать то, к чему у них нет доступа.
Настоящая инкапсуляция в JavaScript
Конечно, как известно JavaScript всегда поддерживал реальную инкапсуляцию данных. Очень легко объявить приватные данные в JavaScript.
Используем Замыкание
const createCounter = () => { // Переменная, определенная в области действия конструктора // является приватной для этой функции. let count = 0; return ({ // Любые другие функции, определенные в той же области, являются привилегированными: // Они оба имеют доступ к закрытой переменной `count` // определяется в любом месте их цепочки областей видимости click: () => count += 1, getCount: () => count.toLocaleString() }); }; const counter = createCounter(); counter.click(); counter.click(); counter.click(); console.log( counter.getCount() );
Привилегированный метод — это метод, который имеет доступ к закрытым данным внутри области действия функции (также известной как лексическая среда). Привилегированные функции и методы имеют доступ на основе ссылок к переменным внутри функции, даже после того, как функция завершилась. Эти ссылки являются действующими, поэтому, если состояние изменяется во внутренней функции, изменения переносятся в каждую привилегированную функцию. Другими словами, когда мы вызываем counter.click(), она изменяет значение, которое видит counter.getCount().
Можно даже наследовать приватное состояние, используя функциональные миксины.
Использование приватных полей
На момент написания этой статьи закрытые поля стали доступны в babel с включенной функцией stage-3. Они так же поддерживаются в Chrome, Opera, браузере Android и Chrome для Android, поэтому есть хороший шанс, что вы можете сделать что-то вроде этого:
class Counter { #count = 0 click () { this.#count += 1; } getCount () { return this.#count.toLocaleString() } } const myCounter = new Counter(); myCounter.click(); myCounter.click(); myCounter.click(); console.log( myCounter.getCount() );
Я все еще предпочитаю использовать фабричные функции на основе замыканий, и пока не использую классы с новой спецификации полей классов, но если вам действительно нужна инкапсуляция, и замыкания, то новые поля классов намного лучше, чем подчеркивания, потому что они не полагаться на соглашение и вместо этого обеспечивают истинную инкапсуляцию.
Эрик Эллиотт — автор книг «Composing Software» и «Programming JavaScript Applications». Как соучредитель EricElliottJS.com и DevAnywhere.io, он обучает разработчиков основным навыкам разработки программного обеспечения. Создает и консультирует команды разработчиков для криптографических проектов, а также вносит свой вклад в опыт разработки программного обеспечения для Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC и ведущих исполнителей звукозаписи, включая Usher, Frank Ocean, Metallica и многих других.