Single Responsibility

Прин­цип един­ствен­ной обя­зан­но­сти / ответ­ствен­но­сти (single responsibility principle) обо­зна­ча­ет, что каж­дый объ­ект дол­жен иметь одну обя­зан­ность и эта обя­зан­ность должна быть полностью инкап­су­ли­ро­вана в класс. Все его сер­висы должны быть направ­лены исклю­чи­тельно на обес­пе­че­ние этой обя­зан­но­сти.

В качестве примера, нарушающего данный принцип, рассмотрим следующую реализацию:
Допустим, мы пишем web приложение для интернет магазина. У нас есть сущность товара и сущность заказа, который включает в себе несколько товаров. При оформлении заказа, мы считаем сумму для оплаты исходя из стоимости товаров и НДС. Ниже приведены шаги реализации.

1. Определяем класс товара

1
2
3
4
5
6
7
8
// Товар
class Item
{
    // Название товара
    public $title;
    // Цена товара
    public $cost;
}

2. Определяем класс заказа

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Заказ
class Order
{
    /**
     * Массив для хранения товаров
     * @var Item[] $items
     */

    private $items = [];

    // Добавляем товар к заказу
    public function addItem(Item $item)
    {
        $this->items[] = $item;
    }

    // Вычисляет сумму заказа без НДС
    public function calcSum()
    {
        $sum = 0;

        // Суммируем стоимость всех товаров
        foreach ($this->items as $item) {
            $sum +=  $item->cost;
        }
       
        return $sum;
    }
   
    // Вычисляет сумму заказа с учетом НДС для заданной страны
    public function calcSumWithVat($country)
    {
        // Считаем стоимость заказа
        $sum = $this->calcSum();

        // Умножаем на коэффициент НДС
        switch ($country) {
            case 'RUSSIA':
                $sum *= 1.18;
                break;
            case 'USA':
                $sum *= 1.08;
                break;
        }

        return $sum;
    }
}

3. Создаем заказ

1
2
3
4
5
6
7
8
9
10
$order = new Order();

// Создаем товар - часы
$watch = new Item();
$watch->title = 'Apple watch';
$watch->cost = 500;
$order->addItem($watch);

// Считаем сумму заказа
$sum = $order->calcSumWithVat('RUSSIA');

В данном примере, класс Order отвечает за расчет стоимости заказа с учетом НДС, также за хранение товаров. У нашего класса не единственная ответственность. Допустим, наш заказчик решил открыть свой бизнес в Японии и хочет, чтобы мы добавили в систему поддержку для этой страны. Помимо многих мест в проекте, нам придется ещё менять метод расчета общей стоимости заказа с учетом НДС. И это будет происходить каждый раз, когда мы будем добавлять поддержку для новой страны.

Чтобы исправить проблему мы можем вынести функционал для расчета НДC в отдельный класс VatCalculator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Класс для расчета НДС
class VatCalculator
{
    const COUNTRY_RUSSIA = 'RUSSIA';
    const COUNTRY_USA = 'USA';

    // Таблица налогов по странам
    protected $vatTable = [
      self::COUNTRY_RUSSIA => 0.18,
      self::COUNTRY_USA => 0.08
    ];

    // Вычисляет сумму заказа с учетом НДС для заданной страны
    public function calcVat($country, $sum)
    {
        if (array_key_exists($country, $this->vatTable)) {
            return $sum * $this->vatTable[$country];
        }

        throw new \Exception('Не удалось определить уровень НДС для страны ' . $country);
    }
}

и считать сумму заказа с учетом НДС следующим образом

1
2
3
4
5
6
7
8
9
// Считаем сумму заказа
$sum = $order->calcSum();

// Вычисляем НДС
$vatCalculator = new VatCalculator();
$vat = $vatCalculator->calcVat(VatCalculator::COUNTRY_RUSSIA, $sum);

// Общая сумма для оплаты ()
$sumForPayment = $sum + $vat;

Тем самым мы добились того, чтобы у каждого класса была единственная ответственность.