Шаблон Facade в JavaScript

Spread the love
  • 8
    Поделились

При создании приложения мы часто сталкиваемся с задачами реализации внешнего API. Иногда методы могут быть простые, иногда сложные. Объединение их под одним общим интерфейсом – одно из применений шаблона Facade (фасад).

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

Или не должны?

Спонсор поста
Вам необходим собственный сервер для 1С, базы данных, почтового сервера или сервер для бизнес приложения? Воспользуйтесь предложеним об аренде выделенного сервера. Стоимость аренды серверов ниже чем у конкурентов. Все оборудование располагается в России в надежном дата-центре уровня Tier3, что полностью соответствует законодательству о хранении и обработке данных на территории РФ.

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

Для примера я подготовил четыре различных варианта поставщиков ресурсов:

class FetchMusic {
  get resources() {
    return [
      { id: 1, title: "The Fragile" },
      { id: 2, title: "Alladin Sane" },
      { id: 3, title: "OK Computer" }
    ];
  }

  fetch(id) {
    return this.resources.find(item => item.id === id);
  }
}

class GetMovie {
  constructor(id) {
    return this.resources.find(item => item.id === id);
  }

  get resources() {
    return [
      { id: 1, title: "Apocalypse Now" },
      { id: 2, title: "Die Hard" },
      { id: 3, title: "Big Lebowski" }
    ];
  }
}

const getTvShow = function(id) {
  const resources = [
    { id: 1, title: "Twin Peaks" },
    { id: 2, title: "Luther" },
    { id: 3, title: "The Simpsons" }
  ];

  return resources.find(item => item.id === 1);
};

const booksResource = [
  { id: 1, title: "Ulysses" },
  { id: 2, title: "Ham on Rye" },
  { id: 3, title: "Quicksilver" }
];

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

Дизайн нашего фасада

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

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

По этому, мы должны определить форму публичного API. Для этого примера достаточно одного геттера. Единственным отличием здесь является тип ресурса – книга, фильм и т. д. Таким образом, мы будем базироваться на типе ресурса.

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

Строим наш фасад

(Я решил использовать класс для этого, но это не является обязательным требованием. Модуль, состоящий из литерала объекта или даже набора функций, вероятно, так же подойдет. Тем не менее, мне нравится эта форма.)

class CultureFasade {
  constructor(type) {
    this.type = type;
  }
}

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

Итак, следующая вещь – определить наши публичные и приватные методы. Для того, чтобы отметить «приватные» методы, я использовал знаменитый _ вместо #, потому что CodePen его пока не поддерживает.

Как мы уже говорили ранее, единственным публичным методом должен быть наш геттер.

class CultureFacade {
  constructor(type) {
    this.type = type;
  }

  get(id) {
    return id;
  }
}

Базовая реализация (скелет) есть. Теперь давайте перейдем к фактическому мясу нашего класса – приватные геттеры.

Прежде всего, нам нужно определить, как будет запрашивается каждый ресурс:

  • Для музыки требуется новый экземпляр, а затем передача ID в методе get;
  • Каждый экземпляр фильма будет возвращает данные, и так же требует ID во время инициализации;
  • ТВ-шоу – это всего лишь одна функция, которая принимает ID и возвращает данные;
  • Книги – это просто ресурс, в котором нам сами придется сделать запрос.

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

Хорошо, начнем с музыки.

class CultureFacade {
  ...

  _findMusic(id) {
    const db = new FetchMusic();
    return db.fetch(id);
  }
}

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

class CultureFacade {
  ...

  _findMusic(id) {
    const db = new FetchMusic();
    return db.fetch(id);
  }

  _findMovie(id) {
    return new GetMovie(id);
  }

  _findTVShow(id) {
    return getTvShow(id);
  }

  _findBook(id) {
    return booksResource.find(item => item.id === id);
  }
}

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

Создаем публичное API

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

Зная это, наш геттер так же должен использовать своего рода фасад. Он должен попытаться получить данные, не предполагая, что это удастся.

Итак, давайте напишем такой метод.

class CultureFacade {
  ...

  get _error() {
    return { status: 404, error: `No item with this id found` };
  }

  _tryToReturn(func, id) {
    const result = func.call(this, id);

    return new Promise((ok, err) => !!result
      ? ok(result)
      : err(this._error));
  }
}

Давай остановимся здесь на минуту. Как видите, метод _tryToReturn также является приватным. Почему? Нет ни какого смысла делать его публичным. Его использование требует знания о других приватных методов. Так же он требуется два параметра – func и id. Хотя последнее совершенно очевидно, первое – нет. Итак, он принимает функцию (точнее, метод нашего класса) при запуске. Далее как видите, выполнение присваивается переменной result. Затем мы проверяем, удалось ли это, и возвращаем Promise. Почему такая сложная конструкция? Promises очень легко отлаживать и выполнять, используя синтаксис async / await или даже простой then / catch.

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

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

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

class CultureFacade {
  constructor(type) {
    this.type = type;
  }

  get(id) {
    switch (this.type) {
      case "music": {
        return this._tryToReturn(this._findMusic, id);
      }

      case "movie": {
        return this._tryToReturn(this._findMovie, id);
      }

      case "tv": {
        return this._tryToReturn(this._findTVShow, id);
      }

      case "book": {
        return this._tryToReturn(this._findBook, id);
      }

      default: {
        throw new Error("No type set!");
      }
    }
  }
}

Примечание об определении типов строк

Наши типы написаны от руки. Это не лучшая практика. Они должны быть определены заранее, так что бы никакая опечатка не вызвала ошибку. Почему бы и нет, давайте сделаем это.

const TYPE_MUSIC = "music";
const TYPE_MOVIE = "movie";
const TYPE_TV = "tv";
const TYPE_BOOK = "book";

class CultureFacade {
  constructor(type) {
    this.type = type;
  }

  get(id) {
    switch (this.type) {
      case TYPE_MUSIC: {
        return this._tryToReturn(this._findMusic, id);
      }

      case TYPE_MOVIE: {
        return this._tryToReturn(this._findMovie, id);
      }

      case TYPE_TV: {
        return this._tryToReturn(this._findTVShow, id);
      }

      case TYPE_BOOK: {
        return this._tryToReturn(this._findBook, id);
      }

      default: {
        throw new Error("No type set!");
      }
    }
  }
}

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

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

Итак, похоже, что мы закончили с реализацией. Давайте используем то что мы создали!

const music = new CultureFacade(TYPE_MUSIC);
music.get(3)
    .then(data => console.log(data))
    .catch(e => console.error(e));

Очень простая реализация с использованием then / catch. Он просто выводит альбом, который мы искали, в нашем случае RadioheadOK Computer.

Хорошо, но давайте попробуем также получить ошибку. Ни один из наших поставщиков не может ничего сказать, если у них нет запрошенного ресурса. Но мы можем!

const movies = new CultureFacade(TYPE_MOVIE);
movie.get(5)
    .then(data => console.log(data))
    .catch(e => console.log(e));

А что у нас здесь? О, консоль выдает ошибку, говоря: «No item with this id found». На самом деле это JSON-совместимый объект!


Как вы можете видеть, шаблон facade может быть очень мощным при правильном использовании. Он может быть очень полезен, когда у вас есть несколько похожих источников, схожие операции и т. д., И вы хотите унифицировать их использование.


Весь код доступен на CodePen.

Оригинальная статья: Tomek Facade pattern in JavaScript

Была ли вам полезна эта статья?
[3 / 3]

Spread the love
  • 8
    Поделились

Добавить комментарий

Ваш e-mail не будет опубликован.