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

«Как переделать сервис подсчёта скидки, чтобы способ расчёта зависел от типа пользователя?» — вопрос из категории ООП, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Для реализации гибкой системы скидок, зависящей от типа пользователя, я бы применил паттерн Стратегия (Strategy). Это позволяет инкапсулировать различные алгоритмы расчёта и выбирать их во время выполнения.

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

// src/Service/Discount/Strategy/DiscountStrategyInterface.php
interface DiscountStrategyInterface
{
    public function calculate(float $originalAmount): float;
}

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

// src/Service/Discount/Strategy/RegularCustomerDiscountStrategy.php
class RegularCustomerDiscountStrategy implements DiscountStrategyInterface
{
    public function calculate(float $originalAmount): float
    {
        // Скидка 5% для обычных клиентов
        return $originalAmount * 0.95;
    }
}

// src/Service/Discount/Strategy/VipCustomerDiscountStrategy.php
class VipCustomerDiscountStrategy implements DiscountStrategyInterface
{
    public function calculate(float $originalAmount): float
    {
        // Скидка 15% для VIP
        return $originalAmount * 0.85;
    }
}

// src/Service/Discount/Strategy/NewYearDiscountStrategy.php
class NewYearDiscountStrategy implements DiscountStrategyInterface
{
    public function calculate(float $originalAmount): float
    {
        // Новогодняя скидка 10% всем
        return $originalAmount * 0.90;
    }
}

3. Создаём контекст (сервис), который использует стратегию:

// src/Service/Discount/DiscountCalculator.php
class DiscountCalculator
{
    private DiscountStrategyInterface $strategy;

    // Стратегия внедряется через конструктор (Dependency Injection)
    public function __construct(DiscountStrategyInterface $strategy)
    {
        $this->strategy = $strategy;
    }

    // Метод для смены стратегии на лету (опционально)
    public function setStrategy(DiscountStrategyInterface $strategy): void
    {
        $this->strategy = $strategy;
    }

    public function calculate(float $amount): float
    {
        // Делегируем расчёт текущей стратегии
        return $this->strategy->calculate($amount);
    }
}

4. Использование в коде приложения:

// Определяем тип пользователя (например, из БД или сессии)
$userType = $user->getType(); // 'vip', 'regular'

// Выбираем стратегию на основе типа пользователя
// Это можно вынести в фабрику или конфигурацию DI-контейнера
switch ($userType) {
    case 'vip':
        $strategy = new VipCustomerDiscountStrategy();
        break;
    case 'seasonal':
        $strategy = new NewYearDiscountStrategy();
        break;
    default:
        $strategy = new RegularCustomerDiscountStrategy();
}

// Создаём калькулятор с нужной стратегией
$discountCalculator = new DiscountCalculator($strategy);
$finalPrice = $discountCalculator->calculate(1000); // 850 для VIP

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

  • Открыто/закрыто: Легко добавить новую стратегию скидок (BlackFridayDiscountStrategy), не меняя код калькулятора или других стратегий.
  • Чистота кода: Каждый алгоритм в отдельном классе.
  • Тестируемость: Стратегии можно тестировать изолированно.