Эффективный объектный дизайн четырьмя способами

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

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

  1. Читаемость. Как и любой хороший код, объектно-ориентированный код должен быть доступен для чтения не только вам, но и другим разработчикам. Некоторые шаблоны проектирования легче интерпретировать, чем другие, и вы всегда должны помнить о удобочитаемости. Если вам сложно понять, что делает ваш код, то другие разработчики почти наверняка ничего не поймут.
  2. Повторение. Одним из основных преимуществ объектно-ориентированного кода является уменьшение избыточности. Если в вашем коде может быть много объектов одного типа, то объектно-ориентированный дизайн почти наверняка подойдет. Однако одни шаблоны уменьшают избыточность больше, чем другие. Имейте это в виду, одновременно учитывая, что большее сокращение избыточности может привести к потере (или, по крайней мере, к более сложной реализации) определенных параметров настройки.
  3. Иерархическая структура. Как мы упоминали ранее, в JavaScript нет настоящих классов, и лучше не думать о своих объектах таким образом; однако есть варианты делегирования подобного поведения различным наборам и подмножествам объектов. Это делается с помощью делегирования прототипов, при котором объект будет искать заданное свойство по всей цепочке прототипов. Таким образом можно создать иерархическую структуру объектов, в которой объект более низкого типа в структуре может делегировать поведение вверх по своей цепочке прототипов (например, объект Chicken, делегирующий поведение layEgg объекту-прототипу более высокого уровня Bird .) Перед тем, как выбрать шаблон проектирования, подумайте, ожидаете ли вы, что иерархическая структура будет необходимой, и если да, то какое поведение следует применить к каким типам объектов.

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

Шаблон создания фабричного объекта

Factory Object Creation Pattern, или просто Factory Pattern, использует так называемые «фабричные функции» для создания объектов аналогичного типа. Каждый объект, созданный такой функцией, имеет одинаковые свойства, включая состояние и поведение. Возьмем, к примеру, следующее:

Здесь у нас есть функция makeRobot(), которая принимает два параметра (name и job) и использует их для присвоения состояния литералу объекта внутри функции, который затем возвращает. Кроме того, функция определяет метод introduce() для того же объекта. В этом примере мы создаем экземпляры двух объектов-роботов, оба из которых имеют одинаковые свойства (хотя и разные значения). Если бы мы захотели, мы могли бы создать тысячи других роботов точно таким же образом и надежно предсказать, какими будут их свойства каждый раз.

Хотя шаблон фабрики полезен для создания похожих объектов, у него есть два основных недостатка. Во-первых, невозможно проверить, был ли данный объект создан определенной фабрикой. Мы не можем, например, сказать что-то вроде bender instanceof makeRobot, чтобы узнать, как был создан bender. Во-вторых, фабричный шаблон не разделяет поведение, скорее, он просто создает новые версии поведения каждый раз, когда он вызывается, и добавляет их к создаваемому объекту. В результате методы повторяются заново для каждого объекта, созданного функцией фабрики, занимая ценное пространство. В большой программе это может оказаться чрезвычайно медленным и расточительным.

Шаблон конструктора

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

  1. Функция немедленно создаст новый объект.
  2. Контекст выполнения функции (this) будет установлен как новый объект.
  3. Код функции будет выполняться в контексте выполнения нового объекта.
  4. Функция неявно вернет новый объект при отсутствии какого-либо другого явного возврата.

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

Этот фрагмент очень похож на предыдущий, за исключением того, что на этот раз мы используем ключевое слово this внутри функции для ссылки на новый объект, установки для него некоторого состояния и свойств, а затем неявного возврата, когда функция завершает выполнение. Ради соглашения (а не по какой-либо фактической синтаксической причине) мы назвали нашу функцию просто Robot с большой буквы «R». И, в отличие от фабричного шаблона, мы даже можем проверить, был ли данный объект создан с помощью Robot функции withinstanceof.

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

Псевдоклассический узор

До сих пор мы действительно не исследовали прототипное делегирование, за исключением краткого упоминания о его существовании. Пришло время увидеть это в действии и одновременно устранить некоторую избыточность кода. Прототипы объектов и их поведение при делегировании достойны целого сообщения в блоге, но здесь мы можем получить хотя бы базовую картину. По сути, когда определенное свойство вызывается для определенного объекта, например someRobot.introduce(), он сначала ищет это свойство на себе. Если такого свойства не существует, он затем просматривает свойства, доступные для его объекта-прототипа, который, в свою очередь, при необходимости смотрит на свой объект-прототип, и так далее вплоть до верхнего уровня Object.prototype. Цепочка прототипов позволяет делегировать поведение, при этом нам не нужно определять какой-либо общий метод для низкоуровневых объектов одного и того же типа. Вместо этого мы можем определить поведение любого общего прототипа и, таким образом, устранить избыточность, определив код только один раз. Вот он в действии с нашими роботами.

Как и в шаблоне конструктора, мы используем ключевое слово new для создания нового объекта, присвоения некоторого состояния и затем неявного возврата этого объекта. Однако в этом случае мы не определяем метод introduce() для каждого из наших роботов. Скорее, мы определяем его на объекте Robot.prototype, который, как мы видели, действует как прототип каждого нового объекта, созданного функцией конструктора Robot. Когда мы пытаемся вызвать, например, wallE.introduce(), объект wallE видит, что у него нет такого метода, и ищет его в цепочке прототипов, быстро находя метод с таким именем на Robot.prototype. Действительно, если мы проверим прототип wallE с помощью Object.getPrototypeOf(), мы увидим, что это действительно Robot.prototype.

Этот шаблон проектирования, известный как псевдоклассический шаблон, решает обе проблемы, которые мы изначально видели в шаблоне Factory; однако он по-прежнему представляет нам несколько неудобную иллюзию классовой системы. Это может привести к некоторым неудачным обходным путям в нашей ментальной модели того, как на самом деле работает JavaScript, и некоторым неожиданным подводным камням в реальном выполнении программы. Одним из решений этой проблемы, популяризированным Кайлом Симпсоном, автором You Don't Know JS, является шаблон Object Linked to Other Object (OLOO), который мы рассмотрим. следующий.

Объект, связанный с другим объектом Узор

Если псевдоклассический шаблон представляет собой предварительную комбинацию шаблона конструктора и прототипного делегирования, то OLOO можно рассматривать как полноценное воплощение системы прототипов JavaScript. В этом шаблоне мы вообще не используем функции для создания объектов. Вместо этого мы определяем своего рода объект blueprint, который затем явно используем в качестве прототипа для любых отдельных объектов, которые нам нужны. Мы можем увидеть это в действии с последним набором роботов.

В этом фрагменте мы сначала определяем объект Robot, который будет служить прототипом для всех будущих роботов. Объект Robot содержит все поведение, которое мы ожидаем от наших роботов; однако он не устанавливает никакого состояния. Вместо этого мы определяем init() метод для Robot, который мы будем использовать для установки состояния для любых будущих роботов. Говоря о будущих роботах, мы делаем это не с помощью функции, а с помощью метода Object.create(), который принимает прототип в качестве аргумента. Передав Robot методу Object.create(), мы гарантируем, что полученный объект имеет Robot в качестве прототипа. Затем мы вызываем метод init() на наших отдельных роботах, чтобы установить необходимое состояние. Мы даже можем проверить, относится ли данный объект к определенному типу, используя удобный метод Object.getPrototypeOf(), как мы делали это в предыдущих фрагментах.

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

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