facade

Паттерн Facade в JavaScript

Наши соц. сети: instagram, fb, tg

Когда мы создаём наше приложение, мы часто встречаемся лицом к лицу с внешними API. Некоторые API достаточно просты, другие могут быть очень сложными и объемными. Унифицировать их под единый интерфейс одна из задач, которую решает Facade.

Давайте представим, что мы создаем приложение, которое показывает информацию о: мультиках (movies), Теле шоу (TV shows), музыке (music), книгах (books). Информацию по каждому ресурсу предоставляют разные источники. Получение реализовано с использованием различных методов, требований и т. д. Мы должны помнить все требования и как правильно запросить каждый ресурс.

Но должны ли, или этого можно избежать?

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

Я подготовил 4е разные реализации получения ресурсов:

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" }
];

They are named using different patterns, they are implemented better, worse, require more or less work. Because I didn't want to overcomplicate, I used simple examples with common response format. But nevertheless, this illustrates the problem well.

Реализация нашего Facade

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

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

Первое, что нужно сделать - определить структуру публичного API. Для данного примера, одного геттера должно хватить с головой. Единственным различием будет тип медиа данных, которые мы получаем - book, movie и т. д. Итак, тип медиа данных будет нашим фундаментом.

Следующая, общая вещь для всех ресурсов. Каждый из них запрашивается по ID. Поэтому, наш геттер должен принимать один параметр - ID.

Создание нашего фасада

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

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

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

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

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

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

 

get(id) {
return id;
}
}

Это будет нашей базовой реализацией. Давайте перейдем к реализации вунтренностей нашего класса - приватным геттерам.

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

  • Музыка - создается экземпляр класса и внутрь метода get передается ID
  • Мультики - каждый экземпляр возвращает данные, необходим 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));
}
}

Давайте остановимся на минутку. Как вы могли видеть, этот метод также приватный. Почему? Сделав его публичным мы не получим никаких преимуществ. На вход он принимает два аргумента - func и id. Он принимает функцию, чтобы выполнить её и проверить результат. Как вы могли заметить, результат выполнения присваивается переменной result. Далее мы проверяем, если выполнение успешно, то возвращаем Promise. Почему такая странная конструкция? Promise очень легко дебажить и исполнять, с помощью конструкции asycn/await или даже с помощью синтаксиса then/catch.

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

Okay, so what we have now? The private methods for querying vendors. Our inner facade to try to query. And our public getter skeleton. Let's expand it into a living being.

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

Поскольку мы полагаемся на предопределенные типы, мы будем использовать очень мощный оператор 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. В нашем случае вернется Radiohead - OK Computer. Кстати, хороший трек.

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

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-совместимый объект.

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