Проект на 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
Как бы вы его разложили, чтобы новое правило добавлялось одной строкой, а не ещё одним уровнем вложенности?
Я бы не прыгал сразу в паттерны. Сначала ранние 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. Без них переделка будет на честном слове.
Если правил станет штук пятнадцать, я бы держал их списком. Новое правило - новая строка в массиве, порядок задаёт приоритет:
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.
kirill_spb, а если скидки начнут суммироваться? VIP плюс промокод, и find с первым совпадением уже не годится, придётся собирать все
Сделал пока как у kostya.sokurov: ранние return и два маленьких метода, а перед этим шесть тестов на все ветки. Метод стал на 12 строк, правило про первый заказ добавилось одной строкой. Список правил отложил. karina_sor как в воду глядела: маркетинг уже намекал, что хочет VIP плюс промокод, тогда буду собирать все подходящие правила и брать максимум или сумму, как договорятся.