← Назад к вопросам
Как переделать сервис подсчета скидки чтобы способ подсчета зависел от пользователя?
2.0 Middle🔥 122 комментариев
#Архитектура и паттерны
Комментарии (2)
🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Архитектура гибкой системы расчетов скидок
Для создания системы, где способ подсчета скидки зависит от пользователя, необходимо реализовать стратегию (Strategy Pattern) или правила (Rule Engine). Вот комплексное решение:
Ключевые концепции
- Абстракция расчета: Выделяем интерфейс для всех алгоритмов скидок
- Контекст пользователя: Определяем, какая стратегия применяется к конкретному пользователю
- Динамическое подключение: Возможность менять логику без изменения основного кода
Базовая реализация на 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;
}
}
Преимущества подхода
- Гибкость: Новые типы скидок добавляются без изменения существующего кода
- Тестируемость: Каждую стратегию можно тестировать изолированно
- Масштабируемость: Легко добавлять сложную логику (комбинации скидок, временные акции)
- Управление через админку: При использовании БД-подхода бизнес-логика управляется без деплоя
- Приоритизация: Стратегии проверяются в определенном порядке (можно добавить приоритет)
Рекомендации по использованию
- Кэширование: Кэшируйте результаты для частых запросов
- Валидация: Добавьте проверку на минимальную итоговую цену
- Логирование: Фиксируйте примененные скидки для анализа
- Мониторинг: Отслеживайте эффективность разных стратегий через метрики
Такой подход полностью отделяет бизнес-логику расчета скидок от основного кода приложения и позволяет гибко управлять правилами в зависимости от характеристик пользователя.