Итак, хабровчане, уже в этот четверг в OTUS стартует курс «Fullstack разработчик JavaScript». И сегодня мы делимся с вами еще одной авторской публикацией в преддверии старта курса.



Автор статьи: Павел Якупов


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

Говоря проще, паттерны проектирования — это удачные решения, которые используются достаточно часто. Паттерны проектирования как понятие сформировались еще в то время, когда языка программирования JavaScript не было в помине, в сфере software engineering паттерны проектирования были подробно описаны еще в 1994 году «Большой четверкой». Однако мы в нашей статье обратимся к реализации некоторых несложных паттернов именно на языке программирования JavaScript.

Быстро пробежимся по основным видам паттернов проектирования:

Паттерны проектирования создания


Как понятно из названия, данные паттерны в основном применяются для создания новых объектов. К ним относится такие паттерны, как Конструктор, Фабрика, Прототип и Синглетон.

Структурные паттерны проектирования


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

Паттерны проектирования поведения


Этот вид паттернов проектирования направлен на то, что бы улучшить коммуникацию между различными объектами. К паттернам проектирования поведения относятся Командир, Итератор, Медиатор, Обзервер, Паттерн Состояния, Стратегия и Шаблон.

В данной статье мы разбираем только паттерны проектирования, связанные с созданием объектов — потому что они немного проще и лучше подходят новичкам, чтобы начать создавать свои to-do приложения, текстовые RPG в консоли, простые игры на Canvas, и т.д.

Паттерн «Модуль»


Этот паттерн проектирования появился в языке JavaScript, так сказать, поневоле. Изначально в языке не было продумано противодействия таким явлениям, как загрязнение глобальной области видимости(а в других языках, таких как С# или С++ существуют namespace, которые решают эту проблему «из коробки»). Кроме того, модули частично необходимы для реиспользования кода, потому как их можно после первого создания и использования их можно подключать к проектам других команд, используя человеко-часы куда более грамотно.
Паттерн модуль все время своего существования(уже в ES5 он широко использовался) использовал IIFE(немедленно вызываемые функциональные выражения).

Приведем простой пример паттерна «Модуль»:

const first_Module = (function() {
let Surname = "Ivanov";
let Nickname = "isakura313";
  function declare_Surname() {
    console.log(Surname);
  }
  return {
    declare_Nickname: function() {
      alert(Nickname);
    }
  }
})();
first_Module.declare_Nickname();
console.log(Surname); // а вот тут уже нет доступа

Паттерн «Модуль» полностью инкапсулирует данные, которые заключены в нем. Мы можем обращаться к ним только с помощью публичных методов, и пока в JavaScript не появится реализации публичных и приватных методов «из коробки» придется реализовывать модуль таким образом.

Паттерн «Конструктор»


Это паттерн проектирования, предназначенный для создания объекта. Конструктором называется функция, которая создает новые объекты. Однако в JavaScript объекты могут быть созданы «на лету», даже без функции-конструктора или определения класса.
Конструктор один из наиболее часто используемых паттернов проектирования. Он применяется для создания объектов определенного типа.

//представим что мы делаем маленькое to-do приложение
//итак, у нас есть класс, который  может создавать нам объекты с цветом и текстом дела
class ItemDeal {
  constructor(name, color) {
    this.name = name;
    this.color = color;
  }
}
 
//у нас есть класс как шаблон, но как нам заставить сохранять в localStorage эти данные?
let item = new ItemDeal(`${text}`, `${select.value - 1}`);  //где text - данные из инпута, a select.value - данные о цвете
let myJson = JSON.stringify(item); // чтобы у нас дела могли сохраняться в //localStorage, нам нужно упаковать только что созданный объект
//localStorage.setItem(item.name, myJson);

Паттерн «Фабрика»


Паттерн Фабрика это еще один паттерн сориентированный на создание объекта из класса. В нем мы предоставляем общие интерфейсы которые делегируют создание объекта подкласса(subclasses).

Данный паттерн проектирования чаще всего используется для того, чтобы манипулировать коллекцией объектов, которые имеют как различные, так и одинаковые характеристики.
В нижеприведенном примере мы создадим класс врага(EnemyFactory) для нашего текстового квеста. Да, пример достаточно простой, но в этом и заключается проблема паттернов проектирования: они нужны для «больших» приложений. Если у вас только три картинки вылетают по клику на странице — то никакие паттерны проектирования вам особо и не будут полезны.

Ладно, просто покажем кусочек кода нашей текстовой RPG в консоли:

class EnemyFactory{
constructor() {
    this.createEnemy = function(type) {
      let enemy;
      if (type === 'goblin' || type === 'ork') enemy = new Infantry();
      else if (type === 'elf') enemy = new Archer();
      enemy.attack = function() {
        return `The ${this._type} is attack`;
      };
      return enemy;
    };
  }
}
class Infantry {
  constructor() {
    this._type = 'goblin';
    this.scream = function() {
      return 'AAAAAAAAA! Za ordu!!!';
    };
  }
}
 
class Archer {
  constructor() {
    this._type = 'elf';
    this.magic_attack = function() {
      return 'Magic fog around you! You cant see!!';
    };
  }
}
 
const enemy_army = new EnemyFactory();
let newGoblin = enemy_army.createEnemy('goblin');
let newElf = enemy_army.createEnemy('elf');
console.log(newGoblin.attack());
console.log(newElf.attack());
console.log(newGoblin.scream());
console.log(newElf.magic_attack());
//произошло сражение

Паттерн «Прототип»


Здесь мы используем некоторый тип «скелета» настоящего объекта для создания нового объекта. А прототипирование это самый нативный тип выстраивания ООП в JavaScript.

//итак, мы продолжаем разработку нашего to-do приложения
const itemDeal = {
  colorOfHeader: blue; // и вк и фейсбук такие. Чем моё приложение хуже?
  create() {
    console.log("our item create");
    //создаем наш item
  },
  delete() {
    console.log("our item delete now");
     // удаляем наш item
  },
};
//однако заказчик вдруг попросил сделать так, что to-do могли
 //пользоваться несколько человек, как  сервис Trello.
const newDeal = Object.create(itemDeal, { owner: { value: 'Paul' } });
console.log(newDeal.__proto__ === itemDeal);  //true

Паттерн «Прототип» оказывается полезен, если в вашем приложении можно как-то расширить или уменьшить функциональность.

Паттерн «Одиночка»


Или, как он более известен, «Singleton». «Одиночка» это специальный паттерн в котором может существовать только один экземпляр класса. Паттерн работает следующим образом — инициализация объекта сработает, если не имеется ни одного экземпляра, созданного или возвращенного. Если имеется, то вернется инициированный объект.
Допустим мы создаем класс главного героя, и хотим, чтобы он был 1, а не 4, как в Джуманджи.

class Hero {
  constructor(name) {
    if (Hero.exists) {
      return Hero.instance;
    }
    this._name = name;
    Hero.instance = this;
    Hero.exists = true;
    return this;
  }
  getName() {
    return this._name;
  }
  setName(name) {
    this._name = name;
  }
}
//пора героям подать голос
const smolder = new Hero('Smolder!');
console.log(smolder.getName()); // Smolder!
 
const ruby = new Hero('Ruby');
console.log(ruby.getName()); //  Smolder!
//синглетон хорош, если у вас один герой в игре. Для Джуманджи он как раз бы и не подошел.
//но мне нужна была игра с несколькими персонажами для иллюстрации работы паттерна

Итак, всем спасибо за внимание! Надеюсь, кому-то эта статья послужит хорошим стартом в ООП на JavaScript(хоть, по честному признанию моего коллеги с многолетним опытом и на Java, и на JavaScript, ООП там, мягко говоря, не очень развитое). Однако в новых стандартах язык значительно улучшился, и я уверен, эта методология еще будет только чаще использоваться(или придет Dart и все заменит).

Полезные ссылки:

developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/create
learn.javascript.ru/class
developer.mozilla.org/ru/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript

Комментарии (13)


  1. norguhtar
    24.09.2019 18:27

    Поясните в чем смысл перепечатывания стандартных паттернов?


    1. Alexufo
      24.09.2019 22:15

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


      1. andres_kovalev
        25.09.2019 00:59

        Надеюсь, потенциальные клиенты сделают выводы, хотя бы почитав комментарии)


        1. Alexufo
          25.09.2019 19:03

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


      1. norguhtar
        25.09.2019 07:04

        Раз уплочено, можно писать банальщину? Как-то я большего ожидал :)


      1. flancer
        25.09.2019 08:07

        То-то у меня недоумение росло, чем дальше я углублялся в публикацию. Метод подачи материала какой-то "шумный". Все слова знакомые, но нужно поднапрячься, чтобы понять, что автор хотел показать в примерах (особенно, если читать комментарии). Лучше уж перевод.


        И кстати, а что такое "коммерческий блог"?


        1. flancer
          25.09.2019 08:11

          по ходу, вот это:
          image


          ОК, буду принимать во внимание.


    1. andres_kovalev
      25.09.2019 00:58

      Возможно, смысл в том, чтобы посмеяться, заменив некоторые названия Синглетон, Командир, Шаблон) Можно придумать свои паттерны вместо существующих (конструктор вместо билдера), можно часть паттернов с похожим названием объединить (фабричный метод и абстрактная фабрика), а потом ещё и придумать им новую неправильную (даже "анти-паттерновую") реализацию. Думаю, много ещё чего можно придумать)


  1. MaxRokatansky Автор
    25.09.2019 11:28

    Друзья, данная статья предназначается для тех, кто только начинает осваивать паттерны проектирования. Если у вас уже имеется серьезный опыт и данный материал для вас не актуален, это не значит, что статья не может быть полезной для новичков. Откуда столько злости?


    1. andres_kovalev
      25.09.2019 23:50
      +1

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


    1. flancer
      26.09.2019 08:02

      Откуда столько злости?

      Это не злость, а feedback. Вам самому не режет глаз в первом же примере?


      first_Module.declare_Nickname();

      let Surname = "...";

      Или в чём назначение функции?


        function declare_Surname() {
          console.log(Surname);
        }

      Она нигде далее в примере не используется.


      Или вот такой фрагмент кода для шаблона "Конструктор":


      //у нас есть класс как шаблон, но как нам заставить сохранять в localStorage эти данные?
      let item = new ItemDeal(`${text}`, `${select.value - 1}`);  //где text - данные из инпута, a select.value - данные о цвете
      let myJson = JSON.stringify(item); // чтобы у нас дела могли сохраняться в //localStorage, нам нужно упаковать только что созданный объект
      //localStorage.setItem(item.name, myJson);

      Откуда вдруг возник localStorage и зачем нам в него что-то сохранять? Где здесь применение шаблона "Конструктор" — new ItemDeal или JSON.stringify?


      Или вот такой текст в комментах кода:


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

      Ощущение, что автор попутал пространство кода и пространство самой публикации.


      А вот нарушение принципа наименьшего удивления:


      const smolder = new Hero('Smolder!');
      console.log(smolder.getName()); // Smolder!
      
      const ruby = new Hero('Ruby');
      console.log(ruby.getName()); //  Smolder!

      Как правильно заметил коллега andres_kovalev :


      Как раз опытному разработчику подобная дезинформация вреда не нанесет, а что прикажете делать "наученным" такими статьями новичкам?


      1. flancer
        26.09.2019 08:13
        -1

        Вот хороший для новичка пример реализации "Одиночки":


        var Singleton = new function(){
            var instance = this;
            // Код конструктора
            return function(){ return instance }
        }
        console.log( new Singleton === new Singleton ); // true


    1. Jintoki
      26.09.2019 10:35

      Спасибо за статью.