SOLID «D»: Принцип инверсии зависимостей
Принцип единственной обязанности, открытости-закрытости, подстановки, разделения интерфейсов и инверсии зависимостей – пятерка принципов, на которые следует ориентироваться при написании кода.
Хотя говорить о преобладающей важности одного из принципов будет не >1дюверно, но отметить влияние принципа инверсии зависимостей на код нужно обязательно. Если вы заметили, что другие принципы трудно понять или применить, начните с этого, а остальные применяйте уже к коду, соответствующему DIP.
Определение
- Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Этот принцип выделил Роберт С. Мартин в своей книге о быстрой разработке программ, а затем переиздал в версии книги для языка C#. Принцип инверсии зависимостей - последний из пятерки принципов SOLID.
DIP в реальном мире
Если вы программируете не очень аккуратно, не совсем разбираетесь в правилах программирования и не соблюдаете ряд принципов SOLID, в своей работе вам, возможно, придется пройти все 7 кругов ада, ощутить все на собственном опыте, прежде чем код станет действительно хорошим.
Принципы SOLID - это исключительно архитектурные принципы Роберта С. Мартина, которые полностью меняют правила игры, весь ход программирования. Далее мы проиллюстрируем влияние нескольких архитектурных решений, которые появились благодаря принципу DIP, и серьезно повлияли на один из наших проектов.
Большинство веб-проектов включают в себя три основных технологии: HTML, PHP и SQL. Определенная версия каждого из этих приложений, о которых мы говорим, или то, какой тип реализаций SQL вы используете – все это не имеет абсолютно никакого значения в нашем случае. Дело в том, что информация из HTML формы должна заканчиваться в базе данных. Остальное обеспечит РНР.
Из этого следует уяснить одну важную вещь - хороший способ, которым эти три технологии представляют три различных архитектурных слоя:
- пользовательский интерфейс,
- бизнес-логику,
- долговременное сохранение данных.
Скоро мы поговорим об этих слоях. А сейчас давайте сосредоточимся на некоторых странных, но часто встречающихся решениях, применяемых для того, чтобы заставить технологии работать вместе.
Есть много проектов, которые используют SQL код в тегах PHP внутри HTML-файла. Или PHP-код отражает и страницы HTML, и непосредственно интерпретирует глобальные переменные $_GET или $_POST. Казалось бы, все хорошо. Но в чем же тогда проблема?
На изображении выше показана сырая версия того, что мы описывали в предыдущем абзаце. Стрелки указывают на различные зависимости, и мы можем сделать вывод, что все базируется на всем, все зависит от всего. Если нам придется изменить таблицу базы данных, скорее всего, в конечном итоге, мы закончим редактированием HTML-файла. Или если мы изменим поле в HTML, то закончим изменением столбца в операторе SQL. Или, как видно из второй схемы, нам действительно придется сильно изменить наш PHP, если изменится HTML. А в худшем случае, если мы генерируем весь HTML-контент из PHP-файла, скорее всего, нам придется менять PHP, чтобы изменить HTML-контент. Поэтому нет никаких сомнений в том, что зависимости просто лавируют между нашими классами и модулями. Но на этом все не заканчивается: вы можете хранить операции или код на PHP в таблицах SQL.
На схеме выше видно, что запросы к базе данных SQL возвращают PHP-код, сгенерированный с данными из таблиц. Эти PHP-е функции или классы создают другие SQL-запросы, которые возвращают уже другой PHP-код. Этот цикл будет продолжаться до тех пор, пока вся информация не будет получена и возвращена, вероятно, пользовательскому интерфейсу.
Многим это покажется полной чушью, но если вы еще не работали с проектом, созданным и реализованным по этому сценарию, то в будущем, скорее всего, вам придется с этим столкнуться. Большинство существующих проектов, независимо от используемых языков программирования, были написаны программистами старой закалки, которым было все равно или они не знали, как можно сделать код лучше. Если вы сейчас читаете этот мануал о принципах программирования, значит, вам нужно научиться программировать на порядок лучше, чем вы можете на сегодняшний день. Это значит, что вы уважаете или даже только начинаете уважать свою профессию, хотите понять свое ремесло и сделать его лучше.
Еще одна версия может рассказать нам об ошибках, сделанных нашими предшественниками, и последствиями от них. В некоторых проектах вы можете получить, в итоге, практически неподдерживаемое состояние из-за их старой и кросс-зависимой архитектуры. В итоге, вам придется просто отказаться от таких проектов навсегда, и тогда вы поймете, что больше не хотите повторять эти ошибки снова. Поэтому стремитесь делать чистую архитектуру, которая будет соответствовать принципам SOLID, во-первых, а во-вторых, принципу инверсии зависимостей.
В этой архитектуре есть несколько интересных моментов:
- пользовательский интерфейс (в большинстве случаев, веб-фреймворк MVC) или любой другой механизм доставки в нашем проекте будет зависеть от бизнес-логики. Бизнес-логика сама по себе довольно абстрактна, а пользовательский интерфейс – само воплощение конкретики. Он представляет собой одну из деталей проекта, и к тому же, очень нестабильную. Ничто не должно зависеть от пользовательского интерфейса, ничто не должно зависеть от веб-фреймворка MVC;
- еще одно интересное наблюдение, сделанное нами, говорит, что долговременное сохранение, база данных, наш MySQL или PostgreSQL базируются на бизнес-логике. Это позволяет менять сохраняемость так, как это нужно нам. Если завтра нам понадобится изменить MySQL вместе с PostgreSQL или просто текстовые файлы, мы легко сможем это сделать. Нам, конечно, придется реализовать определенный уровень сохраняемости для новых методов сохранения состояния, но для этого не придется менять отдельные строки кода в нашей бизнес-логике;
- в конце концов, в правой части нашей бизнес-логики, вне ее, у нас есть все классы, которые создают классы бизнес-логики. Эти классы созданы как точки входа в наши приложения. Многие люди склонны думать, что они принадлежат к бизнес-логике, но делают они это только для того, чтобы создать бизнес-объекты. Они – просто классы, которые помогают нам создавать другие классы. Бизнес-объекты и логика, которую они обеспечивают, не зависят от них. Мы могли бы использовать различные модели или создать простой объект, чтобы обеспечить бизнес-логику. Это не имеет значения. После того, как бизнес-объекты созданы, они начинают выполнять свою работу.
Приступим к коду
Соблюдать принцип инверсии зависимостей (DIP) на архитектурном уровне довольно легко, если вы соблюдаете классические шаблоны проектирования. Использовать и показать это внутри бизнес-логики достаточно просто, а местами - даже весело. Представим себе приложение для чтения электронных книг.
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-файлов или любых других форматов файлов. Наши тесты – проверки на некоторых базовых строках. Если бы мы писали настоящее приложение, единственная разница заключалась бы в способе тестирования разных файловых форматов. Структура зависимости была бы очень похожа на предложенную в примере.
Использование читалки формата PDF, которая использует книгу в формате PDF, может быть вполне здравым решением в ряде ограниченных случаев. Если наша задача заключалась в том, чтобы написать только читалку формата PDF, то такое решение фактически соответствует задаче. Но мы хотели написать универсальную читалку, которая использовала бы несколько разных форматов, в том числе, и уже реализованную PDF-версию. Переименуем класс нашей читалки.
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.";
}
}
Переименование не дало каких-либо функциональных эффектов. Тест пройден на «отлично».
Testing started at 1:04 PM ...
PHPUnit 3.7.28
Time: 13 ms, Memory: 2.50Mb
OK (1 test, 1 assertion)
Process finished with exit code 0
Наша читалка уже стала гораздо абстрактнее. Намного универсальнее. Сейчас у нас есть универсальный EBookReader, который может читать довольно специфический формат книг – PDFBook. Наша абстракция зависит от деталей. То, что наша книга находится в формате PDF – все лишь деталь, от которой ничего не должно зависеть.
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.
Мы создали интерфейс для нашей читалки, который назвали EBookReader, и который отражает все потребности EBookReader. Это – прямой результат соблюдения принципа разделения интерфейса, который основывается на идее, что интерфейсы должны отражать потребности клиентов. Интерфейсы относятся к клиентам и называются таким образом, чтобы отражать типы и объекты, которые необходимы клиентам. Интерфейсы должны содержать методы, которые клиенты хотят использовать. Для EBookReader вполне естественно использовать EBook и содержать метод read().
Теперь вместо единственной зависимости, у нас их становится две.
- Первая зависимость указывает от EBookReader к интерфейсу EBook и на используемый тип. EBookReader использует EBook.
- Вторая зависимость уже несколько другого рода. Она указывает от PDFBook к тому же интерфейсу EBook, но на реализуемый тип. PDFBook – это просто особая форма EBook, которая реализует интерфейс для того, чтобы удовлетворить потребности клиентов.
Неудивительно, ведь такое решение позволит нам просматривать в нашей читалке различные типы электронных книг. Единственное условие для всех этих книг – они должны соответствовать интерфейсу EBook и реализовывать его.
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.";
}
}
Все это приводит нас к принципу открытости-закрытости, и круг замыкается. Принцип инверсии зависимостей помогает нам соблюдать все остальные принципы из пятерки SOLID. Соблюдая принцип DIP, мы практически начинаем соблюдать OCP, можем разделять обязанности, правильно используем подтипы и можем разделять интерфейсы.
Финальные мысли
Ну, вот и все, мы наконец-то закончили и полностью разобрали все мануалы о принципах SOLID. Это должно полностью изменить ваши представления об архитектуре и сделать вашу работу проще и интереснее. Мы должны стремиться делать наш код лучше, используя эту пятерку.
В объектно-ориентированном программировании пятерка принципов SOLID – один из важнейших стержней, которые должны делать код лучше, а жизнь программистов - проще.
Источник: code.tutsplus.com