← Назад к вопросам

Как переделать сервис подсчета скидки чтобы способ подсчета зависел от пользователя?

2.0 Middle🔥 122 комментариев
#Архитектура и паттерны

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

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Архитектура гибкой системы расчетов скидок

Для создания системы, где способ подсчета скидки зависит от пользователя, необходимо реализовать стратегию (Strategy Pattern) или правила (Rule Engine). Вот комплексное решение:

Ключевые концепции

  1. Абстракция расчета: Выделяем интерфейс для всех алгоритмов скидок
  2. Контекст пользователя: Определяем, какая стратегия применяется к конкретному пользователю
  3. Динамическое подключение: Возможность менять логику без изменения основного кода

Базовая реализация на PHP

Шаг 1: Определяем интерфейс стратегии

<?php

namespace App\Services\Discount;

interface DiscountStrategyInterface
{
    /**
     * Рассчитывает финальную сумму после скидки
     * 
     * @param float $originalAmount Исходная сумма
     * @param User $user Объект пользователя
     * @return float Итоговая сумма
     */
    public function calculate(float $originalAmount, User $user): float;
    
    /**
     * Проверяет, применима ли стратегия к пользователю
     */
    public function supports(User $user): bool;
}

Шаг 2: Реализуем конкретные стратегии

<?php

namespace App\Services\Discount\Strategies;

class VipDiscountStrategy implements DiscountStrategyInterface
{
    public function calculate(float $originalAmount, User $user): float
    {
        // Скидка 20% для VIP-пользователей
        return $originalAmount * 0.8;
    }
    
    public function supports(User $user): bool
    {
        return $user->isVip() && $user->getLoyaltyLevel() >= 3;
    }
}

class FirstPurchaseDiscountStrategy implements DiscountStrategyInterface
{
    public function calculate(float $originalAmount, User $user): float
    {
        // Фиксированная скидка для первой покупки
        return max($originalAmount - 500, 0);
    }
    
    public function supports(User $user): bool
    {
        return $user->getPurchaseCount() === 0;
    }
}

class VolumeDiscountStrategy implements DiscountStrategyInterface
{
    public function calculate(float $originalAmount, User $user): float
    {
        // Скидка в зависимости от суммы покупок за год
        $annualSpent = $user->getAnnualSpent();
        
        if ($annualSpent > 50000) {
            return $originalAmount * 0.7; // 30%
        } elseif ($annualSpent > 20000) {
            return $originalAmount * 0.85; // 15%
        }
        
        return $originalAmount;
    }
    
    public function supports(User $user): bool
    {
        return $user->getAnnualSpent() > 10000;
    }
}

Шаг 3: Создаем фабрику или сервис-локатор

<?php

namespace App\Services\Discount;

class DiscountCalculator
{
    /** @var DiscountStrategyInterface[] */
    private array $strategies = [];
    
    public function __construct(iterable $strategies)
    {
        foreach ($strategies as $strategy) {
            if ($strategy instanceof DiscountStrategyInterface) {
                $this->strategies[] = $strategy;
            }
        }
    }
    
    public function calculate(User $user, float $amount): float
    {
        // Находим подходящую стратегию для пользователя
        foreach ($this->strategies as $strategy) {
            if ($strategy->supports($user)) {
                return $strategy->calculate($amount, $user);
            }
        }
        
        // Возвращаем оригинальную сумму, если стратегия не найдена
        return $amount;
    }
    
    /**
     * Для случаев, когда нужно применить несколько стратегий
     */
    public function calculateWithAllApplicable(User $user, float $amount): float
    {
        $finalAmount = $amount;
        
        foreach ($this->strategies as $strategy) {
            if ($strategy->supports($user)) {
                $finalAmount = $strategy->calculate($finalAmount, $user);
            }
        }
        
        return $finalAmount;
    }
}

Шаг 4: Конфигурация в Symfony/Laravel

Для Symfony:

# config/services.yaml
services:
    App\Services\Discount\DiscountCalculator:
        arguments:
            - !tagged_iterator app.discount_strategy
    
    App\Services\Discount\Strategies\VipDiscountStrategy:
        tags: ['app.discount_strategy']
    
    App\Services\Discount\Strategies\FirstPurchaseDiscountStrategy:
        tags: ['app.discount_strategy']

Для Laravel:

// AppServiceProvider.php
$this->app->tag([
    VipDiscountStrategy::class,
    FirstPurchaseDiscountStrategy::class,
    VolumeDiscountStrategy::class,
], 'discountStrategies');

$this->app->bind(DiscountCalculator::class, function ($app) {
    return new DiscountCalculator($app->tagged('discountStrategies'));
});

Расширенная архитектура с хранением правил в БД

Для максимальной гибкости можно хранить правила в базе данных:

<?php

namespace App\Services\Discount;

class DatabaseRuleDiscountStrategy implements DiscountStrategyInterface
{
    private EntityManagerInterface $em;
    
    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }
    
    public function calculate(float $originalAmount, User $user): float
    {
        $rules = $this->em->getRepository(DiscountRule::class)
            ->findActiveRulesForUser($user);
        
        $finalAmount = $originalAmount;
        
        foreach ($rules as $rule) {
            $finalAmount = $this->applyRule($rule, $finalAmount, $user);
        }
        
        return $finalAmount;
    }
    
    public function supports(User $user): bool
    {
        // Всегда проверяем правила из БД
        return true;
    }
    
    private function applyRule(DiscountRule $rule, float $amount, User $user): float
    {
        // Логика применения правила из БД
        switch ($rule->getType()) {
            case 'percentage':
                return $amount * (1 - $rule->getValue() / 100);
            case 'fixed':
                return max($amount - $rule->getValue(), 0);
            case 'threshold':
                return $amount >= $rule->getThreshold() 
                    ? $amount * (1 - $rule->getValue() / 100)
                    : $amount;
        }
        
        return $amount;
    }
}

Преимущества подхода

  • Гибкость: Новые типы скидок добавляются без изменения существующего кода
  • Тестируемость: Каждую стратегию можно тестировать изолированно
  • Масштабируемость: Легко добавлять сложную логику (комбинации скидок, временные акции)
  • Управление через админку: При использовании БД-подхода бизнес-логика управляется без деплоя
  • Приоритизация: Стратегии проверяются в определенном порядке (можно добавить приоритет)

Рекомендации по использованию

  1. Кэширование: Кэшируйте результаты для частых запросов
  2. Валидация: Добавьте проверку на минимальную итоговую цену
  3. Логирование: Фиксируйте примененные скидки для анализа
  4. Мониторинг: Отслеживайте эффективность разных стратегий через метрики

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