├── .gitignore
├── img
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── 6.png
├── 7.png
├── 8.png
├── 9.png
├── 10.png
├── 11.png
├── 12.png
├── 13.png
├── 14.png
├── 15.png
├── 16.png
├── 17.png
└── 18.png
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /*
2 | !/img
3 | !.gitignore
4 | !README.md
5 |
--------------------------------------------------------------------------------
/img/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/1.png
--------------------------------------------------------------------------------
/img/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/2.png
--------------------------------------------------------------------------------
/img/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/3.png
--------------------------------------------------------------------------------
/img/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/4.png
--------------------------------------------------------------------------------
/img/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/5.png
--------------------------------------------------------------------------------
/img/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/6.png
--------------------------------------------------------------------------------
/img/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/7.png
--------------------------------------------------------------------------------
/img/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/8.png
--------------------------------------------------------------------------------
/img/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/9.png
--------------------------------------------------------------------------------
/img/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/10.png
--------------------------------------------------------------------------------
/img/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/11.png
--------------------------------------------------------------------------------
/img/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/12.png
--------------------------------------------------------------------------------
/img/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/13.png
--------------------------------------------------------------------------------
/img/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/14.png
--------------------------------------------------------------------------------
/img/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/15.png
--------------------------------------------------------------------------------
/img/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/16.png
--------------------------------------------------------------------------------
/img/17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/17.png
--------------------------------------------------------------------------------
/img/18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlance/solid-principles/HEAD/img/18.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | SOLID «S»: Принцип единственной обязанности (single responsibility principle, SPR)
2 | ===
3 |
4 | Принцип единственной обязанности - один из принципов, которого следует придерживаться при
5 | написании кода. Он декларирует, что каждый объект должен иметь одну
6 | единственную обязанность и эта обязанность должна быть инкапсулирована в
7 | класс.
8 |
9 | Термин **«SOLID»** представляет собой аббревиатуру пяти важнейших принципов
10 | работы с классами в объектно-ориентированном проектировании:
11 |
12 | 1. [Принципа
13 | единственной обязанности](#S)
14 | 2. [Принципа
15 | открытости/закрытости](#O)
16 | 3. [Принципа подстановки Барбары
17 | Лисков](#L)
18 | 4. [Принципа разделения
19 | интерфейса](#I)
20 | 5. [Принципа инверсии
21 | зависимостей](#D)
22 |
23 | Этими пятью гибкими принципами следует руководствоваться при написании
24 | кода.
25 |
26 | ### Определение
27 |
28 | **Класс должен иметь одну и только одну причину для изменений.**
29 |
30 | Это один из 5 гибких принципов SOLID, определенных в книге «Быстрая
31 | разработка программ. Принципы, примеры, практика» Робертом С. Мартином.
32 | Затем эта книга была переиздана в версии для C\# «Принципы, паттерны и
33 | методики гибкой разработки на языке C\#». То, что декларирует данный
34 | принцип, вполне легко понять, однако не так легко реализовать на
35 | практике.
36 |
37 | Класс должен иметь одну единственную причину для изменений. Но чем
38 | обусловлена подобная необходимость? В компилируемых статически
39 | типизированных языках программирования существование нескольких причин
40 | может повлечь за собой ряд нежелательных изменений.
41 |
42 | Две различные причины для изменений подобны двум различным командам,
43 | которые могут работать с одним кодом, и каждая из них будет
44 | разворачивать свое собственное решение, которое в случае C++, C\# Java
45 | или других компилируемых языков, может привести к несовместимости между
46 | различными частями приложения.
47 |
48 | Даже если вы не используете компилируемый язык, вам, возможно,
49 | потребуется повторное тестирование определенного класса или модуля по
50 | различным причинам. Это означает, что вы потратите больше времени и
51 | усилий.
52 |
53 | ### Аудитория
54 |
55 | Определение одной единственной ответственности, которую должен иметь
56 | класс или модуль, является гораздо более сложной задачей, чем простое
57 | просматривание списка.
58 |
59 | Например, один из способов поиска причин изменения - анализ аудитории
60 | класса. Пользователи приложения, которые обслуживаются определенным
61 | модулем, будут являться теми лицами, которые требуют изменений класса.
62 | Вот несколько модулей и их возможная аудитория.
63 |
64 | 1. Модуль сохраняемости - аудитория включает администраторов баз данных
65 | и проектировщиков ПО.
66 | 2. Модуль отчетности - аудитория включает клерков, бухгалтеров и
67 | операционистов.
68 | 3. Модуль расчета платежей для системы расчета заработной платы -
69 | аудитория может включать юристов, менеджеров и бухгалтеров.
70 | 4. Модуль поиска книг в библиотеке - аудитория может включать
71 | библиотекаря и читателей.
72 |
73 | ### Роли и актеры
74 |
75 | Ассоциирование конкретных лиц с ролями – весьма непростая задача. В
76 | небольшой фирме одно единственное лицо может играть сразу несколько
77 | ролей, в то время как в крупной организации может быть привлечено
78 | несколько исполнителей для выполнения одной функции. Поэтому, пожалуй,
79 | будет немаловажно подумать об этих ролях. Однако определить сами роли
80 | бывает достаточно сложно. Гораздо легче обозначить актёров, играющих эти
81 | роли и связать актёров и нашу аудиторию.
82 |
83 | Таким образом, если аудитория обусловливает причины изменения, актёры
84 | определяют аудиторию. Это позволит нам свести понятия конкретных лиц
85 | вроде «Архитектора Джона» или «Секретаря Марии» к операциям.
86 |
87 | Таким образом, согласно Роберту С. Мартину, ответственность - это
88 | определенный набор функций, которые выполняет один взятый актёр.
89 |
90 | ### Источник изменений
91 |
92 | Рассуждая таким образом, можно сделать вывод о том, что актёры
93 | представляют собой источник изменений для набора функций, которые
94 | удовлетворяют потребности самих актёров. Наряду с изменением
95 | потребностей актёров, этот специфичный набор функций тоже должен
96 | измениться и приспособиться под новые потребности актеров.
97 |
98 | **Актер для ответственности - единственный источник изменений этой
99 | ответственности. (Роберт С. Мартин)**
100 |
101 | ### Классические примеры
102 |
103 | Объекты, которые могут распечатывать сами себя
104 |
105 | Предположим, что у нас есть класс `Book`, который инкапсулирует в себе
106 | книгу вместе с её функциональностью.
107 |
108 | ```php
109 | getTitle(). ' - ' . $this->getAuthor();
236 | file_put_contents($filename, serialize($this));
237 | }
238 | }
239 | ```
240 | Мы снова можем определить несколько актёров, например, Систему
241 | управления книгой и Сохраняемость. Этот класс подлежит модификации
242 | каждый раз при измении сохраняемости или способа перелистывания
243 | страницы. Можно отметить несколько вариантов изменения данных.
244 | ```php
245 | getTitle() . ' - ' . $book->getAuthor();
276 | file_put_contents($filename, serialize($book));
277 | }
278 | }
279 | ```
280 |
281 | Переместив метод сохранения объекта в другой класс, мы сможем явно
282 | разделить ответственность и легко изменить данный метод сохраняемости,
283 | никак не влияя на класс Book. Так, внедрение класса `DatabasePersistence`
284 | будет абсолютно тривиальным, и наша бизнес-логика, выстроенная вокруг
285 | действий с книгой никак не изменится.
286 |
287 | ### Высокоуровневое представление
288 |
289 | В своих предыдущих статьях я не раз упоминал и предлагал Вашему вниманию
290 | схему высокоуровневой архитектуры, которая приведена ниже.
291 |
292 |
293 |
294 |
295 |
296 | Если мы проанализируем данную схему, то сразу поймем, как соблюдается
297 | ПЕО. Создание нового объекта обозначено с правой стороны схемы с помощью
298 | **«Фабрик» (Factories)** и единой точки входа нашего приложения (Main). Один
299 | актёр - одна ответственность. О сохраняемости **(Persistence)** также
300 | позаботились, расположив ее внизу. Отдельный модуль предназначается для
301 | отдельной ответственности. И наконец, с левой стороны мы разместили
302 | представление, или механизм доставки, в виде MVC или каком-либо другом
303 | типе пользовательского интерфейса. И вновь соблюден [принцип единой
304 | ответственности](#S). Все, что нам остается выяснить, - это что делать с
305 | самой бизнес-логикой.
306 |
307 | ### Вопросы проектирования программного обеспечения
308 |
309 | Когда мы обдумываем, как лучше написать программное обеспечение, нам
310 | обычно приходится принимать во внимание множество различных аспектов.
311 | Например, несколько требований, предъявляемых к классу и оказывающих на
312 | него влияние, могут быть представлены в виде оси изменений. Эти оси
313 | изменений могут играть роль ключа к единой ответственности. Вполне
314 | вероятно, что группы требований, влияющих на ту же группу функций, будут
315 | иметь причины для изменений или могут быть выдвинуты на первый план.
316 |
317 | Главное достоинство ПО - простота внесения изменений. Следующее важное
318 | качество - функциональность с точки зрения способности ПО удовлетворять
319 | как можно более широкий круг потребностей пользователей. Однако для
320 | достижения высокого значения на втором уровне, сначала нужно обязательно
321 | выяснить значение первого критерия. Чтобы поднять значение первичного
322 | критерия на должный уровень, мы должны, соблюдая ПЕО, построить такую
323 | архитектуру, которую можно легко модифицировать, расширять и в которую
324 | можно быстро добавлять новую функциональность.
325 |
326 | ### Изложим по шагам:
327 |
328 | Значение первого критерия должно быть установлено до определения
329 | значения второго критерия.
330 |
331 | 1. Второй критерий отвечает за удовлетворение потребностей
332 | пользователей.
333 | 2. Потребности пользователей – это потребности актёров.
334 | 3. Потребности актёров определяют необходимость изменения этих актёров.
335 | 4. Потребности в изменениях актёров, в свою очередь, определяют нашу
336 | ответственность.
337 |
338 | Таким образом, в процессе разработки архитектуры нашего программного
339 | обеспечения, мы должны:
340 |
341 | 1. Определить актёров.
342 | 2. Выявить область ответственности каждого актёра.
343 | 3. Сгруппировать классы и функции так, чтобы каждый из них отвечал
344 | только за свою часть.
345 |
346 | ### Менее очевидный пример
347 | ```php
348 | findBookBy($book->getTitle(), $book->getAuthor());
437 | }
438 |
439 | }
440 | ```
441 | Для того чтобы найти нужную книгу, библиотекарь должен будет применить
442 | класс `BookLocator`. Посетителю же потребуется только класс `Book`. Конечно
443 | же, `BookLocator` можно реализовать несколькими разными способами. Так,
444 | можно использовать автора книги и ее название, чтобы найти информацию из
445 | объекта `Book`. Это всегда зависит от нашей задачи. Важно то, что при
446 | переезде библиотеки в другое помещение организация хранения книг, скорее
447 | всего, изменится, и библиотекарю придется искать книги в новой
448 | библиотеке, но при этом объект `Book` затронут не будет. Точно также, если
449 | мы позволим читателям просматривать только аннотации книг, закрыв доступ
450 | к их страницам – мы никак не повлияем ни на библиотекаря, ни на
451 | собственно процесс поиска полок, на которых находятся книги.
452 |
453 | Однако если наша задача – исключить библиотекаря и разработать механизм
454 | самообслуживания в нашей библиотеке, то мы можем считать, что ПЕО был
455 | соблюден в нашем первом примере. Читатели в этом случае сами станут
456 | выступать в роли библиотекарей, и им придется идти искать книги
457 | самостоятельно, после чего подтверждать получение нужной книги в
458 | автоматизированной системе. Существует и такая возможность. Главное, что
459 | здесь нужно запомнить, - это то, что Вы всегда должны обдумывать свои
460 | задачи очень тщательно.
461 |
462 | ### Выводы
463 |
464 | Принцип [единcтвенной ответственности](#S) должен соблюдаться каждый раз,
465 | когда вы пишите код. Построение классов и модулей во многом определяется
466 | ПЕО, который позволяет сокращать зависимость между ними. Но, как и
467 | каждая медаль, ПЕО имеет две противоположные стороны. Очень удобно
468 | планировать архитектуру приложения, учитывая ПЕО, с самого начала
469 | разработки. Также удобно сразу выделить столько актеров, сколько нам
470 | понадобится. Однако, с точки зрения архитектуры, крайне опасно пытаться
471 | продумать все составляющие части приложения с самого начала. Излишнее
472 | соблюдение ПЕО может с легкостью привести к чрезмерной оптимизации, и
473 | вместо хорошей архитектуры, мы получим архитектуру, в которой будет
474 | очень сложно разобраться, какой класс или модуль за что отвечает.
475 |
476 | Таким образом, каждый раз, когда вы обнаруживаете, что класс или модуль
477 | может вскоре измениться по разным причинам, постарайтесь сделать
478 | необходимые шаги для соблюдения ПЕО, при этом не нужно сильно
479 | переусердствовать, так как излишняя оптимизация может, наоборот, нести
480 | за собой дополнительные проблемы.
481 |
482 | SOLID «O»: Принцип открытости/закрытости (open/closed principle, OCP)
483 | ===
484 |
485 | Принцип открытости/закрытости легко
486 | нарушить, но и написать код, который соответствует этому принципу, не
487 | так уж и сложно.
488 |
489 | Сущность программного обеспечения (классы, модули и функции) должна быть
490 | открыта для усовершенствования, но закрыта для различных изменений.
491 |
492 | Принцип открытости-закрытости (OCP) был сформулирован французским
493 | программистом Бертраном Майером и впервые вышел в мир в его книге
494 | «Object-Oriented Software Construction» в 1988 году. Популярность к
495 | этому принципу пришла в начале 2000-х годов, когда его включили в SOLID.
496 |
497 | Здесь же речь идет о разработке модулей, классов и функций таким
498 | образом, чтобы в ситуации, когда понадобится новый функционал, не
499 | пришлось менять уже существующий код. Решение – в написании нового кода,
500 | который будет использовать существующий. Это может вызвать недоумение у
501 | разработчиков, которые пишут на Java, C, C++ или C\#, так как
502 | затрагивается не только исходный код, но и двоичный. Имеется в виду
503 | создание новых возможностей таким образом, чтобы не пришлось заново
504 | распределять двоичные файлы, файлы с расширением «exe» и DLL-библиотеки.
505 |
506 | ### SRP в контексте SOLID
507 |
508 | Если двигаться дальше, получится, что каждый новый принцип можно
509 | рассматривать в контексте уже рассмотренных ранее. Так, [принцип
510 | единственной обязанности](#S) (SRP) гласит, что на одном объекте может лежать
511 | только одна обязанность. Сравнивая **OCP** и **SRP**, можно отметить их
512 | комплементарность, взаимодополняемость. Код, разработанный с учетом **SRP**,
513 | визуально будет близок к такому же коду, но учитывающему **OCP**. Когда у
514 | нас есть код, каждый объект которого имеет одну обязанность, введение
515 | новой функции создаст вторую обязанность, второй повод для изменения.
516 | Это может нарушить оба принципа.
517 |
518 | Точно также, если у нас есть код, который должен меняться только тогда,
519 | когда его основные функции меняются или, наоборот, должны оставаться
520 | неизменными при добавлении новой функции, то в этом коде будут соблюдены
521 | оба принципа. Но это не значит, что **SRP**-принцип всегда приводит к **OCP**,
522 | или, наоборот, но в преобладающем большинстве случаев, если соблюден
523 | один принцип, привести код к соблюдению второго не составит большого
524 | труда.
525 |
526 | ### Очевидный пример нарушения принципа OCP
527 |
528 | С исключительно технической точки зрения принцип открытости-закрытости
529 | очень прост: между двумя классами есть простые связи, но один из классов
530 | нарушает принцип **OCP**.
531 |
532 |
533 |
534 |
535 |
536 | Класс `User` напрямую использует класс `Logic`. Если второй класс `Logic`
537 | нужно реализовать таким образом, чтобы можно было использовать и старые
538 | наработки, и новые, существующий класс `Logic` придется несколько
539 | изменить. `User` напрямую связан с классом `Logic`, а значит, способа
540 | изменить его так, чтобы не пришлось менять и `User`, попросту не
541 | существует. Когда речь идет о языках со статической типизацией, то класс
542 | `User`, скорее всего, тоже придется изменить.
543 |
544 | Когда же речь идет о компилируемых языках, то исполняемые файлы `User` и
545 | `Logic` и динамические библиотеки потребуют перекомпиляции клиентов, а это
546 | – крайне нежелательный процесс.
547 |
548 | ### Смотрим код
549 |
550 | Основываясь на размещенной выше схеме, можно сделать вывод о том, что
551 | если любой один класс использует другой класс, то [принцип
552 | открытости-закрытости](#O) будет нарушаться. Строго говоря, это – верно.
553 | Очень интересно найти тот самый предел, ту черту, за которой приходит
554 | понимание: соответствовать принципу **OCP** гораздо сложнее, чем изменить
555 | уже существующий код, или же затраты на изменение кода будут слишком
556 | большими.
557 |
558 | К примеру, нужно написать класс, который показывает прогресс закачки
559 | файла через некое приложение в процентах. Использоваться будет два
560 | основных класса - `Progress` и `File`.
561 | ```php
562 | public function testItCanGetTheProgressOfAFileAsAPercent()
563 | {
564 | $file = new File();
565 | $file->length = 200;
566 | $file->sent = 100;
567 |
568 | $progress = new Progress($file);
569 |
570 | $this->assertEquals(50, $progress->getAsPercent());
571 | }
572 | ```
573 | В примере был использован `Progress`. В качестве результата нужно получить
574 | значение в процентах, независимо от фактического размера файла. Класс
575 | `File` был использован в качестве источника информации для класса
576 | `Progress`. У файла есть определенная длина в байтах и поле под названием
577 | `sent`, которое предоставляет объем данных, переданных загрузчику. В
578 | данный момент не важно, как именно эти значения будут обновляться в
579 | приложении. Можно предположить, что существует некая волшебная логика,
580 | которая это и делает, поэтому в примере их можно просто установить.
581 | ```php
582 | file = $file;
604 | }
605 |
606 | public function getAsPercent()
607 | {
608 | return $this->file->sent * 100 / $this->file->length;
609 | }
610 | }
611 | ```
612 | Проще говоря, `Progress` – это класс, который принимает `File` в
613 | конструкторе. Для ясности тип переменной был определен в параметрах
614 | конструктора. Существует единственный полезный метод в `Progress`, это -
615 | `getAsPercent()`, который принимает отправляемые значения и длину из `File`
616 | и переводит все в проценты. Просто, понятно и работает.
617 |
618 | Testing started at 5:39 PM ...
619 | PHPUnit 3.7.28 by Sebastian Bergmann.
620 | .
621 | Time: 15 ms, Memory: 2.50Mb
622 | OK (1 test, 1 assertion)
623 |
624 | Код выглядит правильно, но он все равно нарушает [принцип
625 | открытости-закрытости](#O). Но, как и почему?
626 |
627 | ### Изменение требований
628 |
629 | Вполне ожидаемо, что каждое приложение будет эволюционировать по мере
630 | того, как появится нужда в новых функциях. Одной из новых возможностей
631 | некоего нашего приложения может стать не только закачка музыки, но и ее
632 | прослушивание. Длина `File` представлена в байтах, а продолжительность
633 | музыки – в секундах. Слушателям нужно предложить хороший показатель
634 | прогресса, но нельзя ли использовать для этого уже существующий?
635 |
636 | Оказывается, нет, нельзя, так как наш прогресс уже связан с `File` и
637 | понимает только файлы. Это не изменится даже тогда, когда мы сможем
638 | переделать его для распознавания музыки. Но для того, чтобы появилось
639 | распознавание музыкальных файлов, нужно, чтобы `Progress` имел
640 | представление о `Music` и о `File`. Если бы решение соответствовало принципу
641 | **OCP**, то `File` или `Progress` не пришлось бы менять, а существующий
642 | показатель прогресса можно было бы легко адаптировать к музыке.
643 |
644 | ### Решение 1: используем динамическую природу PHP
645 |
646 | У динамически типизированных языков есть преимущество: они могут
647 | распознавать тип объекта в процессе исполнения. Это позволяет не
648 | использовать отдельное определение типа в конструкторе `Progress` с вполне
649 | работоспособным кодом.
650 | ```php
651 | file = $file;
660 | }
661 |
662 | public function getAsPercent()
663 | {
664 | return $this->file->sent * 100 / $this->file->length;
665 | }
666 | }
667 | ```
668 | Теперь в `Progress` можно добавить все, что только угодно.
669 | ```php
670 | artist . '/' . $this->album . '.png';
684 | }
685 | }
686 | ```
687 | И класс `Music` будет работать отлично. Проверить это можно на простом
688 | примере:
689 | ```php
690 | public function testItCanGetTheProgressOfAMusicStreamAsAPercent()
691 | {
692 | $music = new Music();
693 | $music->length = 200;
694 | $music->sent = 100;
695 |
696 | $progress = new Progress($music);
697 |
698 | $this->assertEquals(50, $progress->getAsPercent());
699 | }
700 | ```
701 | В общем, любое измеримое содержание можно использовать вместе с классом
702 | `Progress`. Можно выразить это в переменной, изменив ее имя:
703 | ```php
704 | measurableContent = $measurableContent;
713 | }
714 |
715 | public function getAsPercent()
716 | {
717 | return $this->measurableContent->sent * 100 / $this->measurableContent->length;
718 | }
719 |
720 | }
721 | ```
722 | Все, кажется, отлично, но в этом подходе есть одна громадная проблема.
723 | Когда `File` был указан в роли определителя типа, уверенность в том, что
724 | класс будет отменно работать, только крепчала. Это было вполне очевидно,
725 | а о каких-либо неточностях могла бы сообщить простая ошибка.
726 |
727 | Argument 1 passed to Progress::__construct()
728 | must be an instance of File,
729 | instance of Music given.
730 |
731 | Конечный результат был бы одинаковым в обоих случаях, но в первом мы
732 | получили бы сообщение. Правда, очень неясное. Нет способа узнать, что
733 | переменной (в нашем случае это - строка) не хватает каких-либо свойств
734 | или они просто не были найдены. Отладка и дебагинг в этом случае
735 | становятся проблемой: программисту приходится открывать класс `Progress` и
736 | перечитывать его для того, чтобы найти и понять проблему. Это можно
737 | уточнить по поведению `Progress`, а если точнее, то благодаря доступу к
738 | полям `sent` и `length` в методе `getAsPercent()`. Но в реальной жизни все
739 | может быть гораздо сложнее.
740 |
741 | Подобное решение нужно применять только в том случае, если ни одно из
742 | предложенных ниже решений нельзя реализовать с минимальными затратами
743 | (трудность реализации или слишком большие архитектурные изменения,
744 | которые не оправдают усилий).
745 |
746 | ### Решение 2: Стратегия паттернов проектирования
747 |
748 | Это – наиболее распространенное и наиболее доступное решение для
749 | соответствия **OCP**, простой и эффективный метод.
750 |
751 |
752 |
753 |
754 |
755 | Шаблон **Стратегия** отлично показывает используемый интерфейс. Интерфейс –
756 | это особый тип организации в объектно-ориентированном программировании,
757 | который определяет отношения между клиентом и классом сервера. Оба
758 | класса будут вести себя так, чтобы достигнуть желаемого.
759 | ```php
760 | setLength(200);
777 | $file->setSent(100);
778 |
779 | $progress = new Progress($file);
780 |
781 | $this->assertEquals(50, $progress->getAsPercent());
782 | }
783 | ```
784 | Как обычно, сначала пойдут примеры, где мы будем пользоваться сеттерами
785 | для установки значений. Кстати, сеттеры также можно определить в нашем
786 | измерительном интерфейсе (Measurable), но будьте внимательны с тем, что
787 | вы туда прописываете. Интерфейс для определения контракта между
788 | клиентским классом `Progress` и различными серверными классами `File` и
789 | `Music`.
790 |
791 | Нужно ли устанавливать значения для `Progress`? Вероятно, нет. Вряд ли вам
792 | придется определять сеттеры в интерфейсе, но если вы все же решите это
793 | сделать, то заставите все серверные классы работать с сеттерами. Одним
794 | из них вполне могут быть необходимы сеттеры, но другие могут вести себя
795 | неожиданно непредсказуемо. Что нужно сделать для того, чтобы `Progress`
796 | показывал температуру печи? Класс `OvenTemperature` можно инициализировать
797 | со значениями в конструкторе или же получить информацию от третьего
798 | класса. Но иметь здесь сеттеры – это странно.
799 | ```php
800 | length = $length;
813 | }
814 |
815 | public function getLength()
816 | {
817 | return $this->length;
818 | }
819 |
820 | public function setSent($sent)
821 | {
822 | $this->sent = $sent;
823 | }
824 |
825 | public function getSent()
826 | {
827 | return $this->sent;
828 | }
829 |
830 | public function getRelativePath()
831 | {
832 | return dirname($this->filename);
833 | }
834 |
835 | public function getFullPath()
836 | {
837 | return realpath($this->getRelativePath());
838 | }
839 | }
840 | ```
841 | Класс `File` был слегка изменен для того, чтобы соответствовать
842 | требованиям выше, и теперь он реализует интерфейс `Measurable`; в нем есть
843 | сеттеры и геттеры для полей, которые нам нужны. А класс `Music` на него
844 | очень похож.
845 | ```php
846 | measurableContent = $measurableContent;
855 | }
856 |
857 | public function getAsPercent()
858 | {
859 | return $this->measurableContent->getSent() * 100 / $this->measurableContent->getLength();
860 | }
861 |
862 | }
863 | ```
864 | Класс `Progress` тоже нужно немножко обновить, указав в конструкторе тип.
865 | Наш тип – это `Measurable` (измерительный). После этого у нас появляется
866 | явный контракт. В `Progress` всегда будут методы доступа – мы определили
867 | их в интерфейсе измерения. Классы `File` и `Music` всегда смогут сделать
868 | все, что нужно классу `Progress`, путем простого исполнения методов
869 | интерфейса: это необходимо, когда класс реализует интерфейс.
870 |
871 | ### Заметка об имени интерфейса
872 |
873 | Люди часто называют интерфейс с заглавной буквы или добавляют слово
874 | **«Interface»** в конце, например, `IFile` или `FileInterface`. Это обозначение
875 | старого образца и работало оно со старыми стандартами. Имя переменной
876 | или файла должно четко и ясно давать понять суть его содержимого. `IDE`
877 | определяет что-либо со скоростью в долю секунды и именно это позволяет
878 | нам сконцентрироваться на работе.
879 |
880 | Интерфейсы принадлежат их клиентам и поэтому, когда мы ходим дать имя
881 | интерфейсу, нам нужно полностью забыть о реализации и думать только о
882 | клиенте. Когда мы назвали интерфейс измеримым (Measurable), то думали о
883 | `Progress`. Если бы мы были прогрессом, что нам нужно было бы для того,
884 | чтобы переводить что-либо в проценты? Ответ более, чем прост: что-то,
885 | что можно посчитать. Поэтому мы и присвоили название `Measurable`.
886 |
887 | Не стоит забывать о том, что реализация может быть из разных областей. В
888 | нашем случае, это – музыка и файлы. Но готовый прогресс мы легко можем
889 | заново использовать в гоночном симуляторе, и тогда нашими измеряемыми
890 | классами станут скорость, количество топлива и так далее.
891 |
892 | ### Решение 3: используем шаблонный метод
893 |
894 | **Шаблонный метод** очень смахивает на стратегию, но с одним отличием:
895 | вместо интерфейса он использует абстрактные классы. Шаблонный метод
896 | рекомендуется использовать в том случае, если клиент для нашего
897 | приложения очень специфический, с небольшой возможностью повторного
898 | использования и в том случае, если у серверных классов общее поведение.
899 |
900 |
901 |
902 |
903 |
904 | ### Просмотр высокого уровня
905 |
906 | Итак, как же все это влияет на нашу архитектуру высокого уровня?
907 |
908 | Если изображение выше представляет текущую архитектуру нашего
909 | приложения, то добавление нового модуля с пятью классами (синий цвет)
910 | должно вполне ожидаемо повлиять на всю расстановку (красный цвет).
911 |
912 |
913 |
914 |
915 |
916 | В большинстве систем невозможно не повлиять на код при вводе новых
917 | классов. Но соответствие [принципу открытости-закрытости](#O) может
918 | значительно сократить классы и модули, требующие постоянного изменения.
919 |
920 | Изучая очередной новый принцип, не пытайтесь удержать в голове
921 | одновременно с ним и все остальное, иначе вы просто запутаетесь в
922 | интерфейсах для каждого класса. Такую конструкцию будет тяжело понимать
923 | и поддерживать. Наиболее оптимальным решением в этом случае станет учет
924 | возможностей и определение, будут ли здесь другие типы и серверные
925 | классы.
926 |
927 | В общем-то, можно легко представить себе новую функцию или найти ее в
928 | логах другого серверного класса. В таких случаях, интерфейс нужно
929 | добавить с самого начала. Если же вы не уверены или не можете
930 | разобраться – просто пропустите эту часть. Пусть добавлением интерфейса
931 | занимается другой программист или даже вы, но в будущем.
932 |
933 | Если вы будете все продумывать при добавлении интерфейсов, то
934 | модификаций будет мало, они будут быстрыми и легкими. Помните, если код
935 | пришлось изменить один раз, скорее всего, это нужно будет делать снова и
936 | здесь принцип открытости-закрытости может здорово помочь.
937 |
938 | SOLID «L»: Принцип подстановки Барбары Лисков (Liskov substitution principle, LSP)
939 | ===
940 |
941 | **Подклассы не могут замещать поведение базовых классов.**
942 |
943 | Концепция принципа подстановки была предложена Барбарой Лисков в ее докладе на
944 | конференции 1987 года, а спустя 7 лет – опубликована в соавторстве с
945 | Джаннет Вин. Оригинальное определение принципа, предложенное Барбарой,
946 | следующее:
947 |
948 | «В том случае, если q(x) – свойство, верное по отношению к объектам х
949 | некого типа T, то свойство q(y) тоже будет верным относительно ряда
950 | объектов y, которые относятся к типу S, при этом S – подтип некого типа
951 | T.»
952 |
953 | Некоторое время спустя, после публикации Робертом С. Мартином всей
954 | пятерки принципов SOLID в книге о быстрой разработке программ, а затем и
955 | после публикации версии книги о быстрой разработке для языка
956 | программирования C\#, принцип стал называться принципом подстановки
957 | Барбары Лисков.
958 |
959 | Это приводит нас к определению, которое дал сам Роберт С. Мартин:
960 |
961 | **Подтипы должны дополнять базовые типы.**
962 |
963 | Если это разъяснить, то получится, что подклассы должны переопределять
964 | методы базового класса так, чтобы не нарушалась функциональность с точки
965 | зрения клиента. Подробно это можно рассмотреть на простом примере:
966 |
967 | Есть существующий класс `Vehicle`, который может быть и абстрактным в том
968 | числе, и две реализации:
969 | ```php
970 | engageIgnition();
990 | parent::startEngine();
991 | }
992 |
993 | private function engageIgnition()
994 | {
995 | // Ignition procedure
996 | }
997 | }
998 |
999 | class ElectricBus extends Vehicle
1000 | {
1001 | public function accelerate()
1002 | {
1003 | $this->increaseVoltage();
1004 | $this->connectIndividualEngines();
1005 | }
1006 |
1007 | private function increaseVoltage()
1008 | {
1009 | // Electric logic
1010 | }
1011 |
1012 | private function connectIndividualEngines()
1013 | {
1014 | // Connection logic
1015 | }
1016 |
1017 | }
1018 | ```
1019 | Клиентский класс должен иметь возможность использовать любой из них,
1020 | если он может использовать `Vehicle`.
1021 | ```php
1022 | startEngine();
1029 | $v->accelerate();
1030 | }
1031 | }
1032 | ```
1033 | А это уже приводит нас к простой реализации шаблонного метода
1034 | проектирования так же, как он использовался и с принципом
1035 | открытости-закрытости.
1036 |
1037 |
1038 |
1039 |
1040 |
1041 | Основываясь на предыдущем опыте с принципом открытости-закрытости, можно
1042 | сделать вывод, что принцип Барбары Лисков сильно с ним связан. И в самом
1043 | деле, как сказал Роберт Мартин, нарушение принципа **LSP** – это скрытое
1044 | нарушение принципа **OCP**. Шаблонный метод проектирования – классический
1045 | пример соблюдения и реализации принципа подстановки, который, в свою
1046 | очередь, является одним из способов соблюдения **OCP**.
1047 |
1048 | ### Классический пример нарушения принципа LSP
1049 |
1050 | Чтобы показать нарушение как можно полнее и нагляднее, будет использован
1051 | классический понятный пример.
1052 | ```php
1053 | height = $height;
1064 | }
1065 |
1066 | public function getHeight()
1067 | {
1068 | return $this->height;
1069 | }
1070 |
1071 | public function setWidth($width)
1072 | {
1073 | $this->width = $width;
1074 | }
1075 |
1076 | public function getWidth()
1077 | {
1078 | return $this->width;
1079 | }
1080 |
1081 | }
1082 | ```
1083 | Мы начнем с основной геометрической формы – прямоугольника (Rectangle).
1084 | Это всего лишь простой объект данных с сеттерами и геттерами для ширины
1085 | (width) и высоты (height). Если представить, что приложение уже работает
1086 | и даже на нескольких клиентах, которым нужно управлять этим
1087 | прямоугольником так, чтобы сделать из него квадрат, то придется ввести
1088 | новые функции.
1089 |
1090 | В реальной жизни, в геометрии, квадрат – это просто одна из форм
1091 | прямоугольника. Поэтому нужно попробовать реализовать класс `Square`,
1092 | расширяющий класс `Rectangle`. На первый взгляд, кажется, что подкласс –
1093 | это базовый класс, а принцип подстановки не нарушается.
1094 |
1095 |
1096 |
1097 |
1098 |
1099 | Но будет ли квадрат `Square` прямоугольником `Rectangle` уже в
1100 | программировании?
1101 | ```php
1102 | width = $value;
1109 | $this->height = $value;
1110 | }
1111 |
1112 | public public function setWidth($value)
1113 | {
1114 | $this->width = $value;
1115 | $this->height = $value;
1116 | }
1117 | }
1118 | ```
1119 | Квадрат – это прямоугольник с одинаковой шириной и высотой, а значит,
1120 | реализация в примере выше была бы не совсем корректной. Можно было бы
1121 | переписать сеттеры, чтобы установить ширина и высоту. Но как это
1122 | повлияет на клиентский код?
1123 | ```php
1124 | setWidth(5);
1131 | $rect->setHeight(4);
1132 |
1133 | if($rect->area() != 20) {
1134 | throw new Exception('Bad area!');
1135 | }
1136 |
1137 | return true;
1138 | }
1139 | }
1140 | ```
1141 | Реально получить клиентский класс, который проверяет площадь
1142 | прямоугольника, и реагирует, если ее значение оказывается неправильным.
1143 | ```php
1144 | public function area()
1145 | {
1146 | return $this->width * $this->height;
1147 | }
1148 | ```
1149 | Ну и конечно добавлен метод класса `Rectangle`.
1150 | ```php
1151 | assertTrue($client->areaVerifier($rect));
1160 | }
1161 |
1162 | }
1163 | ```
1164 | С помощью простого теста можно проверить работу: отправим пустой
1165 | прямоугольный объект для определения его площади. Работает. Если наш
1166 | класс `Square` определяется корректно, то его отправка на клиентский
1167 | `areaVerifier()` не повредит функциональности. В конце концов, в
1168 | математическом понимании `Square` – это все тот же `Rectangle`. Но наш ли
1169 | это класс?
1170 | ```php
1171 | public function testSquareArea()
1172 | {
1173 | $rect = new Square();
1174 | $client = new Client();
1175 | $this->assertTrue($client->areaVerifier($rect));
1176 | }
1177 | ```
1178 | Тестирование проходит легко и много времени не занимает. А уведомление
1179 | появляется при запуске теста выше.
1180 |
1181 | PHPUnit 3.7.28 by Sebastian Bergmann.
1182 |
1183 | Exception : Bad area!
1184 | #0 /paht/: /.../.../LspTest.php(18): Client->areaVerifier(Object(Square))
1185 | #1 [internal public function]: LspTest->testSquareArea()
1186 |
1187 | Итак, в нашем «программном» смысле `Square` класс - это не `Rectangle`,
1188 | иначе бы законы геометрии и принцип подстановки Барбары Лисков
1189 | нарушались.
1190 |
1191 | Этот пример особенно хорош тем, что он показывает и нарушение **LSP**, и то,
1192 | что объектно-ориентированное программирование не может применить правила
1193 | реальной жизни к объектам. Каждый объект здесь должен быть абстракцией
1194 | над концепцией. А если мы попытаемся сопоставить реальный объект и
1195 | программный объект, то у нас никогда это не получится.
1196 |
1197 |
1198 | SOLID «I»: Принцип разделения интерфейса (interface segregation principle, ISP)
1199 | ===
1200 |
1201 | Суть принципа разделения интерфейса – в бизнес-логике и клиентском
1202 | общении. Во всех модульных приложениях должен быть интерфейс, которым
1203 | может воспользоваться клиент. Это может быть классический объект,
1204 | реализуемый в шаблонах проектирования вроде `Facades`. Не важно,
1205 | применяется то или иное решение. Суть всегда остается той же: объяснить
1206 | клиентскому коду как правильно использовать модуль. Эти интерфейсы могут
1207 | находиться между различными модулями в одном приложении или проекте, или
1208 | между одним проектом в качестве сторонней библиотеки, служащей для
1209 | подачи еще одного проекта.
1210 |
1211 |
1212 |
1213 |
1214 |
1215 | Хороший способ на старте определить, что именно мы хотим реализовать в
1216 | нашем модуле. Подобное начало может привести к одной из реализаций:
1217 |
1218 | 1. большой класс `Car` или `Bus` реализует методы интерфейса `Vehicle`. Одни
1219 | размеры таких классов советуют избегать их любой ценой;
1220 | 2. маленькие классы вроде `LightsControl`, `SpeedControl` или `RadioCD`
1221 | реализуют весь интерфейс, но делают что-то полезное только для
1222 | реализуемых ими частей;
1223 |
1224 | Очевидно, что ни одно из этих решений не подходит для реализации нашей
1225 | бизнес-логики.
1226 |
1227 |
1228 |
1229 |
1230 |
1231 | Мы могли бы попробовать еще один подход: разбить интерфейс на куски,
1232 | каждый из которых займется своей реализацией. Для маленьких классов –
1233 | идеально. Объекты, реализующие интерфейсы, будут использовать другой тип
1234 | транспорта, например, машину (car) на картинке выше.
1235 |
1236 |
1237 |
1238 |
1239 |
1240 | Но это может фундаментально изменить наше восприятие архитектуры. Вместо
1241 | реализации `Car` становится клиентом. А мы хотим дать возможность клиенту
1242 | использовать весь модуль.
1243 |
1244 |
1245 |
1246 |
1247 |
1248 | Предположим, что проблема реализации уже решена, а бизнес-логика –
1249 | стабильна. Прежде всего, нужно обеспечить единый интерфейс со всеми
1250 | реализациями и пусть клиенты `BusStation`, `HighWay`, `Driver` используют все,
1251 | что угодно из реализации. Это перекладывает ответственность за поведение
1252 | на клиентов (подобный метод часто применялся в старых приложениях).
1253 | [Принцип разделения интерфейса](#I) утверждает, что ни один клиент не должен
1254 | зависеть от неиспользуемых методов.
1255 |
1256 | Но есть одна проблема: все клиенты зависят от всех методов. Разве
1257 | `BusStation` должен зависеть от радиостанции, которую выбрал водитель, или
1258 | от фар автобуса? Нет. Но что, если так оно и будет? Нужно вспомнить
1259 | [принцип единой обязанности](#S). Если `BusStation` зависит от многих реализаций
1260 | (и даже не используемых), то он может потребовать изменений, если
1261 | изменится одна из реализаций. Такого быть не должно.
1262 |
1263 | Интерфейсы относятся к клиентам, а не к реализациям, поэтому и создавать
1264 | их нужно в лучших отношениях с клиентом. Мы должны разбить интерфейсы на
1265 | кусочки так, чтобы они лучше работали с клиентами.
1266 |
1267 |
1268 |
1269 |
1270 |
1271 | Конечно, могут появиться дубляжи, но помните, что интерфейсы – это
1272 | простые определения имен функций и логика в них не реализуется, а
1273 | значит, и проблем с дублированием не будет.
1274 |
1275 | Здесь есть преимущество перед клиентами, которые зависят только от того,
1276 | что им нужно и что они используют сами по себе. В некоторых случаях
1277 | клиенты могут использовать и нуждаться в нескольких интерфейсах, и это
1278 | совершенно нормально, пока они используют все методы всех интерфейсов,
1279 | от которых зависят.
1280 |
1281 | Еще один приятный момент заключается в том, что в нашей бизнес-логике
1282 | один класс может реализовать несколько интерфейсов, если ему это
1283 | понадобится. Так что мы можем обеспечить единую реализацию для всех
1284 | общих методов между интерфейсами. Интерфейсы также заставляют нас думать
1285 | о нашем коде больше с точки зрения клиента, что, в свою очередь,
1286 | приводит нас к уже более легкому тестированию. Так что мы не только
1287 | сделали наш код лучше для наших клиентов, мы также сделали его проще для
1288 | себя, чтобы легче понимать, тестировать и реализовывать.
1289 |
1290 | ### Резюме
1291 |
1292 | [Принцип подстановки Барбары
1293 | Лисков](#L) демонстрирует,
1294 | почему реальные объекты нельзя сопоставлять один к одному с объектами
1295 | программирования, и учит писать код таким образом, чтоб подтипы хорошо
1296 | уживались с базовыми типами. Принцип подстановки дополняет другие
1297 | принципы SOLID и проще интерпретируется в их контексте.
1298 |
1299 | Принцип разделения интерфейса учит нас уважать клиентов еще сильнее, чем
1300 | мы это делали раньше. Внимание к их потребностям может сделать наш код
1301 | намного лучше, а нашу работу – проще и интереснее.
1302 |
1303 |
1304 | SOLID «D»: Принцип инверсии зависимостей (dependency inversion principle, DIP)
1305 | ===
1306 |
1307 | Принцип [единственной
1308 | обязанности](#S), [открытости-закрытости](#O), [подстановки](#L), [разделения
1309 | интерфейсов](#I) и [инверсии
1310 | зависимостей](#D) – пятерка принципов, на которые следует ориентироваться при
1311 | написании кода.
1312 |
1313 | Хотя говорить о преобладающей важности одного из принципов будет не
1314 | верно, но **отметить влияние принципа инверсии зависимостей на код
1315 | нужно обязательно**. Если вы заметили, что другие принципы трудно понять
1316 | или применить, начните с этого, а остальные применяйте уже к коду,
1317 | соответствующему **DIP**
1318 |
1319 | ### Определение
1320 |
1321 | 1. Модули высокого уровня не должны зависеть от модулей низкого уровня.
1322 | Оба должны зависеть от абстракций.
1323 | 2. Абстракции не должны зависеть от деталей. Детали должны зависеть от
1324 | абстракций.
1325 |
1326 | Этот принцип выделил Роберт С. Мартин в своей книге о быстрой разработке
1327 | программ, а затем переиздал в версии книги для языка C\#. Принцип
1328 | инверсии зависимостей - последний из пятерки принципов SOLID.
1329 |
1330 | ### DIP в реальном мире
1331 |
1332 | Если вы программируете не очень аккуратно, не совсем разбираетесь в
1333 | правилах программирования и не соблюдаете ряд принципов SOLID, в своей
1334 | работе вам, возможно, придется пройти все 7 кругов ада, ощутить все на
1335 | собственном опыте, прежде чем код станет действительно хорошим.
1336 |
1337 | Принципы SOLID - это исключительно архитектурные принципы Роберта С.
1338 | Мартина, которые полностью меняют правила игры, весь ход
1339 | программирования. Далее мы проиллюстрируем влияние нескольких
1340 | архитектурных решений, которые появились благодаря принципу **DIP**, и
1341 | серьезно повлияли на один из наших проектов.
1342 |
1343 | Большинство веб-проектов включают в себя три основных технологии: `HTML`,
1344 | `PHP` и `SQL`. Определенная версия каждого из этих приложений, о которых мы
1345 | говорим, или то, какой тип реализаций `SQL` вы используете – все это не
1346 | имеет абсолютно никакого значения в нашем случае. Дело в том, что
1347 | информация из `HTML` формы должна заканчиваться в базе данных. Остальное
1348 | обеспечит `РНР`.
1349 |
1350 | Из этого следует уяснить одну важную вещь - хороший способ, которым эти
1351 | три технологии представляют три различных архитектурных слоя:
1352 |
1353 | 1. пользовательский интерфейс,
1354 | 2. бизнес-логику,
1355 | 3. долговременное сохранение данных.
1356 |
1357 | Скоро мы поговорим об этих слоях. А сейчас давайте сосредоточимся на
1358 | некоторых странных, но часто встречающихся решениях, применяемых для
1359 | того, чтобы заставить технологии работать вместе.
1360 |
1361 | Есть много проектов, которые используют `SQL` код в тегах `PHP` внутри
1362 | `HTML`-файла. Или `PHP`-код отражает и страницы `HTML`, и непосредственно
1363 | интерпретирует глобальные переменные `$_GET` или `$_POST`. Казалось бы,
1364 | все хорошо. Но в чем же тогда проблема?
1365 |
1366 |
1367 |
1368 |
1369 |
1370 | На изображении выше показана сырая версия того, что мы описывали в
1371 | предыдущем абзаце. Стрелки указывают на различные зависимости, и мы
1372 | можем сделать вывод, что все базируется на всем, все зависит от всего.
1373 | Если нам придется изменить таблицу базы данных, скорее всего, в конечном
1374 | итоге, мы закончим редактированием `HTML`-файла. Или если мы изменим поле
1375 | в `HTML`, то закончим изменением столбца в операторе `SQL`. Или, как видно
1376 | из второй схемы, нам действительно придется сильно изменить наш `PHP`,
1377 | если изменится `HTML`. А в худшем случае, если мы генерируем весь
1378 | `HTML`-контент из `PHP`-файла, скорее всего, нам придется менять `PHP`, чтобы
1379 | изменить `HTML`-контент. Поэтому нет никаких сомнений в том, что
1380 | зависимости просто лавируют между нашими классами и модулями. Но на этом
1381 | все не заканчивается: вы можете хранить операции или код на `PHP` в
1382 | таблицах `SQL`.
1383 |
1384 |
1385 |
1386 |
1387 |
1388 | На схеме выше видно, что запросы к базе данных `SQL` возвращают `PHP`-код,
1389 | сгенерированный с данными из таблиц. Эти `PHP`-е функции или классы
1390 | создают другие `SQL`-запросы, которые возвращают уже другой `PHP`-код. Этот
1391 | цикл будет продолжаться до тех пор, пока вся информация не будет
1392 | получена и возвращена, вероятно, пользовательскому интерфейсу.
1393 |
1394 | Многим это покажется полной чушью, но если вы еще не работали с
1395 | проектом, созданным и реализованным по этому сценарию, то в будущем,
1396 | скорее всего, вам придется с этим столкнуться. Большинство существующих
1397 | проектов, независимо от используемых языков программирования, были
1398 | написаны программистами старой закалки, которым было все равно или они
1399 | не знали, как можно сделать код лучше. Если вы сейчас читаете этот
1400 | мануал о принципах программирования, значит, вам нужно научиться
1401 | программировать на порядок лучше, чем вы можете на сегодняшний день. Это
1402 | значит, что вы **уважаете** или даже только начинаете уважать свою
1403 | профессию, **хотите понять свое ремесло и сделать его лучше**.
1404 |
1405 | Еще одна версия может рассказать нам об ошибках, сделанных нашими
1406 | предшественниками, и последствиями от них. В некоторых проектах вы
1407 | можете получить, в итоге, практически неподдерживаемое состояние из-за
1408 | их старой и кросс-зависимой архитектуры. В итоге, вам придется просто
1409 | отказаться от таких проектов навсегда, и тогда вы поймете, что больше не
1410 | хотите повторять эти ошибки снова. Поэтому стремитесь делать чистую
1411 | архитектуру, которая будет соответствовать принципам SOLID, во-первых, а
1412 | во-вторых, принципу инверсии зависимостей.
1413 |
1414 |
1415 |
1416 |
1417 |
1418 | В этой архитектуре есть несколько интересных моментов:
1419 |
1420 | 1. пользовательский интерфейс (в большинстве случаев, веб-фреймворк
1421 | `MVC`) или любой другой механизм доставки в нашем проекте будет
1422 | зависеть от бизнес-логики. Бизнес-логика сама по себе довольно
1423 | абстрактна, а пользовательский интерфейс – само воплощение
1424 | конкретики. Он представляет собой одну из деталей проекта, и к тому
1425 | же, очень нестабильную. Ничто не должно зависеть от
1426 | пользовательского интерфейса, ничто не должно зависеть от
1427 | веб-фреймворка `MVC`;
1428 | 2. еще одно интересное наблюдение, сделанное нами, говорит, что
1429 | долговременное сохранение, база данных, наш `MySQL` или `PostgreSQL`
1430 | базируются на бизнес-логике. Это позволяет менять сохраняемость так,
1431 | как это нужно нам. Если завтра нам понадобится изменить `MySQL` вместе
1432 | с `PostgreSQL` или просто текстовые файлы, мы легко сможем это
1433 | сделать. Нам, конечно, придется реализовать определенный уровень
1434 | сохраняемости для новых методов сохранения состояния, но для этого
1435 | не придется менять отдельные строки кода в нашей бизнес-логике;
1436 | 3. В конце концов, в правой части нашей бизнес-логики, вне ее, у нас
1437 | есть все классы, которые создают классы бизнес-логики. Эти классы
1438 | созданы как точки входа в наши приложения. Многие люди склонны
1439 | думать, что они принадлежат к бизнес-логике, но делают они это
1440 | только для того, чтобы создать бизнес-объекты. Они – просто классы,
1441 | которые помогают нам создавать другие классы. Бизнес-объекты и
1442 | логика, которую они обеспечивают, не зависят от них. Мы могли бы
1443 | использовать различные модели или создать простой объект, чтобы
1444 | обеспечить бизнес-логику. Это не имеет значения. После того, как
1445 | бизнес-объекты созданы, они начинают выполнять свою работу.
1446 |
1447 | ### Приступим к коду
1448 |
1449 | Соблюдать [принцип инверсии зависимостей](#D) (DIP) на архитектурном уровне
1450 | довольно легко, если вы соблюдаете классические шаблоны проектирования.
1451 | Использовать и показать это внутри бизнес-логики достаточно просто, а
1452 | местами - даже весело. Представим себе приложение для чтения электронных
1453 | книг.
1454 | ```php
1455 | assertRegExp('/pdf book/', $reader->read());
1465 | }
1466 | }
1467 |
1468 | class PDFReader
1469 | {
1470 | private $book;
1471 |
1472 | public function __construct(PDFBook $book)
1473 | {
1474 | $this->book = $book;
1475 | }
1476 |
1477 | public function read()
1478 | {
1479 | return $this->book->read();
1480 | }
1481 |
1482 | }
1483 |
1484 | class PDFBook
1485 | {
1486 | public function read()
1487 | {
1488 | return "reading a pdf book.";
1489 | }
1490 | }
1491 | ```
1492 | Мы начали разработку электронной читалки как читалки `PDF`. Пока не
1493 | возникло никаких проблем. У нас есть класс `PDFReader`, который использует
1494 | `PDFBook`. Функция `read()` в читалке относится к методу `read()`. В этом мы
1495 | убедимся путем регулярной проверки выражений после ключевого элемента
1496 | строки, которые возвращает метод PDFBook's reader().
1497 |
1498 | Не забывайте о том, что это всего лишь пример. Мы не станем
1499 | реализовывать логику чтения `PDF`-файлов или любых других форматов файлов.
1500 | Наши тесты – проверки на некоторых базовых строках. Если бы мы писали
1501 | настоящее приложение, единственная разница заключалась бы в способе
1502 | тестирования разных файловых форматов. Структура зависимости была бы
1503 | очень похожа на предложенную в примере.
1504 |
1505 |
1506 |
1507 |
1508 |
1509 | Использование читалки формата `PDF`, которая использует книгу в формате
1510 | `PDF`, может быть вполне здравым решением в ряде ограниченных случаев.
1511 | Если наша задача заключалась в том, чтобы написать только читалку
1512 | формата `PDF`, то такое решение фактически соответствует задаче. Но мы
1513 | хотели написать универсальную читалку, которая использовала бы несколько
1514 | разных форматов, в том числе, и уже реализованную `PDF`-версию.
1515 | Переименуем класс нашей читалки.
1516 | ```php
1517 | assertRegExp('/pdf book/', $r->read());
1527 | }
1528 | }
1529 |
1530 | class EBookReader
1531 | {
1532 | private $book;
1533 |
1534 | public function __construct(PDFBook $book)
1535 | {
1536 | $this->book = $book;
1537 | }
1538 |
1539 | public function read()
1540 | {
1541 | return $this->book->read();
1542 | }
1543 | }
1544 |
1545 | class PDFBook
1546 | {
1547 | public function read()
1548 | {
1549 | return "reading a pdf book.";
1550 | }
1551 | }
1552 | ```
1553 | Переименование не дало каких-либо функциональных эффектов. Тест пройден
1554 | на «отлично».
1555 |
1556 | Testing started at 1:04 PM ...
1557 | PHPUnit 3.7.28
1558 | Time: 13 ms, Memory: 2.50Mb
1559 | OK (1 test, 1 assertion)
1560 | Process finished with exit code 0
1561 |
1562 |
1563 |
1564 |
1565 |
1566 | Наша читалка уже стала гораздо абстрактнее. Намного универсальнее.
1567 | Сейчас у нас есть универсальный `EBookReader`, который может читать
1568 | довольно специфический формат книг – `PDFBook`. Наша абстракция зависит от
1569 | деталей. То, что наша книга находится в формате `PDF` – все лишь деталь,
1570 | от которой ничего не должно зависеть.
1571 | ```php
1572 | assertRegExp('/pdf book/', $reader->read());
1582 | }
1583 |
1584 | }
1585 |
1586 | interface EBook
1587 | {
1588 | public function read();
1589 | }
1590 |
1591 | class EBookReader
1592 | {
1593 | private $book;
1594 |
1595 | public function __construct(EBook $book)
1596 | {
1597 | $this->book = $book;
1598 | }
1599 |
1600 | public function read()
1601 | {
1602 | return $this->book->read();
1603 | }
1604 |
1605 | }
1606 |
1607 | class PDFBook implements EBook
1608 | {
1609 | public function read()
1610 | {
1611 | return "reading a pdf book.";
1612 | }
1613 | }
1614 | ```
1615 | Наиболее популярным и часто используемым решением для того, чтобы
1616 | инвертировать зависимость, является введение более абстрактного модуля в
1617 | наш проект. Наиболее абстрактным элементом в ООП является интерфейс.
1618 | Таким образом, любой другой класс может зависеть от интерфейса, и все
1619 | еще соблюдать **DIP**.
1620 |
1621 | Мы создали интерфейс для нашей читалки, который назвали `EBookReader`, и
1622 | который отражает все потребности `EBook`. Это – прямой результат
1623 | соблюдения [принципа разделения интерфейса](#I), который основывается на идее,
1624 | что интерфейсы должны отражать потребности клиентов. Интерфейсы
1625 | относятся к клиентам и называются таким образом, чтобы отражать типы и
1626 | объекты, которые необходимы клиентам. Интерфейсы должны содержать
1627 | методы, которые клиенты хотят использовать. Для `EBookReader` вполне
1628 | естественно использовать `EBook` и содержать метод `read()`.
1629 |
1630 |
1631 |
1632 |
1633 |
1634 | Теперь вместо единственной зависимости, у нас их становится две.
1635 |
1636 | 1. Первая зависимость указывает от `EBookReader` к интерфейсу `EBook` и на
1637 | используемый тип. `EBookReader` использует `EBook`.
1638 | 2. Вторая зависимость уже несколько другого рода. Она указывает от
1639 | `PDFBook` к тому же интерфейсу `EBook`, но на реализуемый тип. `PDFBook` –
1640 | это просто особая форма `EBook`, которая реализует интерфейс для того,
1641 | чтобы удовлетворить потребности клиентов.
1642 |
1643 | Неудивительно, ведь такое решение позволит нам просматривать в нашей
1644 | читалке различные типы электронных книг. Единственное условие для всех
1645 | этих книг – они должны соответствовать интерфейсу `EBook` и реализовывать
1646 | его.
1647 | ```php
1648 | assertRegExp('/pdf book/', $reader->read());
1658 | }
1659 |
1660 | public function testItCanReadAMobiBook()
1661 | {
1662 | $book = new MobiBook();
1663 | $reader = new EBookReader($book);
1664 |
1665 | $this->assertRegExp('/mobi book/', $reader->read());
1666 | }
1667 |
1668 | }
1669 |
1670 | interface EBook
1671 | {
1672 | public function read();
1673 | }
1674 |
1675 | class EBookReader
1676 | {
1677 | private $book;
1678 |
1679 | public function __construct(EBook $book)
1680 | {
1681 | $this->book = $book;
1682 | }
1683 |
1684 | public function read()
1685 | {
1686 | return $this->book->read();
1687 | }
1688 |
1689 | }
1690 |
1691 | class PDFBook implements EBook
1692 | {
1693 | public function read()
1694 | {
1695 | return "reading a pdf book.";
1696 | }
1697 | }
1698 |
1699 | class MobiBook implements EBook
1700 | {
1701 | public function read()
1702 | {
1703 | return "reading a mobi book.";
1704 | }
1705 | }
1706 | ```
1707 | Все это приводит нас к принципу [открытости-закрытости](#O), и круг
1708 | замыкается. [Принцип инверсии зависимостей](#D) помогает нам соблюдать все
1709 | остальные принципы из пятерки SOLID. Соблюдая принцип **DIP**, мы
1710 | практически начинаем соблюдать **OCP**, можем [разделять обязанности](#S),
1711 | [правильно используем подтипы](#L) и можем [разделять интерфейсы](#I).
1712 |
1713 | ### Финальные мысли
1714 |
1715 | Ну, вот и все, мы наконец-то закончили и полностью разобрали все мануалы
1716 | о принципах SOLID. Это должно полностью изменить ваши представления об
1717 | архитектуре и сделать вашу работу проще и интереснее. Мы должны
1718 | стремиться делать наш код лучше, используя эту пятерку.
1719 |
1720 | В объектно-ориентированном программировании пятерка принципов SOLID –
1721 | один из важнейших стержней, которые должны делать код лучше, а жизнь
1722 | программистов - проще.
1723 |
--------------------------------------------------------------------------------