Слава Україні!

Принципы SOLID

Пять принципов объектно-ориентированного программирования и проектирования
28-го липня 2020, 0:58
Вот как расшифровывается акроним SOLID:
  • S: Single Responsibility Principle (Принцип единственной ответственности).
  • O: Open-Closed Principle (Принцип открытости-закрытости).
  • L: Liskov Substitution Principle (Принцип подстановки Барбары Лисков).
  • I: Interface Segregation Principle (Принцип разделения интерфейса).
  • D: Dependency Inversion Principle (Принцип инверсии зависимостей).

1. S - Принцип единственной ответственности

Класс должен быть ответственен лишь за что-то одно. Если класс отвечает за решение нескольких задач, его подсистемы, реализующие решение этих задач, оказываются связанными друг с другом. Изменения в одной такой подсистеме ведут к изменениям в другой.


class Char
{
    private $name;

    public function getName()
    {
        return $this->name;
    }

    public function saveChar()
    {
        // save $this
    }
}
 

Класс Char описывает какого-то персонажа. И этот класс нарушает принцип единственной ответственности. Ведь, в соответствии с этим принципом, класс должен решать лишь какую-то одну задачу. Он же решает две, занимаясь работой с хранилищем данных в методе saveChar и манипулируя свойствами объекта в методе getName.

Если изменится порядок работы с хранилищем данных, используемым приложением, то придётся вносить изменения во все классы, работающие с хранилищем. Такая архитектура не отличается гибкостью, изменения одних подсистем затрагивают другие, что напоминает эффект домино.

Для того чтобы привести вышеприведённый код в соответствие с принципом единственной ответственности, создадим ещё один класс, единственной задачей которого является работа с хранилищем, в частности — сохранение в нём объектов класса SportCar


class Char
{
    private $name;

    public function getName()
    {
        return $this->name;
    }
}

class SaveChar
{
    public function saveChar(Char $char)
    {
        // save $char
    }
}
 

2. O - Принцип открытости-закрытости

Программные сущности (классы, модули, функции) должны быть открыты для расширения, но не для модификации.

Мы хотим перебрать список персонажей, и узнать о том, чем они занимаются. Представим, что мы решаем эту задачу с помощью функции charBehavior


class Orc
{
    //...
}

class Dwarf
{
    //...
}

 class WhatDo
{
    //...
    public function charBehavior($char)
    {
        $behavior = null;
        if($char instanceof Orc) {
            $behavior = 'Точит топор';
        } else if($char instanceof Dwarf) {
            $behavior = 'Чинит доспехи';
        }
        echo $behavior;
        return;
    }
}
 

Самая главная проблема такой архитектуры заключается в том, что функция определяет то, чем занят тот или иной персонаж, анализируя конкретные объекты. Функция charBehavior не соответствует принципу открытости-закрытости, так как, например, при появлении новых персонажей, нам, для того, чтобы с её помощью можно было бы узнавать их поведение, придётся её изменить.

Например появился эльф:


class Elf
{
    //...
} 

После этого нам придётся поменять код функции charBehavior


//...
public function charBehavior($char)
{
    $behavior = null;
    if($char instanceof Orc) {
        $behavior = 'Точит топор';
    } else if($char instanceof Dwarf) {
        $behavior = 'Чинит доспехи';
    } else if ($char instanceof Elf) {
        $behavior = 'Спрятался в лесу';
    }
    echo $behavior;
    return;
} 

Как видите, при добавлении в массив нового персонажа придётся дополнять код функции, добавляя в неё новые выражения if.

Решение здесь состоит в том, чтобы создать простой интерфейс Char, который имеет метод charBehavior и будет реализован всеми другими персонажами.


interface Char
{
    //...
    public function charBehavior()
}

class Orc implements Char
{
    //...
    public function charBehavior()
    {
        return 'Точит топор';
    }
}

class Dwarf implements Char
{
    //...
    public function charBehavior()
    {
        return 'Чинит доспехи';
    }
}

class Elf implements Char
{
    //...
    public function charBehavior()
    {
        return 'Спрятался в лесу';
    }
}

class WhatDo
{
    //...
    public function behavior($char)
    {
        echo $char->charBehavior();
        return;
    }
} 

3. L - Принцип подстановки Барбары Лисков

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

Цель этого принципа заключаются в том, чтобы классы-наследники могли бы использоваться вместо родительских классов, от которых они образованы, не нарушая работу программы. Если оказывается, что в коде проверяется тип класса, значит принцип подстановки нарушается.

Оставим в покое орков и попьем кофе... Честно говоря, мне просто очень понравился этот пример.

Итак, представьте, что у Вас две кофемашины - базовая и премиальная. Кофемашины одинаковы, единственная разница в том, что премиальная машина делает и ESPRESSO и AMERICANO, а базовая - только ESPRESSO. Основные действия программы должны быть одинаковы для обеих машин.


interface CoffeeMachineInterface {
    public function brewCoffee($selection);
}

class BasicCoffeeMachine implements CoffeeMachineInterface {

    public function brewCoffee($selection) {
        switch ($selection) {
            case 'ESPRESSO':
                return $this->brewEspresso();
            default:
                throw new CoffeeException('Selection not supported');
        }
    }

    protected function brewEspresso() {
        // Brew an espresso...
    }
}

class PremiumCoffeeMachine extends BasicCoffeeMachine {

    public function brewCoffee($selection) {
        switch ($selection) {
            case 'ESPRESSO':
                return $this->brewEspresso();
            case 'AMERICANO':
                return $this->brewAmericanoCoffee();
            default:
                throw new CoffeeException('Selection not supported');
        }
    }

    protected function brewAmericanoCoffee() {
        // Brew a americano coffee...
    }
}

function getCoffeeMachine(User $user) {
    switch ($user->getPlan()) {
        case 'PREMIUM':
            return new PremiumCoffeeMachine();
        case 'BASIC':
        default:
            return new BasicCoffeeMachine();
    }
}

function prepareCoffee(User $user, $selection) {
    $coffeeMachine = getCoffeeMachine($user);
    return $coffeeMachine->brewCoffee($selection);
}
 

4. I - Принцип разделения интерфейса

Вкратце - создавайте узкоспециализированные интерфейсы, предназначенные для конкретного клиента. Клиенты не должны зависеть от интерфейсов, которые они не используют.

Возвращаясь к фентезийной теме, создадим интерфейс владения оружием


interface WeaponInterface {
    public function shotAxe();
    public function shotBow();
}

/**
* Человек - универсален
* И из лука стреляет и топопром махать может :-)
*/
class Human implements WeaponInterface {

    public function shotAxe() {
        echo 'Удар топором!';
    }

    public function shotBow() {
        echo 'Выстрел из лука!';
    }
}

class Orc implements WeaponInterface {

    public function shotAxe() {
        echo 'Удар топором!';
    }

    public function shotBow() {
        throw new Exception('Я не умею стрелять из лука!');
    }
}

class Elf implements WeaponInterface {

    public function shotAxe() {
        throw new Exception('Я не могу поднять топор!');
    }

    public function shotBow() {
        echo 'Выстрел из лука!';
    }
}
 

Проблема, как видите, в том, что у орка и эльфа есть методы, которые не используются. Решение состоит в том, чтобы разделить WeaponInterface на два более конкретных интерфейса, которые используются только тогда, когда это необходимо.


interface OrcInterface {
    public function shotAxe();
}

interface ElfInterface {
    public function shotBow();
}

class Human implements OrcInterface, ElfInterface {

    public function shotAxe() {
        echo 'Удар топором!';
    }

    public function shotBow() {
        echo 'Выстрел из лука!';
    }
}

class Orc implements OrcInterface {

    public function shotAxe() {
        echo 'Удар топором!';
    }
}

class Elf implements ElfInterface {

    public function shotBow() {
        echo 'Выстрел из лука!';
    }
}
 

5. D - Принцип инверсии зависимостей

Вкратце - этот принцип означает, что конкретный класс не должен напрямую зависеть от другого класса, а должен зависеть от абстракции этого класса. Или другими словами - объектом зависимости должна быть абстракция, а не что-то конкретное.

Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Ну и самый ходовой на просторах Интернета пример


class UserDB {

    private $dbConnection;

    public function __construct(MySQLConnection $dbConnection) {
        $this->$dbConnection = $dbConnection;
    }

    public function store(User $user) {
        // Store the user into a database...
    }
}
 

В этом случае класс UserDB напрямую зависит от базы данных MySQL. Это означает, что если мы хотим изменить используемый механизм базы данных, нам нужно переписать этот класс и нарушить принцип открытия-закрытия.

Решение состоит в том, чтобы разработать абстракцию подключения к базе данных


interface DBConnectionInterface {
    public function connect();
}

class MySQLConnection implements DBConnectionInterface {

    public function connect() {
        // Return the MySQL connection...
    }
}

class UserDB {

    private $dbConnection;

    public function __construct(DBConnectionInterface $dbConnection) {
        $this->dbConnection = $dbConnection;
    }

    public function store(User $user) {
        // Store the user into a database...
    }
}
 

Важлива інформація

Міністерство оборони
України
з 24.02 по 07.02
втрати противника
орієнтовно склали:
846650 ( +1340 ) особового складу
9975 ( +10 ) танків
20755 ( +18 ) бойових бронемашин
22785 ( +32 ) артилерійських систем
1271 ( +0 ) РСЗВ
1056 ( +1 ) засоби ППО
369 ( +0 ) літаків
331 ( +0 ) гелікоптерів
36307 ( +96 ) автомобільної техніки
28 ( +0 ) кораблі / катери
3054 ( +0 ) крилаті ракети
24301 ( +116 ) БПЛА
3738 ( +1 ) спец. техніки
4 ( +0 ) установок ОТРК/ТРК
1 ( +0 ) підводні човни