Привет, друзья!


Представляю вашему вниманию перевод первой части серии статей, посвященных паттернам проектирования в TypeScript.


Спасибо Денису Улесову за помощь в переводе материала.


Паттерны (или шаблоны) проектирования (design patterns) описывают типичные способы решения часто встречающихся проблем при проектировании программ.


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


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


Если привести аналогии, то алгоритм — это кулинарный рецепт с четкими шагами, а паттерн — инженерный чертеж, на котором нарисовано решение, но не конкретные шаги его реализации (источник — https://refactoring.guru/ru/design-patterns, в настоящее время сайт работает только с VPN).


В данной статье рассматриваются следующие паттерны:


  • Стратегия / Strategy;
  • Цепочка обязанностей / Chain of Responsibility;
  • Наблюдатель / Observer;
  • Издатель-Подписчик / Publisher/Subscriber как частный случай использования паттерна "Наблюдатель".

❯ Стратегия / Strategy


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


function login(mode) {
  if (mode === "account") {
    loginWithPassword();
  } else if (mode === "email") {
    loginWithEmail();
  } else if (mode === "mobile") {
    loginWithMobile();
  }
}

Иногда приложение может поддерживать другие методы аутентификации и авторизации, например, в дополнение к электронной почте, страница авторизации Medium поддерживает сторонних провайдеров аутентификации, таких как Google, Facebook, Apple и Twitter.





Для поддержки новых методов нам потребуется снова и снова изменять и дополнять функцию login:


function login(mode) {
  if (mode === "account") {
    loginWithPassword();
  } else if (mode === "email") {
    loginWithEmail();
  } else if (mode === "mobile") {
    loginWithMobile();
  } else if (mode === "google") {
    loginWithGoogle();
  } else if (mode === "facebook") {
    loginWithFacebook();
  } else if (mode === "apple") {
    loginWithApple();
  } else if (mode === "twitter") {
    loginWithTwitter();
  }
}

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


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





Сначала мы определяем интерфейс Strategy. Затем на основе этого интерфейса реализуем две стратегии авторизации — через Twitter и логин/пароль.


Интерфейс Strategy


interface Strategy {
  authenticate(args: any[]): boolean;
}

Класс TwitterStrategy


class TwitterStrategy implements Strategy {
  authenticate(args: any[]) {
    const [token] = args;

    if (token !== "tw123") {
      console.error("Аутентификация с помощью аккаунта Twitter провалилась!");
      return false;
    }

    console.log("Аутентификация с помощью аккаунта Twitter выполнена успешно!");

    return true;
  }
}

Класс LocalStrategy


class LocalStrategy implements Strategy {
  authenticate(args: any[]) {
    const [username, password] = args;

    if (username !== "bytefer" && password !== "666") {
      console.log("Неправильное имя пользователя или пароль!");
      return false;
    }

    console.log("Аутентификация с помощью логина и пароля выполнена успешно!");
    return true;
  }
}

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


Класс Authenticator


class Authenticator {
  strategies: Record<string, Strategy> = {};

  use(name: string, strategy: Strategy) {
    this.strategies[name] = strategy;
  }

  authenticate(name: string, ...args: any) {
    if (!this.strategies[name]) {
      console.error("Политика аутентификации не установлена!");
      return false;
    }

    return this.strategies[name].authenticate.apply(null, args);
  }
}

Пример того, как это работает:


const auth = new Authenticator();

auth.use("twitter", new TwitterStrategy());

auth.use("local", new LocalStrategy());

function login(mode: string, ...args: any) {
  return auth.authenticate(mode, args);
}

login("twitter", "123");

login("local", "bytefer", "666");

Результат запуска приведенного кода выглядит следующим образом:





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


Если вы используете Node.js для разработки сервиса аутентификации, обратите внимание на модуль passport.js:


В настоящее время данный модуль поддерживает 538 стратегий аутентификации:





Случаи использования паттерна "Стратегия":


  • когда необходим динамический выбор одного из нескольких доступных алгоритмов. Как правило, каждый алгоритм инкапсулируется в отдельной стратегии (классе);
  • когда имеется несколько классов, которые отличаются только поведением, и выбор конкретного поведения можно произвести динамически во время выполнения кода.

❯ Цепочка обязанностей / Chain of Responsibility


Паттерн "Цепочка обязанностей" (далее также — "Цепочка") позволяет избежать тесной связи и взаимного влияния между отправителем и получателем запроса, предоставляя нескольким объектам возможность последовательно обрабатывать запрос. В рассматриваемом паттерне многочисленные объекты ссылаются друг на друга, формируя цепочку объектов. Запрос передаются по цепочке до тех пор, пока один из объектов не осуществит его окончательную обработку.





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


При разработке программного обеспечения распространенным сценарием применения "Цепочки" является посредник (промежуточное ПО, middleware).


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





Мы определяем интерфейс Handler. Данный интерфейс, в свою очередь, определяет следующие методы:


  • use(h: Handler): Handler — для регистрации обработчика (посредника);
  • get(url: string, callback: (data: any) => void): void — для вызова обработчика.

Интерфейс Handler


interface Handler {
  use(h: Handler): Handler;

  get(url: string, callback: (data: any) => void): void;
}

Далее определяется абстрактный класс AbstractHandler, который инкапсулирует логику обработки запроса. Этот класс соединяет обработчики в цепочку последовательных ссылок:


Абстрактный класс AbstractHandler


abstract class AbstractHandler implements Handler {
  next!: Handler;

  use(h: Handler) {
    this.next = h;
    return this.next;
  }

  get(url: string, callback: (data: any) => void) {
    if (this.next) {
      return this.next.get(url, callback);
    }
  }
}

На основе AbstractHandler определяются классы AuthMiddleware и LoggerMiddleware. Посредник AuthMiddleware используется для обработки аутентификации пользователей, а посредник LoggerMidddleware — для вывода информации о запросе:


Класс AuthMiddleware


class AuthMiddleware extends AbstractHandler {
  isAuthenticated: boolean;

  constructor(username: string, password: string) {
    super();

    this.isAuthenticated = false;

    if (username === "bytefer" && password === "666") {
      this.isAuthenticated = true;
    }
  }

  get(url: string, callback: (data: any) => void) {
    if (this.isAuthenticated) {
      return super.get(url, callback);
    } else {
      throw new Error("Не авторизован!");
    }
  }
}

Класс LoggerMiddleware


class LoggerMiddleware extends AbstractHandler {
  get(url: string, callback: (data: any) => void) {
    console.log(`Адрес запроса: ${url}.`);

    return super.get(url, callback);
  }
}

Определяем класс Route для регистрации созданных посредников:


Класс Route


class Route extends AbstractHandler {
  urlDataMap: { [key: string]: any };

  constructor() {
    super();
    this.urlDataMap = {
      "/api/todos": [
        { title: "Изучение паттернов проектирования" },
      ],
      "/api/random": () => Math.random(),
    };
  }

 get(url: string, callback: (data: any) => void) {
  super.get(url, callback);

  if (this.urlDataMap.hasOwnProperty(url)) {
      const value = this.urlDataMap[url];
      const result = typeof value === "function" ? value() : value;
      callback(result);
    }
  }
}

Пример регистрации посредников с помощью Route:


const route = new Route();

route.use(new AuthMiddleware("bytefer", "666"))
 .use(new LoggerMiddleware());

route.get("/api/todos", (data) => {
  console.log(JSON.stringify(data, null, 2));
});

route.get("/api/random", (data) => {
  console.log(data);
});




Результат выполнения приведенного кода выглядит следующим образом:





Случаи использования паттерна "Цепочка обязанностей":


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

❯ Наблюдатель / Observer


Паттерн "Наблюдатель" широко используется в веб-приложениях — MutationObserver, IntersectionObserver, PerformanceObserver, ResizeObserver, ReportingObserver. Все эти API можно рассматривать как примеры применения "Наблюдателя". Кроме того, данный паттерн также используется для перманентного мониторинга событий и реагирования на модификацию данных.


В "Наблюдателе" существует две основные роли: Субъект/Subject и Наблюдатель/Observer.


Паттерн "Наблюдатель" определяет отношение один ко многим (one-to-many), позволяя нескольким объектам-наблюдателям одновременно следить за наблюдаемым субъектом. При изменении состояния наблюдаемого субъекта об этом уведомляются все объекты-наблюдатели, чтобы они, в свою очередь, могли обновить собственное состояние.





На приведенной диаграмме в качестве Субъекта выступает моя статья (Article), а Наблюдателями являются Chris1993 и Bytefish. "Наблюдатель" поддерживает простую связь в режиме широковещательной передачи (broadcast), поэтому все наблюдатели автоматически уведомляются о публикации новой статьи.


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





Мы определяем интерфейсы Observer и Subject, которые используются для описания соответствующих объектов:


Интерфейс Observer


interface Observer {
  notify(article: Article): void;
}

Интерфейс Subject


interface Subject {
  observers: Observer[];

  addObserver(observer: Observer): void;

  deleteObserver(observer: Observer): void;

  notifyObservers(article: Article): void;
}

Затем мы определяем классы ConcreteObserver и ConcreteSubject, которые реализуют соответствующие интерфейсы:


Класс ConcreteObserver


class ConcreteObserver implements Observer {
  constructor(private name: string) {}

  notify(article: Article) {
    console.log(`"Статья: ${article.title}" была отправлена  ${this.name}.`);
  }
}

Класс ConcreteSubject


class ConcreteSubject implements Subject{
  public observers: Observer[] = [];

  public addObserver(observer: Observer): void {
    this.observers.push(observer);
  }

  public deleteObserver(observer: Observer): void {
    const n: number = this.observers.indexOf(observer);

    n != -1 && this.observers.splice(n, 1);
  }

  public notifyObservers(article: Article): void {
    this.observers.forEach((observer) => observer.notify(article));
  }
}

Проверяем работоспособность методов наших классов:


const subject: Subject = new ConcreteSubject();

const chris1993 = new ConcreteObserver("Chris1993");

const bytefish = new ConcreteObserver("Bytefish");

subject.addObserver(chris1993);
subject.addObserver(bytefish);

subject.notifyObservers({
  author: "Bytefer",
  title: "Observer Pattern in TypeScript",
  url: "https://medium.com/***",
});

subject.deleteObserver(bytefish);

subject.notifyObservers({
  author: "Bytefer",
  title: "Adapter Pattern in TypeScript",
  url: "https://medium.com/***",
});

Результат выполнения приведенного кода выглядит следующим образом:


"Статья: Observer Pattern in TypeScript" была отправлена Chris1993.
"Статья: Observer Pattern in TypeScript" была отправлена Bytefish.
"Статья: Adapter Pattern in TypeScript" была отправлена Chris1993.

Представим, что в настоящее время я пишу на две основные тематики — JavaScript и TypeScript. Поэтому, если я захочу опубликовать новую статью, то об этом необходимо уведомить только читателей, интересующихся JavaScript, или только читателей, интересующихся TypeScript. При использовании паттерна "Наблюдатель", нам придется создать два отдельных Субъекта. Однако лучшим решением будет использование паттерна "Издатель-Подписчик".


Паттерн Издатель-Подписчик / Pub/Sub


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


В "Издателе-Подписчике" существует три основные роли: Издатель/Publisher, Канал передачи сообщений/Channel и Подписчик/Subscriber.





На приведенной диаграмме Издатель — это Bytefer, тема A и тема B в Каналах соответствуют JavaScript и TypeScript, а ПодписчикиChris1993, Bytefish и др.


Реализуем класс EventEmitter с помощью рассматриваемого паттерна:


type EventHandler = (...args: any[]) => any;

class EventEmitter {
  private c = new Map<string, EventHandler[]>();

  subscribe(topic: string, ...handlers: EventHandler[]) {
    let topics = this.c.get(topic);

    if (!topics) {
      this.c.set(topic, (topics = []));
    }

    topics.push(...handlers);
  }

  unsubscribe(topic: string, handler?: EventHandler): boolean {
    if (!handler) {
      return this.c.delete(topic);
    }

    const topics = this.c.get(topic);

    if (!topics) {
      return false;
    }

    const index = topics.indexOf(handler);

    if (index < 0) {
      return false;
    }

    topics.splice(index, 1);

    if (topics.length === 0) {
      this.c.delete(topic);
    }

    return true;
  }

  publish(topic: string, ...args: any[]): any[] | null {
    const topics = this.c.get(topic);

    if (!topics) {
      return null;
    }

    return topics.map((handler) => {
      try {
        return handler(...args);
      } catch (e) {
        console.error(e);
        return null;
      }
    });
  }
}

Пример использования EventEmitter:


const eventEmitter = new EventEmitter();

eventEmitter.subscribe("ts",
  (msg) => console.log(`Получено:${msg}`));

eventEmitter.publish("ts", `Паттерн "Наблюдатель"`);

eventEmitter.unsubscribe("ts");

eventEmitter.publish("ts", `Паттерн "Издатель/Подписчик"`);

Результат выполнения приведенного кода: Получено: Паттерн "Наблюдатель".


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


Надеюсь, вам было интересно и вы узнали что-то новое.


Во второй части рассматриваются такие паттерны, как:


  • Шаблон / Template;
  • Адаптер / Adapter;
  • Фабрика / Factory;
  • Абстрактная фабрика / Abstract Factory.

Благодарю за внимание и happy coding!




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


  1. markelov69
    15.11.2022 13:49
    +9

    function login(mode) {
      if (mode === "account") {
        loginWithPassword();
      } else if (mode === "email") {
        loginWithEmail();
      } else if (mode === "mobile") {
        loginWithMobile();
      } else if (mode === "google") {
        loginWithGoogle();
      } else if (mode === "facebook") {
        loginWithFacebook();
      } else if (mode === "apple") {
        loginWithApple();
      } else if (mode === "twitter") {
        loginWithTwitter();
      }
    }

    Со временем обнаружится, что эту функцию все труднее поддерживать.

    Мне вот интересно в чем может быть сложность? Даже если ещё 10 условий добавить или больше, хоть 100. Тут же всё просто как 2х2, если это, то вот это, написано черным по белому.

    Понятное дело можно более аккуратно это расписать, но сути это не меняет, сложности то нет на самом деле.

    const modes = {
      account: loginWithPassword,
      email: loginWithEmail,
      mobile: loginWithMobile,
      // ... и так до бесконечности
    }
    
    function login(mode) {
      const method = modes[mode];
      if (method) {
        method();
      } else {
        throw new Error('Политика аутентификации не установлена!');
      }
    }

    Это как один из вариантов, где так же сложности 0 и при добавлении новых политик сложности так и останется 0.

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


    1. BerkutEagle
      15.11.2022 15:51

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


      1. markelov69
        15.11.2022 16:03
        +1

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

        А конкретный пример с кодом пожалуйста можно? Чтобы было видно наглядно как простые варианты пасуют, а вариант со стратегией удобен получается.


        1. BerkutEagle
          15.11.2022 16:47
          +2

          Можно добавлять/заменять/настраивать стратегии по месту. При этом авторы аутентификатора и самих стратегий не зависят друг от друга и работают в рамках контракта (интерфейс Strategy)

          //web
          if (isRU) {
            auth.use('fb', new FBStrategy({...proxySettings}));
            auth.use('vk', new VKStrategy());
          }
          
          //desktop
          if (isDesktop) {
            auth.use('account', new LDAPStrategy({dc: 'dc.acme.com'}));
          }
          
          //mobile
          if (isMobile) {
            auth.use('account', new AndroidAccountsStrategy());
          }
          

          Собственно, в статье есть ссылка на pasport.js, как пример, для которого написано куча стратегий. Можно комбинировать их под свои потребности.


          1. markelov69
            15.11.2022 17:31
            +2

            Так вы привели такой же банальный пример, который не является какой то сложностью и т.п.

            const modes = new Map();
            
            //web
            if (isRU) {
              modes.set('fb', FB({...proxySettings}))
              modes.set('vk', VK());
            }
            
            //desktop
            if (isDesktop) {
              modes.set('account', LDAP({dc: 'dc.acme.com'}));
            }
            
            //mobile
            if (isMobile) {
              modes.set('account', AndroidAccounts());
            }
            
            function login(mode) {
              const method = modes.get(modes);
              if (method) {
                method();
              } else {
                throw new Error('Политика аутентификации не установлена!');
              }
            }

            В чем разница-то не пойму? Условия есть? Есть. Конфиг есть? Есть. И в чем удобство именно применения паттерна стратегия в данном случае?


            1. BerkutEagle
              15.11.2022 19:06

              И Вы только что реализовали паттерн "стратегия", только другими инструментами

              Хотя, нет, не совсем

              Я привёл, условно, три куска кода из трёх разных проектов. Их пишут разные команды. Сам аутентификатор пилит четвертая.

              Получается конкретно на месте у меня нет функции login. Есть экземпляр Auth, там могут быть даже какие-то предустановленные стратегии. Я его могу как есть использовать, + могу что-то своё добавить.


              1. markelov69
                15.11.2022 19:08

                И вы только что реализовали паттерн "стратегия", только другими инструментами

                Да это не инструменты, это вообще просто самая стандартная и банальная реализация)


                1. MaryRabinovich
                  15.11.2022 22:43
                  -1

                  Именно так. И паттерн - это абстракция для самых стандартных и банальных реализаций. Как это часто бывает с абстракциями, под одну абстракцию подходит сразу много разных версий конкретики. Если у вас есть переключатель и куча объектов, способных через этот переключатель выбрать себе какой-нибудь образ действий в каком-то случае, это - паттерн стратегия. Если у вас есть объект, способный дёргать скопом какую-то группу, куда можно вписываться, это - паттерн подписка. И он, очевидно, не совсем близок стратегии. При этом и тот, и этот паттерны абсолютно типичны.


            1. MaryRabinovich
              15.11.2022 19:07
              -1

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

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

              Во-первых, так меньше риска что-то сломать.

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


              1. markelov69
                15.11.2022 19:38
                +2

                Представьте себе более сложные действия, с внутренней сложной логикой каждое.

                Круто, а каким боком тут помогает стратегия?))) Мы уже выяснили что она ничего не дает и ни на что не влияет. Только носит "гордое" имя паттерн) Прям не стесняйтесь, покажите это в коде. Я хочу наконец-то выяснить где ее применять и применима ли она на самом деле вообще где-то.

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

                А if'ы нельзя разбить на отдельные файлы?

                Во-первых, так меньше риска что-то сломать.

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

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

                А на чанки только паттерн стратегия разбивается, да? import('lalla').then это всё обман, и работает только при условии что вы сборщику в разговоре по душам обмолвились о том, что вы тут не просто так, а паттерны используете, да ещё и не абы какие, а стратегию.


                1. MaryRabinovich
                  15.11.2022 22:26

                  Только носит "гордое" имя паттерн) Прям не стесняйтесь, покажите это в коде.

                  Похоже, код ни при чём.

                  Похоже, проблема в транслитерированном англоязычном термине "паттерн". Который на английском ни разу не гордый, а как раз в хвост и в гриву используется, в разных причём отраслях, для выражения того факта, что есть различные способы что-то сделать.

                  Если бы перевели словарно, было бы по-русски "способ" или "вариант выполнения задачи". Проблема со словарными переводами в том, что они не точные. В итоге часто термины не переводятся, а транслитерируются - переписываются в других литерах, в другом алфавите, но остаются на слух как были.

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

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


                  1. markelov69
                    15.11.2022 22:53

                    Так где главный ответ на вопрос, в каком случае стратегия явно лучше if'ов? Ответ в виде кода пожалуйста


                    1. MaryRabinovich
                      15.11.2022 23:13
                      -1

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

                      Ответ в виде кода пожалуйста

                      Вам проще думать на уровне кода.

                      Но выше @BerkutEagleпривёл пример, когда на уровне кода вообще не выйдет - код в разных стратегиях не просто разный, а на разных языках, и пишется в разных командах. При этом команды между собой договаривались, что делать, именно на уровне паттернов.

                      Мне тоже проще думать на уровне паттернов. Мне проще так планировать мою работу с проектом.

                      Хотя ладно, попробую выдумать что-то, где уровень кода будет уж очень запутанным, если сразу за него взяться, без паттернов.

                      Давайте запилим игру "зоопарк". В зоопарк подселяют животных, разные виды. Каждый из этих видов по-своему взаимодействует с посетителем: кто-то убегает вглубь вольера, кто-то начинает орать, сидя на месте. Кто-то подходит и что-нибудь ест из рук. Ну и засада в том, что мы не знаем, кто ещё там появится - какие ещё животные понадобятся со временем.

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

                      Как это можно реализовывать?

                      Например, иф-елсами. Вот есть событие "подошёл посетитель". Дальше каждый класс каждого животного имеет свой совершенно отдельный индивидуальный метод, что ему делать.

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

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

                      Естественно, всё это можно писать просто на уровне кода. Можно просто рефакторингом прийти к нескольким функциям для реагирования на "подошёл посетитель".

                      Но некоторым людям - не всем, но некоторым - проще заранее подумать об этом как "вот, там будет событие Подошёл посетитель, и способы реагирования вынесем-ка в стратегию".

                      Будет у нас отдельная папка "стратегии взаимодействия с посетителем".

                      Ещё будет, скажем, отдельная совершенно "стратегии взаимодействия с сотрудниками зоопарка". Распределение может быть вообще другим. Кто-то, кто убегает от посетителя, может и от сотрудника убегать. Кто-то может от посетителя убегать, но любить сотрудника. И др.

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

                      И да, всё это можно продумать и выполнить без всяких паттернов.


                      1. markelov69
                        16.11.2022 00:24

                        Вот вы сами же расписали то, что со стратегией эта задача реализуется более сложно, чем так сказать гибким методом и индивидуальным подходом. Применять инструменты/паттерны и т.д и т.п, нужно для упрощения жизни, а не для усложнения. Каждую задачу эффективно решать индивидуальным подходом, а не применять паттерн ради паттерна(в вашем случае вы это выдаете за серебряную пулю). Если говорить на языке аббревиатур, то для меня само собой разумеющиеся и первостепенные вещи это "KISS (Keep it simple, stupid)YAGNI (You aren't gonna need it)Чем меньше, тем лучше (Less is more) ". Всё остальное не имеет значения и без разницы как оно называется, стратегия, квест, стрелялка, я всегда делаю именно так, как удобно, как очевидно, код должен легко читаться и быть понятным сверху вниз, слева направо.

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

                        Вот прям так да? Прям на уровне паттернов? Т.е. не словами, что и как мы делаем, а так: Вася ты делай стратегию, Петя ты делай Наблюдателя и т.п. так получается?

                        Мне тоже проще думать на уровне паттернов. Мне проще так планировать мою работу с проектом.

                        Это вы так пытаетесь возвысить себя над теми кто думает как лучше сделать, а не паттернами?)



                        И самое главное, в чем разница в if'ами? Куда не глянь, так это одно и то же, просто назвали стратегией.


                      1. MaryRabinovich
                        16.11.2022 08:34
                        -1

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

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

                        Можете ли вы в моём комментарии выше выяснить, это игра на питоне, на джаваскрипте или ещё на чём-то? Нет. Потому что на чём угодно.

                        Вот прям так да? Прям на уровне паттернов? Т.е. не словами, что и как мы делаем, а так: Вася ты делай стратегию, Петя ты делай Наблюдателя и т.п. так получается?

                        Да, именно так. Хотя, конечно, не в одно слово, а с конкретизацией: стратегия чего именно, и в каких формах мы уже видим её ("выделяем реакцию на посетителя в стратегию, и заготавливаем три первых версии: убежать вглубь вольера, сидеть на месте орать, подойти к посетителю").

                        Мне тоже проще думать на уровне паттернов. Мне проще так планировать мою работу с проектом.

                        Это вы так пытаетесь возвысить себя над теми кто думает как лучше сделать, а не паттернами?)

                        ?

                        Все думают, как лучше сделать. И некоторым проще так (лучше) сделать именно с помощью паттернов. Я себя чувствую с паттернами не лучше, а свободнее.

                        И самое главное, в чем разница в if'ами? Куда не глянь, так это одно и то же, просто назвали стратегией.

                        Бывают ифы, бывают объекты с ключами. В РНР это будут ассоциативные массивы, а не объекты. Ну или те же ифы, свитчи. Ещё - вполне сумасшедший пример, ни разу ещё так не делала, но почему бы и нет - класс вы навешиваете из ваниллы, но анимация происходит уже на уровне стилей. И есть несколько классов с разными анимациями.

                        ______________________


                        Сказать по правде, мне надоел этот разговор, он бессмысленный. Я пишу "как я стала свободнее, познакомившись с паттернами". Вы отвечаете "вы стремитесь возвыситься". Я про паттерны, вы про самолюбие. Причём про самолюбие вы под статьёй про паттерны.

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

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


                      1. markelov69
                        16.11.2022 10:03

                        Вы так и ответили на вопрос, чем стратегия отличается от if'ов или obj[key].method() ? Судя по вашим рассуждениям вы точно это знаете, я вот нет и хочу узнать. А вам жалко ответить


                      1. MaryRabinovich
                        16.11.2022 10:17
                        -1

                        чем стратегия отличается от if'ов или obj[key].method()

                        Ничем. Вернее, сам вопрос поставлен примерно так же, как, например, "чем лисы отличаются от млекопитающих"

                        • лисы от млекопитающих не отличаются - лисы являются вариантом млекопитающих

                        • есть ещё дофига млекопитающих, они не лисы

                        • есть ещё варианты помимо млекопитающих, например мухи. Хотя скорее не мухи, а насекомые, или членистоногие, если мы хотим выдерживать сравнения на одном каком-то родственном уровне абстракции

                        Добавлю, кстати, что классификация в биологии возникла позже, чем описания отдельных видов. И так же с паттернами - паттерны были вычленены из уже привычных способов писать код. И как вообще и что вычленять - отдельная тема, там не всё гладко. Есть другие системы паттернов, помимо классического набора "банды четырёх". А та же "банда четырёх" старательно упирает на то, что предложили они только некоторый набор паттернов, а не исчерпывающую систему.


                      1. markelov69
                        16.11.2022 10:21

                        Ничем.

                        Ну вот, значит я правильно думаю. Спасибо за подтверждение.


                      1. MaryRabinovich
                        16.11.2022 11:03

                        Ничем на уровне кода. Как лисы на уровне лис - это не столько млекопитающие, сколько лисы. Глупо в беседе конкретно о лисах думать о млекопитающих.

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

                        Т.е., если вы в принципе только по лисам, вам может быть вообще ни разу в жизни не важно, относится ли она к царству грибов или нет. Это всё время конкретные лисы, конкретный набор иф-элс.

                        Но если у вас зоопарк или биолаборатория, вам уже требуются абстракции, классифицирующие лис и ещё кого-то в один общий кластер.

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

                        Я на днях написала плагин для хрома для локального бана в комментариях - см. у меня в статье "Habracleaner, плагин для Chrome". Ну и там джаваскриптовых ровно два файла - один занят формой вверху страницы, другой - комментариями. И там не то, что паттернами не пахнет, там вообще лапшекод практически. Ну потому что была задача конкретная, примитивная, без каких-либо планов по её расширению.

                        Т.е., никто не стремится пихать всюду паттерны ради паттернов.


                      1. markelov69
                        16.11.2022 11:15
                        +1

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

                        Т.е. программу которую можно расширять и поддерживать можно писать только когда ты думаешь паттернами, стратегиями, стрелялкам, квестами, а не просто думаешь как сделать хорошо, просто, понятно, очевидно и надежно так?


                      1. MaryRabinovich
                        16.11.2022 12:03

                        Т.е. программу которую можно расширять и поддерживать можно писать только когда...

                        Найдите, пожалуйста, у меня слово "только"?

                        Если вам даже на многослойных многокомандных проектах с поддержкой на много лет удобнее без чего бы то ни было кроме KISS, DRY и YAGNI, ну так и флаг вам в руки.

                        А для чего вам, кстати, все эти KISS и DRY, почему бы не просто сосредоточиться на читаемости, расширяемости и простоте? (сарказм, но всё таки)


                      1. markelov69
                        16.11.2022 12:18

                        А для чего вам, кстати, все эти KISS и DRY, почему бы не просто сосредоточиться на читаемости, расширяемости и простоте?

                        KISS, DRY и YAGNI это и есть про читаемость, очевидность и простоту. Но я не применяю KISS, DRY и YAGNI преднамеренно, просто эти определения подходят под то, как я пишу код. А я пишу его думая своей головой, а не применяю паттерны чтобы утешить чье-то эго и казаться "умнее" за счет этого.

                        Если вам даже на многослойных многокомандных проектах с поддержкой на много лет удобнее без чего бы то ни было кроме KISS, DRY и YAGNI, ну так и флаг вам в руки.

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


                      1. MaryRabinovich
                        16.11.2022 12:21

                        Но я не применяю ХХХ преднамеренно, просто эти определения подходят под то, как я пишу код

                        (это про паттерны и про меня)

                        если можно делать ровно тоже самое и даже лучше без этого

                        А тут вопрос. Есть ли у вас реально возможность сравнивать? Есть ли у вас возможность сравнить, как вы сами пишете с паттернами и без? Мне пока что кажется, что паттерны как таковые для вас выглядят гиперсложно, и вы выдумываете причины, чтобы от них держаться подальше гордо. Не просто так держаться от них подальше, а именно агрессивно держаться. Скажем, прийти под статью о паттернах и написать "какая дрянь эта ваша заливная рыба".

                        Т.е., пока что мне слабо верится, что вы уже написали что-то на паттернах, и постфактум видите, что нет, это вам не дало ни черта, кроме будничных утренних драм.

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


                      1. markelov69
                        16.11.2022 12:31

                        Я просто называю вещи своими именами. Не Стратегия, а if'ы, не Наблюдатель, а pub/sub или EventEmitter, не Функция высшего порядка, а функция обертка, и т.д. и т.п. Это резко снижает значимость оперирования этими терминами, потому что за ними ничего особенного не стоит, вот вы и беситесь))


                      1. MaryRabinovich
                        16.11.2022 12:51

                        Я не бешусь, я офигеваю и ржу. Это другое совсем.

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

                        А я ещё преподаю. И для меня это любопытная педагогическая ситуация: попробовать так и эдак взаимодействовать с человеком, не просто не шибко владеющим каким-то методом, но громогласно (см. первый ваш комментарий в треде) его обесценивающим.

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


                      1. markelov69
                        16.11.2022 12:56

                        Так я же говорю:

                        Я просто называю вещи своими именами. Не Стратегия, а if'ы, не Наблюдатель, а pub/sub или EventEmitter, не Функция высшего порядка, а функция обертка, и т.д. и т.п. Это резко снижает значимость оперирования этими терминами, потому что за ними ничего особенного не стоит

                        А вы:

                        Я не бешусь, я офигеваю и ржу. Это другое совсем.

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

                        Не Стратегия, а if'ы, не Наблюдатель, а pub/sub или EventEmitter, не Функция высшего порядка, а функция обертка, и т.д. и т.п.

                        Это не так?

                        Люди, перестающие отвечать, не "поняли вашу правоту", а просто махнули рукой.

                        Нет, просто факту они поняли, что Стратегия это ничто иное как if'ы или obj[key].exec(). А вы это уже признали, но упорно пытаетесь возвысить этот термин над if'ами всё равно, только не ясно почему?)


                      1. MaryRabinovich
                        16.11.2022 13:05

                        Дальше всё именно что относится к теме. И я раз за разом отвечаю на ваши вопросы. Просто вы не прочитываете ответы. То ли вы не понимаете эти ответы, уже прочитав, то ли даже не смотрите, что там написано.

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

                        У вас то же самое. То ли не можете прочитать, то ли прочитываете, но не понимаете, то ли даже не пробуете вчитыватся.

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

                        Возможно, как раз поэтому вы не прочитываете в моих ответах 90% сути. Вы глубочайше сосредоточены на поиске подтверждений того, что паттерны не нужны. Поэтому, скажем, в моём ответе "ничем они не отличаются" вы эту фразу заметили, и... и всё. Дальше, похоже, вы тот комментарий не видели. А он есть.


                      1. markelov69
                        16.11.2022 13:07

                        Нe ладно, ещё раз:

                        Не Стратегия, а if'ы, не Наблюдатель, а pub/sub или EventEmitter, не Функция высшего порядка, а функция обертка, и т.д. и т.п.

                        Это не так?


                      1. MaryRabinovich
                        16.11.2022 13:14

                        Вот полностью начало моего комментария (намного) выше:

                        Ничем. Вернее, сам вопрос поставлен примерно так же, как, например, "чем лисы отличаются от млекопитающих"

                        И там ещё много буков.

                        Но вы там в ответ написали кратенько, вот так вот:

                        Ничем

                        Ну вот, значит я правильно думаю. Спасибо за подтверждение.

                        _________________

                        Сейчас, кстати, перечитав тот мой комментарий ("Ничем. Вернее, сам вопрос поставлен примерно так же..."), я вижу, что мне тут к нему добавить наглухо нечего. Если хотите каких-то моих ответов, пожалуйста, перечитайте его.

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


                      1. markelov69
                        16.11.2022 13:22

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


                      1. MaryRabinovich
                        16.11.2022 13:29
                        -1

                        Хорошо. Вы правы:)


            1. Homer4ik
              16.11.2022 14:35
              +1

              В статье приведен "каноничная" реализация паттерна через интерфейс. В вашем же случае FK(...), LDAP(...) и т.д. - это те же самые стратегии, только реализованны через делегат.

              В чем преимущество предложенного варианта над вашим? Зависит от контекста. Коль статья про тайпскрипт, представим, что нам так же и logout надо реализовать. В предложенном варианте - добавили с интерфейс нужный метод и дальше компилятор сам сообщит, в какой из реализаций мы забыли его реализовать. А в вашем варианте?


              1. markelov69
                16.11.2022 14:57

                Коль статья про тайпскрипт, представим, что нам так же и logout надо реализовать. В предложенном варианте - добавили с интерфейс нужный метод и дальше компилятор сам сообщит, в какой из реализаций мы забыли его реализовать. А в вашем варианте?

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

                class VK implements Auth { ... }

                И когда добавим к интерфейсу Auth новый обязательный метод, например logout как вы и говорили, то да, компилятор будет ругаться до тех пор, пока мы его везде не реализуем.

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


    1. evgeniyPP
      15.11.2022 21:47
      +1

      В статье, видимо, забыли указать, что в функции login должна быть еще какая-то общая логика, иначе всё это бессмысленно и проще просто саму функцию вызывать)
      Т.е., почему бы тогда вместо login('twitter', '123'); сразу не написать loginWithTwitter('123');? Количество символов то же, лишних абстракций меньше.

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

      ООП тут, конечно, не нужно, в статье оно просто для понтов, "стратегию" можно реализовать и простым колбэком:

      import { login } from './auth';
      import { loginWithTwitter, loginWithLocal } from './auth/drivers';
      
      login(loginWithTwitter, '123');
      login(loginWithLocal, "bytefer", "666");
      


  1. bromzh
    15.11.2022 15:04
    +5

    Наличие any в каждом из примеров огорчает. Не так уж сложно нормально типизировать всё это.


  1. pu5her
    16.11.2022 01:27
    +1

    Важным функционалом почти любого веб-приложения является регистрация пользователя (аутентификация) и выполнение пользователем входа в систему (авторизация)

    аутентификация это проверка подлинности, а авторизация предоставление доступа.


  1. Gagydzer
    16.11.2022 14:35

    В ООП новичек, смотрю реализацию Handler, можете объяснить для чего в class Route в super.get передается callback? он же ни где по цепочке use не будет вызван, или я ошибаюсь? А если все же и будет вызван, то в дальнейшем в этом методе он будет вызван повторно и мы получим два результата на route.get?