Заказать проект
Оставьте заявку для получения коммерческого предложения.
Заполните форму и мы вышлем Вам предложение в котором решим,
чем можем вам помочь.
Будь SOLID

Будь SOLID

22 Июня 2018
Александр Гоцалюк
Back End Developer
Александр Гоцалюк
следующая статья

Проектирование в программировании - это отдельная и обширная тема. Для качественного проектирования приложений, нужны знания ООП, шаблонов проектирования и опыт качественной разработки.

Освоение и применение 5 принципов SOLID в разработке приложений  поможет совершить быстрые переход на уровень выше в качестве и функционале. Они дают возможность разрабатывать приложения которые будут проще и дешевле поддерживать и развивать.

Часто бывает, что задача приложения довольно простая и быстрая в реализации. При ее реализации, разработчик может не задумываться о возможных дополнениях функционала или переносах на другие фреймворки. Ему вообще могут быть безразличны все перечисленные выше моменты, все сводиться к “получил ТЗ” - ”выполнил” - “работает” - “сдал” - “клиент доволен”. И вот когда наступит время что-то добавить или переделать, всплывут проблемы проектирования в приложении.

Если Вы хотите писать качественный, поддерживаемый и масштабируемый код, - знание SOLID принципов это минимум, который нужно не только знать и использовать, но еще мыслить относительно этих принципов.

Что такое SOLID?

SOLID - это акроним для пяти принципов проектирования, названных Робертом Мартином в начале 2000-х.

Расшифровка акронима:


Принцип единственной ответственности (SRP)

“A class should have only one reason to change.”
Robert C. Martin

Класс должен иметь только одну причину для редактирования. Другими словами это значит, что объект должен иметь одну ответственность и она должна быть полностью реализована в классе (инкапсулирована).

Например, в системе есть объект “Post”, который держит в себе данные о статье нашего блога. В определенные моменты нам нужно получать его данные в разных форматах, для создания разных типов отчетностей (html, xml, csv, json). Неопытные программисты могут описать следующий класс:

class Post
{
    private $title;
    private $content;
    private $author;

    public function getTitle()
    {
        return $this->title;
    }

    public function getContent()
    {
        return $this->content;
    }

    public function getAuthor()
    {
        return $this->author;
    }

    // сеттеры упущено для экономии места в примере

    public function toHTML()
    {
        //code
    }

    public function toXML()
    {
        //code
    }

    public function toCSV()
    {
        //code
    }

    public function toJSON()
    {
        //code
    }
}

На самом деле, очень неудобно вносить изменения, после того, как наш класс введен в проект. Например, добавление/удаление нового формата отчетности. С точки зрения логики и SRP, нужно “разбить” класс на два подтипа: “Post” и “Report”. С обязанностями управления данными статьи и формированием отчета, соответственно.

class Post
{
    private $title;
    private $content;
    private $author;

    public function getTitle()
    {
        return $this->title;
    }

    public function getContent()
    {
        return $this->content;
    }

    public function getAuthor()
    {
        return $this->author;
    }

    // сеттеры упущено для экономии места в примере

    /**
     * Возвращает массив данных объекта нужных для формирования отчета
     *
     * @return array
     */
    public function getData(){
        //code
    }
}

class Report
{
    private $data;

    /**
     * @param array $data
     */
    public function __construct($data)
    {
        $this->data = $data;
    }

    public function toHTML()
    {
        //code
    }

    public function toXML()
    {
        //code
    }

    public function toCSV()
    {
        //code
    }

    public function toJSON()
    {
        //code
    }
}

Теперь, когда нужно будет добавить/удалить какой-то формат отчетности,  нужно редактировать только класс “Report”, а для смены данных статьи изменять только “Post”. Это и отвечает за принцип единственной ответственности.

Не могу не отметить, что слепое следование данному принципу может привести проект к избыточной сложности во время поддержки и тестировании. Так что, SRP лучше применять только если это оправдано. Например, если у объекта слишком много разных обязанностей; любое изменение логики в одном классе приводит к потребности изменения других классов; невозможно отделить класс для применения в другой сфере проекта, так как это потянет ненужные зависимости.

Принцип открытости/закрытости (OCP)

“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”
Bertrand Meyer

Программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для изменения. Если разобраться детальнее, то сущности должны быть открыты для расширения, путем создания их новых типов. В результате, изменения не должны вносится  в код, который использует эти сущности.

Для демонстрации этого принципа рассмотрим следующий пример кода:  

class User
{
    protected $logger;

    public function __construct(Logger $logger) {
        $this->logger = $logger;
    }

    public function setName($name)
    {
        try{
            //save in DB
        } catch (Exception $e) {
            $this->logger->log($e->getMessage());
        }
    }
}

class Logger
{
    private function saveToFile($message) {
        //...
    }
    public function log($message) {
        //...
        $this->saveToFile($message);
    }
}

$logger  = new Logger();
$user = new User($logger);
$user>setName('Валера');

В данном примере у нас есть класс “User”, предназначен для работы с данными пользователей и класс “Logger”, который сохраняет ошибки в файл. И тут вдруг, заказчик ставит задачу: логи по пользователю должны сохраняться в базу данных вместо файла (логи по другим классам по прежнему должны сохранятся в файл). Чтобы реализовать задачу, нам придется пренебречь принципом открытости/закрытости. Так как будут модифицироваться уже готовые классы. В случае простой системы, на это можно закрыть глаза. Но при разработке масштабного проекта или работе в команде, это может привести к фатальным ошибкам. Для соответствия рассматриваемому принципу, наведенный выше код можно спроектировать следующим примером:

interface ILogger {
    public function log();
}

class User
{
    protected $logger;

    public function __construct(ILogger $logger) {
        $this->logger = $logger;
    }

    public function setName($name)
    {
        try{
            //save in DB
        } catch (Exception $e) {
            $this->logger->log($e->getMessage());
        }
    }
}

class FileLogger implements ILogger {
 
    private function saveToFile($message) {
        //...
    }
    public function log($message) {
        //...
        $this->saveToFile($message);
    }
}
 
class DBLogger implements ILogger {
 
    private function saveToDB($message) {
        //...
    }
    public function log($message) {
        //...
        $this->saveToDB($message);
    }
}

$logger  = new DBLogger();
$user = new User($logger);
$user>setName('Валера');

Принцип подстановки Барбары Лисков (LSP)

В оригинале данный принцип звучит так: Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.

Роберт С. Мартин перефразировал этот принцип так: Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.

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

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

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

abstract class mammal
{
    protected $speed;

    public function getSpeed()
    {
        return $this->speed;
    }

    // ...
}

Например, обезьяна будет убегать по городу примерно со скоростью 10 км/час.

class monkey extends mammal
{
    public function __construct()
    {
        $this->speed = 10;
    }

    public function getSpeed()
    {
        // ...
        return $this->speed;
    }
}

Если описывать ситуацию с дельфином (а он млекопитающий, если что), то метод получения скорости должен выдавать ошибку, так как он не сможет перемещаться по городу.

class dolphin extends  mammal
{
    public function getSpeed()
    {
        throw new Exception("dolphin can't run away");
    }
}

В классе ‘monkey’ метод ‘getSpeed’ возвращает такой же тип данных, как и супер-класс  ‘mammal’ - принцип LSP соблюден.

В классе ‘dolphin’ метод ‘getSpeed’ возвращает ‘Exception’, что нарушает принцип LSP.

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

Принцип разделения интерфейса (ISP)

Формулировка:

Клиенты не должны зависеть от методов, которые они не используют.

Если перефразировать:

Много специализированных интерфейсов лучше, чем один универсальный.

Пример:

interface ITransformer
{
    public function toCar();
    public function toPlane();
    public function toShip();
}

class MegaTransformer implements ITransformer {
    public function toCar(){
   	 echo 'transform to car';
    }
     
    public function toPlane(){
   	 echo 'transform to plane';
    }
     
    public function toShip(){
   	 echo 'transform to ship';
    }
}
 
class OptimusTransformer implements ITransformer {
    public function toCar(){
   	 echo 'transform to car';
    }
     
    public function toPlane(){
   	 throw new Exception('i can`t transform to plane');
    }
     
    public function toShip(){
   	 throw new Exception('i can`t transform to ship');
    }
}

В результате использования сложного интерфейса, приходится блокировать ненужные методы. Для соответствия рассматриваемому принципу нужно разделить и упростить класс “OptimusTransformer”.

interface ICarTransformer
{
    public function toCar();
}

interface IPlaneTransformer
{
    public function toPlane();
}

interface IShipTransformer
{
    public function toShip();
}

class MegaTransformer implements ICarTransformer, IPlaneTransformer, IShipTransformer
{
	//code
}

class OptimusTransformer implements ICarTransformer {
    public function toCar(){
   	 echo 'transform to car';
    }
}

Принцип инверсии зависимостей (DIP)

Есть две формулировки принципа инверсии зависимостей:

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

  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

Например у детей (класс ‘Kid’) источник денег на карманные расходы —  родители.

class Kid
{
    public function getMoney()
    {
        $parents = new Parents();
        return $parents->getMoney();
    }
}

Когда ребенок вырастает (‘Man’) и нуждается в деньгах, его источником доходов становится работа. Которую еще и можно выбрать, основываясь на предпочтениях.

class Man
{
    private $job;

    public function __construct(Job $job)
    {
        $this->job = $job;
    }

    public function getMoney()
    {
        return $this->job->getMoney();
    }
}

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

class IMan
{
    private $moneySource;

    public function __construct(MoneyProvider $moneyProvider)
    {
        $this->moneySource = $moneyProvider;
    }

    public function getMoney()
    {
        return $this->moneySource->getMoney();
    }
}

interface MoneyProvider {
    public function getFood();
}

class Business implements moneyProvider
{
    public function getFood()
    {
        // code
    }
}

В итоге человек (‘IMan’) привязывается не к каким-то классам, а к абстракции. Это позволяет ему выбирать любой источник денег, который реализовывает интерфейс ‘MoneyProvider’.  


Разобравшись и применяя разобранные принципы на практике Вы сможете уберечь свое приложение от таких проблем как:

1. Закрепощенность. Система сложно поддается изменениям, так как любое, даже минимальное, изменение может вызвать эффект "снежного кома", который затрагивает другие компоненты системы;

2. Неустойчивость. Изменения в системе разрушают работу в тех мест, которые не имеют прямого отношения к непосредственно измеряемому компоненту;

3. Неподвижность. В системе сложно выделить элементы, которые можно  повторно использовать в других приложениях;

4. Сложность рефакторинга. Трудно читать и прослеживать последовательность выполнения модулей проекта в системе.




Записаться На Консультацию
Записаться На Консультацию
Мы свяжемся
с вами
в течении
10 минут
laptop
Мы свяжемся с вами в течении 10 минут