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

В этой статье я собираюсь доказать, что использование шаблона через функцию-конструктор без ключевого слова this — является наиболее предпочтительным подходом для инкапсуляции состояния. Вдохновением для меня послужила широкая критика предложения TC39 (Прим. ред.: TC39 — комитет, который занимается развитием JavaScript) о приватных полях на Reddit.

Поговорим о том, как инкапсулировать состояние:

  1. С помощью ключевого слова class ES2015
  2. С помощью простых функций и объектных литералов
  3. Без использования this и с помощью приватных полей

В конце статьи вас ждет бонус, демонстрирующий структуру данного шаблона. Любителям React Hooks это должно понравиться!

Использование классов ES2015

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

class MyObj1 {
constructor(initVal) {
this.myVal = initVal
}

set(x) {
this.myVal = x
}
}

После создания экземпляра:

const x = new MyObj1(0)

Установить значение myVal можно с помощью:

x.set(2)

Или:

x.myVal = 2

Аналогичным образом можно получить доступ к полю напрямую:

console.log(x.myVal) // output: 2

Синтаксис выглядит очень простым и понятным для тех, кто знаком с другими языками ООП, однако у данного подхода есть и свои существенные недостатки.

Отсутствие приватных полей

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

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

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

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

Дополнительные умственные затраты и площадь поверхности API

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

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

Классическое наследование

В добавок ко всему, в силу особенностей синтаксиса (через ключевое слово extends) предпочтение отдается наследованию, а не композиции, а традиционный классический шаблон наследования предлагается в некорректном виде.

Использование простых функций и объектов

объектно-ориентированное программирование без классов

Итак, что же следует использовать, чтобы избежать ключевого слова class?

Простые функции и объекты! Рассмотрим базовые блоки, которые знакомы даже начинающим разработчикам.

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

Объявление функции происходит следующим образом:

const MyObj2 = initVal => {
return {
myVal: initVal,
set: function(x) {
this.myVal = x
}
}
}

Обратите внимание на отсутствие необходимости использования class или constructor. Это просто функция, которая возвращает объект. Вот один из примеров создания:

const x = MyObj2(0)

В данном случае нет необходимости использовать ключевое слово new, поскольку мы не будем изменять прототипы. Это просто функция, которая возвращает объект.

Аналогично классовому подходу ES2015, можно установить значение с помощью:

x.set(2)

Или:

x.myVal = 2

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

Это просто базовый Javascript: Функции и объектные литералы

Отсутствие ключевого слова `this`

Ключевое слово this в Javascript сбивает с толку как новичков, так и старых программистов. В старом формате кода можно увидеть, что this устанавливается в self или that. А быстрый поиск в Google покажет множество статей с попытками объяснения того, как ключевое слово this работает в Javascript.

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

При простом использовании замыкания, можно определить функцию, создающую объект, подобным образом:

const MyObj3 = initVal => {
let myVal = initVal
return {
get: function() {
return myVal
},
set: function(val) {
myVal = val
}
}
}

Аналогично представленному выше подходу, создание объекта можно осуществить вот так:

const x = MyObj3(0)

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

x.get()

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

Замыкания обеспечивают сильный контракт

Теоретически, переменная “сохраняется” внутри объекта, возвращаемого функцией, благодаря замыканиям Javascript. Это означает, что установить значение можно с помощью метода setter ( x.set(2)), но при попытке изменить поле напрямую ( x.myVal = 2) ничего не произойдет.

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

Что насчет прототипов?

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

И даже Дуглас Крокфорд (тот, кто ранее был сторонником прототипного наследования) теперь рекомендует объектно-ориентированное программирование “без классов” (т.е. шаблон, описанный выше).

Что насчет производительности?

Одним из недостатков использования замыканий является проблема производительности. Несмотря на то, что при создании объекта не заметно никакой разницы, вызовы метода для объекта с помощью замыканий будут осуществляться примерно на 80% медленнее.

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

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

Простота имеет значение

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

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

Зачем добавлять классы, когда можно использовать простые функции и простые объекты Javascript? При работе с функциями в Javascript требуется понимание замыканий. Примечательно, что Redux использует этот шаблон в функции createStore.

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

Бонус: Контрпример с композицией!

Внимание!!! Это может напомнить вам React Hooks 🙂

// state container definition
const useState = initVal => {
let val = initVal;

const get = () => val;
const set = x => (val = x);

return Object.freeze({ get, set });
};

// make a counter by using the state container
const makeCounter = () => {
const { get, set } = useState(0);

const inc = () => set(get() + 1);
const dec = () => set(get() - 1);

return Object.freeze({ get, inc, dec });
};

// create the counter object
const myCounter = makeCounter();

// let's test our counter out
console.log(myCounter.get()); // 0
myCounter.inc();
myCounter.inc();
console.log(myCounter.get()); // 2
myCounter.dec();
myCounter.dec();
console.log(myCounter.get()); // 0

Перевод статьи Adrian LiJavascript state encapsulation without classes in 2019 (with private fields!)

Предыдущая статьяНасколько хорошо вы знакомы с основами C?
Следующая статьяХитрости объектно-ориентированного программирования. Часть 5: Правило бойскаутов