JavaScript

Применение шаблонов проектирования во Vue.js: шаблон Стратегия

Spread the love

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

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

Проблема: хирургия дробовиком (Shotgun Surgery)

Shotgun Surgery — это такой подход к архитектуре кода, в котором любые модификации требуют внесения множества мелких изменений во многих разных местах.


(image source: https://refactoring.guru/smells/shotgun-surgery)

Как это может происходить в проекте? Давайте представим, что нам нужно создать ценовые карты (pricing cards) для продукта, и нам нужно будет менять цену, валюту, стратегию скидок и сообщения в зависимости от того, откуда пришел клиент:

Большинство из нас, вероятно, реализовали бы ценовую карту следующим образом:

  • Создали компоненты: PricingCard, PricingHeader, PricingBody.
  • Добавили бы вспомогательные функции: getDiscountMessage (in utils/discount.js), formatPriceByCurrency (in utils/price.js).
  • Добавили бы компонент PricingBody которые высчитывает конечную цену.

Структура проекта могла бы выглядеть так

...
   components
      PricingCard.vue
      PricingCardBody.vue
      PricingCardHeader.vue
   utils
      discount.js
      price.js
      types.js
   App.vue

Архив с полной версией проекта на Vue.js с Composition API

Теперь давайте представим, что нам нужно изменить тарифный план для определенный страны или добавить новый тарифный план для другой страны. Что вам придется делать с приведенной выше реализацией? Вам нужно будет изменить по крайней мере 3 места и добавить больше условных выражений в и без того запутанные блоки if-else:

  • Изменить компонент PricingBody.
  • Изменить функцию getDiscountMessage.
  • Изменить функцию formatPriceByCurrency.

Если вы уже слышали о S.O.L.I.D, мы уже нарушаем первые два принципа: принцип единой ответственности (The Single Responsibility Principle) и принцип открытости-закрытости (The Open-Closed Principle).

Решение: Шаблон стратегии

Шаблон стратегии довольно прост. Мы можем просто представить, что каждый из наших тарифных планов для стран — это стратегия. И в этом классе стратегии мы реализуем всю связанную логику для этой стратегии.

Предположим, вы знакомы с ООП, у нас может быть абстрактный класс (PriceStrategy), который реализует общую/общую логику, а затем стратегия с другой логикой будет наследовать этот абстрактный класс.

Поменять структуру проекта на:

...
   components
      PricingCard.vue
      PricingCardBody.vue
      PricingCardHeader.vue
   services
      priceStrategies
          PriceStrategy.js
          JapanPriceStrategy.js
          StandardPriceStrategy.js
          VietnamPriceStrategy.js
   utils
      discount.js
      price.js
      types.js
   App.vue

Абстрактный класс PriceStrategy будет выглядит следующим образом:

import { Country, Currency } from '../../utils/types.js';

class PriceStrategy {
  country = Country.AMERICA;
  currency = Currency.USD;
  discountRatio = 0;

  getCountry() {
    return this.country;
  }

  formatPrice(price) {
    return [this.currency, price.toLocaleString()].join('');
  }

  getDiscountAmount(price) {
    return price * this.discountRatio;
  }

  getFinalPrice(price) {
    return price - this.getDiscountAmount(price);
  }

  shouldDiscount() {
    return this.discountRatio > 0;
  }

  getDiscountMessage(price) {
    const formattedDiscountAmount = this.formatPrice(
      this.getDiscountAmount(price)
    );

    return `It's lucky that you come from ${this.country}, because we're running a program that discounts the price by ${formattedDiscountAmount}.`;
  }
}

export default PriceStrategy;

И мы просто передаем созданную стратегию в качестве prop компоненту PricingCard:

<PricingCardHeader />
    <div class="divider" />
    <PricingCardBody
      :price="props.price"
      :strategy="props.strategy"
    />

Опять же, если вы знакомы с ООП, мы используем здесь не только наследование, но и полиморфизм.

Полная реализация решения:

И давайте снова зададим тот же вопрос: как добавить новый тарифный план для новой страны? В этом решении нам просто нужно добавить новый класс стратегии, и нам не нужно изменять какой-либо существующий код. Поступая таким образом, мы также удовлетворяем S.O.L.I.D.

Заключение

Итак, обнаружив архитектуру проекта по типу — Shotgun Surgery — в нашей кодовой базе, мы применили шаблон проектирования — Strategy Pattern — для его решения. Наша структура кода исходила из этого:

и была преобразована в:

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

Статья написана на основе: Applying Design Patterns in React: Strategy Pattern

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

Spread the love
DenSP

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

12 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

12 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

Когда и как выбирать между медиа запросами и контейнерными запросами

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

2 года ago