Статьи

SOLID: часть 4 — принцип обращения зависимостей

Одиночная ответственность (SRP) , открытый / закрытый (OCP) , подстановка Лискова, разделение интерфейса и инверсия зависимости . Пять гибких принципов, которыми вы должны руководствоваться при написании кода.

Было бы несправедливо сказать вам, что один из принципов SOLID важнее другого. Однако, вероятно, ни один из других не имеет такого непосредственного и глубокого влияния на ваш код, как принцип инверсии зависимости, или DIP, короче говоря. Если вам трудно понять или применить другие принципы, начните с этого и примените остальные к коду, который уже соответствует DIP.

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

Этот принцип был определен Робертом Мартином в его книге « Разработка гибких программ, Принципы, Шаблоны и Практики», а затем переиздан в версии C # книги « Принципы, Шаблоны и Практики Agile в C #» , и является последним из пяти. Твердые гибкие принципы.

Прежде чем мы начнем кодировать, я хотел бы рассказать вам историю. В Syneto мы не всегда были так осторожны с нашим кодом. Несколько лет назад мы знали меньше, и хотя мы старались изо всех сил, не все наши проекты были такими хорошими. Мы прошли через ад и снова и многому научились методом проб и ошибок.

Принципы SOLID и принципы чистой архитектуры Uncle Bob (Robert C. Martin) стали для нас переломными моментами и изменили наш способ кодирования способами, которые трудно описать. Я попытаюсь проиллюстрировать, в двух словах, несколько ключевых архитектурных решений, навязанных DIP, которые оказали большое влияние на наши проекты.

Большинство веб-проектов содержат три основные технологии: HTML, PHP и SQL. Конкретная версия этих приложений, о которой мы говорим, или тип используемых вами реализаций SQL не имеет значения. Дело в том, что информация из HTML-формы должна так или иначе попадать в базу данных. Клей между ними может быть обеспечен с помощью PHP.

От этого необходимо избавиться: насколько хорошо эти три технологии представляют три разных архитектурных уровня: пользовательский интерфейс, бизнес-логика и постоянство. Мы поговорим о последствиях этих слоев через минуту. А пока давайте сосредоточимся на некоторых странных, но часто встречающихся решениях, чтобы технологии работали вместе.

Много раз я видел проекты, которые использовали код SQL в теге PHP внутри файла HTML или код PHP, повторяющий страницы и страницы HTML и интерпретирующий непосредственно глобальные переменные $_GET или $_POST . Но почему это плохо?

HTML-PHP-SQL-кросс-зависимость

Изображения выше представляют сырую версию того, что мы описали в предыдущем абзаце. Стрелки представляют различные зависимости, и, как мы можем заключить, в основном все зависит от всего. Если нам нужно изменить таблицу базы данных, мы можем отредактировать файл HTML. Или, если мы изменим поле в HTML, мы можем изменить имя столбца в операторе SQL. Или, если мы посмотрим на вторую схему, нам вполне может понадобиться изменить наш PHP, если HTML-код изменится, или в очень плохих случаях, когда мы генерируем весь HTML-контент из файла PHP, нам, безусловно, потребуется изменить файл PHP на изменить содержимое HTML. Таким образом, нет никаких сомнений в том, что зависимости между классами и модулями зигзагообразны. Но это не заканчивается здесь. Вы можете хранить процедуры; PHP-код в таблицах SQL.

HTML-PHP-SQL-хранимая-процедура

В приведенной выше схеме запросы к базе данных SQL возвращают код PHP, сгенерированный с данными из таблиц. Эти функции или классы PHP выполняют другие запросы SQL, которые возвращают другой код PHP, и цикл продолжается, пока, наконец, вся информация не будет получена и возвращена … вероятно, в пользовательский интерфейс.

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

Другой вариант — повторить ошибки ваших предшественников и жить с последствиями. В Syneto, после того, как один из наших проектов достиг почти не поддерживаемого состояния из-за его старой и взаимозависимой архитектуры, и нам пришлось в основном отказаться от него навсегда, мы решили больше никогда не идти по этому пути. С тех пор мы стремимся иметь чистую архитектуру, которая правильно учитывает принципы SOLID и, самое главное, принцип инверсии зависимостей.

HighLevelDesign

Что удивительного в этой архитектуре, так это то, как указываются зависимости:

  • Пользовательский интерфейс (в большинстве случаев веб-среда MVC) или любой другой механизм доставки, используемый в вашем проекте, будет зависеть от бизнес-логики. Бизнес логика довольно абстрактна. Пользовательский интерфейс очень конкретный. Пользовательский интерфейс — это просто деталь проекта, и он также очень изменчив. Ничто не должно зависеть от пользовательского интерфейса, ничто не должно зависеть от вашей инфраструктуры MVC.
  • Другое интересное наблюдение, которое мы можем сделать, заключается в том, что постоянство, база данных, ваш MySQL или PostgreSQL зависят от бизнес-логики. Ваша бизнес-логика не зависит от базы данных. Это позволяет обмениваться постоянством, как вы хотите. Если завтра вы захотите поменять MySQL на PostgreSQL или просто текстовые файлы, вы можете это сделать. Вам, конечно, потребуется реализовать специальный уровень персистентности для нового метода персистентности, но вам не нужно будет изменять одну строчку кода в вашей бизнес-логике. Более подробное объяснение темы персистентности содержится в учебном пособии « Развитие на уровне персистентности» .
  • Наконец, справа от бизнес-логики, вне ее, у нас есть все классы, которые создают классы бизнес-логики. Это фабрики и классы, созданные точкой входа в наше приложение. Многие люди склонны считать, что они относятся к бизнес-логике, но пока они создают бизнес-объекты, их единственная причина заключается в том, чтобы сделать это. Это классы только для того, чтобы помочь нам создать другие классы. Бизнес-объекты и логика, которую они предоставляют, не зависят от этих фабрик. Мы могли бы использовать различные шаблоны, такие как Simple Factory, Abstract Factory, Builder или создание простых объектов, чтобы обеспечить бизнес-логику. Это не важно После создания бизнес-объектов они могут выполнять свою работу.

Применение принципа инверсии зависимости (DIP) на архитектурном уровне довольно легко, если вы уважаете классические гибкие шаблоны проектирования . Тренировать и иллюстрировать его внутри бизнес-логики также довольно легко и даже может быть весело. Мы представим приложение для чтения электронных книг.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Test extends PHPUnit_Framework_TestCase {
 
    function testItCanReadAPDFBook() {
        $b = new PDFBook();
        $r = new PDFReader($b);
 
        $this->assertRegExp(‘/pdf book/’, $r->read());
    }
 
}
 
class PDFReader {
 
    private $book;
 
    function __construct(PDFBook $book) {
        $this->book = $book;
    }
 
    function read() {
        return $this->book->read();
    }
 
}
 
class PDFBook {
 
    function read() {
        return «reading a pdf book.»;
    }
}

Мы начинаем развивать нашу электронную книгу в формате PDF. Все идет нормально. У нас есть класс PDFReader использующий PDFBook . Функция read() читателя делегирует методу read() книги. Мы просто проверяем это, выполняя проверку регулярных выражений после ключевой части строки, возвращаемой методом PDFBook ‘s reader() .

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

pdfreader-pdfbook

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Test extends PHPUnit_Framework_TestCase {
 
    function testItCanReadAPDFBook() {
        $b = new PDFBook();
        $r = new EBookReader($b);
 
        $this->assertRegExp(‘/pdf book/’, $r->read());
    }
 
}
 
class EBookReader {
 
    private $book;
 
    function __construct(PDFBook $book) {
        $this->book = $book;
    }
 
    function read() {
        return $this->book->read();
    }
 
}
 
class PDFBook {
 
    function read() {
        return «reading a pdf book.»;
    }
}

Переименование не имело функциональных встречных эффектов. Тесты все еще проходят.

Тестирование началось в 13:04 …
PHPUnit 3.7.28 от Себастьяна Бергмана.
Время: 13 мс, память: 2,50 МБ
ОК (1 тест, 1 утверждение)
Процесс завершен с кодом выхода 0

Но это имеет серьезный дизайнерский эффект.

ebookreader-pdfbook

Наш читатель стал намного более абстрактным. Гораздо более общий. У нас есть общий EBookReader который использует очень специфический тип книги, PDFBook . Абстракция зависит от детали. Тот факт, что наша книга относится к типу PDF, должен быть только деталью, и никто не должен от нее зависеть.

01
02
03
04
05
06
07
08
09
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
class Test extends PHPUnit_Framework_TestCase {
 
    function testItCanReadAPDFBook() {
        $b = new PDFBook();
        $r = new EBookReader($b);
 
        $this->assertRegExp(‘/pdf book/’, $r->read());
    }
 
}
 
interface EBook {
    function read();
}
 
class EBookReader {
 
    private $book;
 
    function __construct(EBook $book) {
        $this->book = $book;
    }
 
    function read() {
        return $this->book->read();
    }
 
}
 
class PDFBook implements EBook{
 
    function read() {
        return «reading a pdf book.»;
    }
}

Наиболее распространенным и наиболее часто используемым решением для инвертирования зависимости является введение более абстрактного модуля в наш дизайн. «Самым абстрактным элементом в ООП является интерфейс. Таким образом, любой другой класс может зависеть от интерфейса и при этом уважать DIP».

Мы создали интерфейс для нашего читателя . Интерфейс называется EBook и представляет потребности EBookReader . Это является прямым результатом соблюдения принципа разделения интерфейса (ISP), который продвигает идею о том, что интерфейсы должны отражать потребности клиентов. Интерфейсы принадлежат клиентам, и поэтому они названы так, чтобы отражать типы и объекты, которые нужны клиентам, и они будут содержать методы, которые клиенты хотят использовать. Для EBookReader естественно использовать EBooks и иметь метод read() .

ebookreader-ebookinterface-pdfbook

Вместо одной зависимости у нас теперь есть две зависимости.

  • Первая зависимость указывает от EBookReader на интерфейс EBook и он имеет тип использования. EBookReader использует электронные EBooks .
  • Вторая зависимость отличается. Он указывает от PDFBook на тот же интерфейс EBook но он имеет реализацию типа. PDFBook — это просто особая форма EBook , поэтому он реализует этот интерфейс для удовлетворения потребностей клиента.

Неудивительно, что это решение также позволяет нам подключать различные типы электронных книг к нашему читателю. Единственное условие для всех этих книг — удовлетворить интерфейс EBook и реализовать его.

01
02
03
04
05
06
07
08
09
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
48
49
class Test extends PHPUnit_Framework_TestCase {
 
    function testItCanReadAPDFBook() {
        $b = new PDFBook();
        $r = new EBookReader($b);
 
        $this->assertRegExp(‘/pdf book/’, $r->read());
    }
 
    function testItCanReadAMobiBook() {
        $b = new MobiBook();
        $r = new EBookReader($b);
 
        $this->assertRegExp(‘/mobi book/’, $r->read());
    }
 
}
 
interface EBook {
    function read();
}
 
class EBookReader {
 
    private $book;
 
    function __construct(EBook $book) {
        $this->book = $book;
    }
 
    function read() {
        return $this->book->read();
    }
 
}
 
class PDFBook implements EBook {
 
    function read() {
        return «reading a pdf book.»;
    }
}
 
class MobiBook implements EBook {
 
    function read() {
        return «reading a mobi book.»;
    }
}

Что, в свою очередь, приводит нас к Открытому / Закрытому Принципу , и круг замкнут.

Принцип обращения зависимостей — тот, который ведет или помогает нам уважать все другие принципы. Уважение DIP будет:

  • Почти заставить вас уважать OCP.
  • Позвольте вам разделить обязанности.
  • Заставить вас правильно использовать подтипы.
  • Предложите вам возможность отделить ваши интерфейсы.

Вот и все. Мы сделали. Все учебники о принципах SOLID завершены. Лично для меня открытие этих принципов и реализация проектов с их учетом было огромным изменением. Я полностью изменил свое отношение к дизайну и архитектуре, и с тех пор могу сказать, что все проекты, над которыми я работаю, экспоненциально проще в управлении и понимании.

Я считаю принципы SOLID одной из важнейших концепций объектно-ориентированного проектирования. Эти концепции должны помочь нам сделать наш код лучше, а жизнь программистов — намного проще. Хорошо продуманный код легче понять программистам. Компьютеры умны, они могут понимать код независимо от его сложности. С другой стороны, у людей есть ограниченное количество вещей, которые они могут хранить в своем активном, сфокусированном уме. В частности, количество таких вещей — «Волшебное число семь», плюс или минус два .

Мы должны стремиться структурировать наш код вокруг этих чисел, и есть несколько методов, которые помогают нам сделать это. Функции с максимум четырьмя строками (пять с включенной строкой определения), чтобы они все могли вписаться в наш разум. Отступы не проходят пять уровней глубины. Занятия не более чем с девятью методами. Шаблоны проектирования, которые обычно используют от пяти до девяти классов. Наш высокоуровневый дизайн в схемах выше использует четыре-пять концепций. Существует пять принципов SOLID, каждый из которых требует от пяти до девяти суб-концепций / модулей / классов. Идеальный размер команды программистов — от пяти до девяти. Идеальное количество команд в компании — от пяти до девяти.

Как вы можете видеть, магическое число семь, плюс или минус два повсюду вокруг нас, так почему ваш код должен отличаться?