Метод скидки разрос...
 
Уведомления
Очистить всё

Метод скидки разросся до 40 строк вложенных if. Как разложить?

5 Сообщения
4 Пользователи
0 Реакции
623 Просмотры
eldar14
Сообщения: 9
Автор темы
Дружит с bundler middle-разработчик
Присоединился: 4 года назад
[#1]

Проект на Roda, Ruby 3.1. Есть метод расчёта скидки, живёт в хелпере, и каждый месяц к нему добавляют ещё одно условие: то праздничная акция, то скидка за первый заказ. Сейчас там под сорок строк, ниже самый страшный кусок. Тестов на него три, и все про VIP.

def discount_for(order)
  if order.customer.vip?
    if order.total > 10_000
      15
    else
      10
    end
  else
    if order.promo_code
      if order.promo_code.expired?
        0
      else
        order.promo_code.percent
      end
    elsif order.items.size >= 5
      5
    else
      0
    end
  end
end

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


4 ответа
kostya.sokurov
Сообщения: 73
Держит прод бэкенд-разработчик, senior
Присоединился: 4 года назад

Я бы не прыгал сразу в паттерны. Сначала ранние return, и половина вложенности уходит сама:

def discount_for(order)
  return vip_discount(order) if order.customer.vip?
  return promo_discount(order.promo_code) if order.promo_code
  return 5 if order.items.size >= 5

  0
end

def vip_discount(order)
  order.total > 10_000 ? 15 : 10
end

def promo_discount(code)
  code.expired? ? 0 : code.percent
end

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


Ответ
kirill_spb
Сообщения: 27
Собрал свой gem разработчик инструментов
Присоединился: 4 года назад

Если правил станет штук пятнадцать, я бы держал их списком. Новое правило - новая строка в массиве, порядок задаёт приоритет:

Rule = Struct.new(:applies, :percent)

RULES = [
  Rule.new(->(o) { o.customer.vip? && o.total > 10_000 }, ->(_) { 15 }),
  Rule.new(->(o) { o.customer.vip? }, ->(_) { 10 }),
  Rule.new(->(o) { o.promo_code&.expired? == false }, ->(o) { o.promo_code.percent }),
  Rule.new(->(o) { o.promo_code.nil? && o.items.size >= 5 }, ->(_) { 5 })
].freeze

def discount_for(order)
  rule = RULES.find { |r| r.applies.(order) }
  rule ? rule.percent.(order) : 0
end

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


Ответ
karina_sor
Сообщения: 6
Дружит с bundler бэкенд-разработчица
Присоединился: 4 года назад

kirill_spb, а если скидки начнут суммироваться? VIP плюс промокод, и find с первым совпадением уже не годится, придётся собирать все


Ответ
eldar14
Сообщения: 9
Автор темы
Дружит с bundler middle-разработчик
Присоединился: 4 года назад

Сделал пока как у kostya.sokurov: ранние return и два маленьких метода, а перед этим шесть тестов на все ветки. Метод стал на 12 строк, правило про первый заказ добавилось одной строкой. Список правил отложил. karina_sor как в воду глядела: маркетинг уже намекал, что хочет VIP плюс промокод, тогда буду собирать все подходящие правила и брать максимум или сумму, как договорятся.


Ответ