Чистая архитектура на PHP. Как её измерять и контролировать?

Предисловие


Как ты уже понял из названия, говорить я тут собрался о самом, что ни на есть, “высоком” — Чистой архитектуре. А толчком к развитию всей этой истории послужила книга Роберта Мартина “Чистая архитектура”. Если еще не читал, осмелюсь порекомендовать! Автор раскрывает много важных тем, активно делится своим богатым жизненным опытом (из проф. области естественно) и сделанными на его основе выводами, эпизодически вплетает в главы истории о том, как виртуозно говнокодили (ну и не только, конечно же) наши отцы и деды в далёких 60-х, 70-х, 80-х и даже лихих 90-х, как по крупинкам собирали всеми любимые принципы SOLID и их аналоги в мире компонентов, и чему научились за прошедшие полвека. В процессе чтения книги хорошо прослеживается линия развития индустрии разработки ПО, типичные проблемы, с которыми пацанам приходилось сталкиваться, и способы их решения.

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

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

Для удобства статья поделена на 2 части.

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

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

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

Содержание



Часть 1


Что же такое архитектура ПО?


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

Какова цель архитектуры?


Здесь ответ один:

Цель архитектуры ПО — уменьшить человеческие трудозатраты на создание и сопровождение системы.

Немного вводной..

С чего начинаются многие проекты?


«Мы сможем навести порядок потом, нам бы только выйти на рынок!»
В результате порядок так и не наводится, потому что давление конкуренции на рынке никогда не ослабевает.

«Самая большая ложь, в которую верят многие разработчики, – что грязный код поможет им быстро выйти на рынок, но в действительности он затормозит их движение в долгосрочной перспективе. Разработчики, уверовавшие в эту ложь, проявляют самонадеянность Зайца, полагая, что в будущем смогут перейти от создания беспорядка к наведению порядка, но они допускают простую ошибку. Дело в том, что создание беспорядка всегда оказывается медленнее, чем неуклонное соблюдение чистоты, независимо от выбранного масштаба времени».

Две ценности программных продуктов


  1. Поведение (нечто срочное, но не всегда важное)
  2. Архитектура (нечто важное, но не всегда срочное)

Многие зачастую с головой утопают в первой, и полностью пренебрегают второй.

Типа: “Зачем тратить время на основательное продумывание архитектуры, главное ведь чтоб работало правильно!”.

Но они упускают один важный момент: «Если правильно работающая программа не допускает возможности ее изменения, она перестанет работать правильно, когда изменятся требования, и они не смогут заставить ее работать правильно. То есть программа станет бесполезной.

Если же программа работает неправильно, но легко поддается изменению, они смогут заставить работать ее правильно и поддерживать ее работоспособность по мере изменения требований. То есть программа постоянно будет оставаться полезной.»

Мне понравилось такое высказывание: “Если вы думаете, что хорошая архитектура стоит дорого, попробуйте плохую архитектуру.” (Брайан Фут и Джозеф Йодер)

Выходит важно уделять должное внимание чистоте кода и архитектуре в целом с самых первых строк. Но, если с чистотой кода всё относительно понятно (вынюхивай себе всякие плохие запахи в коде, да исправляй, к тому ещё и инструментов полным-полно умные IDE, статические анализаторы и прочая несомненно важная дребедень), то с архитектурой не совсем так (по крайней мере мне).

Как можно контролировать то, что настолько расплывчато, что с трудом поддаётся даже определению единой формулировкой, что имеет весьма разностороннее представление и каждый второй, блин, говнюк это видит и норовит трактовать по своему, если ты не внук Ванги, не выигрывал в шоу “Интуиция” и не отличаешься особой чувствительностью пятой точки? Тут автор книги подкидывает парочку ценных мыслей для размышления… Давай разбираться.

Зачем придумали SOLID и почему одного его не достаточно?


Все мы наверняка знаем о принципах SOLID.

Их цель – создать программные структуры среднего уровня, которые:

  • терпимы к изменениям;
  • просты и понятны;
  • образуют основу для компонентов, которые могут использоваться во многих программных системах.

Но: «Как из хороших кирпичей можно сложить никуда не годную стену, так и из хорошо продуманных компонентов среднего уровня можно создать никуда не годную систему».
Поэтому в мире компонентов есть аналоги принципов SOLID, а также есть высокоуровневые принципы создания архитектур.

Я этих тем коснусь очень поверхностно, подробнее можно ознакомиться в книге. Погнали!

Связность компонентов и принципы её определяющие


«К какому компоненту отнести тот или иной класс? Это важное решение должно приниматься в соответствии с зарекомендовавшими себя принципами разработки программного обеспечения. К сожалению, подобные решения носят особый характер и принимаются почти исключительно исходя из контекста»

REP: Reuse/Release Equivalence Principle – Принцип эквивалентности повторного использования и выпусков.

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

Классы и модули, объединяемые в компонент, должны выпускаться вместе.

CCP: Common Closure Principle – принцип согласованного изменения.
Это принцип SRP (single responsibility) перефразированный для компонентов.

В один компонент должны включаться классы, изменяющиеся по одним и тем же причинам и в одно и то же время. В разные компоненты должны включаться классы, изменяющиеся в разное время и по разным причинам.

CRP: Common Reuse Principle – принцип совместного повторного использования.
Этот принцип имеет схожесть с ISP (interface segregation).

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

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

«Возможно, вы уже заметили, что три принципа связности компонентов вступают в противоречие друг с другом. Принципы эквивалентности повторного использования (REP) и согласованного изменения (CCP) являются включительными: оба стремятся сделать компоненты как можно крупнее. Принцип повторного использования (CRP) – исключительный, стремящийся сделать компоненты как можно мельче. Задача хорошего архитектора – разрешить это противоречие».

image

«В прошлом мы смотрели на связность проще, чем предполагают принципы эквивалентности повторного использования (REP), согласованного изменения (CCP) и совместного повторного использования (CRP). Когда-то мы думали, что связность – это просто атрибут, что модуль выполняет одну и только одну функцию. Однако три принципа связности компонентов описывают намного более сложное многообразие. Выбирая классы для включения в компоненты, нужно учитывать противодействующие силы, связанные с удобством повторного использования и разработки. Поиск баланса этих сил, исходя из потребностей приложения, – непростая задача. Кроме того, баланс практически всегда постоянно смещается. То есть разбиение, считающееся удачным сегодня, может оказаться неудачным через год. Как следствие, состав компонентов почти наверняка будет изменяться с течением времени и смещением фокуса проекта с удобства разработки к удобству повторного использования».

Сочетаемость компонентов


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

ADP: Acyclic Dependencies Principle — принцип ацикличности зависимостей.
Циклы в графе зависимостей компонентов недопустимы!

image

Из какого бы компонента, отображенного на графе, вы не начали движение по стрелкам, указывающим направление зависимости, у вас не получится вернуться в первоначальную точку. Это ациклический ориентированный граф (DAG — Directed Acyclic Graph).

image

На этом изображении у нас появляется зависимость, приводящая к появлению цикла. Образовавшуюся циклическую зависимость всегда можно разорвать.

Способы разрыва циклических зависимостей


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

image

2. Введением нового компонента

image

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

Проектирование сверху вниз, а точнее его неосуществимость


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

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

Фактически диаграммы зависимостей компонентов слабо отражают функции приложения. В большей степени они являются отражением удобства сборки и сопровождения приложения. В этом главная причина, почему они не проектируются в начале разработки проекта. В этот период нет программного обеспечения, которое требуется собирать и сопровождать, поэтому нет необходимости составлять карту сборки и сопровождения. Но с появлением все большего числа модулей на ранних стадиях реализации и проектирования возрастает потребность управлять зависимостями… Кроме того, появляется желание максимально ограничить влияние изменений, поэтому мы начинаем обращать внимание на принципы единственной ответственности (SRP) и согласованного изменения (CCP) и объединять классы, которые наверняка будут изменяться вместе.

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

По мере развития приложения мы начинаем беспокоиться о создании элементов многократного пользования. На этом этапе на состав компонентов начинает влиять принцип совместного повторного использования (CRP). Наконец, с появлением циклов мы начинаем применять принцип ацикличности зависимостей (ADP), в результате начинает изменяться и разрастаться граф зависимостей компонентов.

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

Эй, ты еще здесь? Чел, это рил важно, с этим нужно хотя бы поздороваться, чтоб въехать в смысл следующей части, там будет сырой туториал по использованию тулзы.

SDP: Stable Dependencies Principle — принцип устойчивых зависимостей.
Зависимости должны быть направлены в сторону устойчивости!

«Что подразумевается под “устойчивостью”? Представьте себе монету, стоящую на ребре. Является ли такое ее положение устойчивым? Скорее всего, вы ответите “нет”. Однако если оградить ее от вибраций и дуновений ветра, она может оставаться в таком положении сколь угодно долго. То есть устойчивость напрямую не связана с частотой изменений. Монета не изменяется, но едва ли кто-то скажет, что, стоя на ребре, она находится в устойчивом положении.

В толковом словаре говорится, что устойчивость – это “способность сохранять свое состояние при внешних воздействиях”. Устойчивость связана с количеством работы, которую требуется проделать, чтобы изменить состояние. С одной стороны, монета, стоящая на ребре, находится в неустойчивом состоянии, потому что требуется приложить крошечное усилие, чтобы опрокинуть ее. С другой стороны, стол находится в очень устойчивом состоянии, потому что для его опрокидывания требуются намного более существенные усилия.

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

image

image

Как же оценить устойчивость компонента? Автор предлагает подсчитать количество его входящих и исходящих зависимостей и на их основе вычислить меру его устойчивости.

  • Fan-in (число входов): количество входящих зависимостей. Эта метрика определяет количество классов вне данного компонента, которые зависят от классов внутри компонента.
  • Fan-out (число выходов): количество исходящих зависимостей. Эта метрика определяет количество классов внутри данного компонента, зависящих от классов за его пределами.
  • I: неустойчивость: I = Fan-out ÷ (Fan-in + Fan-out). Значение этой метрики изменяется в диапазоне [0, 1]. I = 0 соответствует максимальной устойчивости компонента, I = 1 – максимальной неустойчивости.

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

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

Дружище, не засыпай, я рядом. Осталось совсем немного!

SAP: Stable Abstractions Principle — принцип устойчивости абстракций. Устойчивость компонента пропорциональна его абстрактности.

«Некоторые части программных систем должны меняться очень редко. Эти части представляют высокоуровневые архитектурные и другие важные решения. Никто не желает, чтобы такие решения были изменчивыми. Поэтому программное обеспечение, инкапсулирующее высокоуровневые правила, должно находиться в устойчивых компонентах (I = 0). Неустойчивые (I = 1) должны содержать только изменчивый код – код, который можно было бы легко и быстро изменить.

Но если высокоуровневые правила поместить в устойчивые компоненты, это усложнит изменение исходного кода, реализующего их. Это может сделать всю архитектуру негибкой. Как компонент с максимальной устойчивостью (I = 0) сделать гибким настолько, чтобы он сохранял устойчивость при изменениях? Ответ заключается в соблюдении принципа открытости/закрытости (OCP). Этот принцип говорит, что можно и нужно создавать классы, достаточно гибкие, чтобы их можно было наследовать (расширять) без изменения. Какие классы соответствуют этому принципу? Абстрактные.

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

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

Принципы устойчивости абстракций (SAP) и устойчивых зависимостей (SDP) вместе соответствуют принципу инверсии зависимостей (DIP) для компонентов. Это верно, потому что принцип SDP требует, чтобы зависимости были направлены в сторону устойчивости, а принцип SAP утверждает, что устойчивость подразумевает абстрактность. То есть зависимости должны быть направлены в сторону абстрактности.

Однако принцип DIP сформулирован для классов, и в случае с классами нет никаких полутонов. Класс либо абстрактный, либо нет. Принципы SDP и SAP действуют в отношении компонентов и допускают ситуацию, когда компонент частично абстрактный или частично устойчивый».

Как же измерить абстрактность компонента? Тут тоже всё очень просто.
Значение меры абстрактности компонента определяется отношением количества его интерфейсов и абстрактных классов к общему числу классов.

  • Nc: число классов в компоненте.
  • Na: число абстрактных классов и интерфейсов в компоненте.
  • A: абстрактность. A = Na ÷ Nc.

Значение метрики A изменяется в диапазоне от 0 до 1. 0 означает полное отсутствие абстрактных элементов в компоненте, а 1 означает, что компонент не содержит ничего, кроме абстрактных классов и интерфейсов.

Зависимость между I и A

Если отобразить на графике (Y — абстрактность, X — неустойчивость) “хорошие” компоненты обоих видов: абстрактные устойчивые и конкретные неустойчивые, получим вот что:

image

Но, т.к. компоненты имеют разные степени абстрактности и устойчивости, далеко не все попадут в эти 2 точки (0, 1) или (1, 0), и так как нельзя потребовать, чтобы все компоненты находились в них, мы должны предположить, что на графике A/I имеется некоторое множество точек, определяющих оптимальные позиции для компонентов.

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

image

Зона боли

Рассмотрим компоненты, располагающиеся вблизи точки (0, 0).

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

Правильно спроектированные компоненты обычно не должны находиться вблизи с точкой (0, 0).
Однако в действительности существуют программные сущности, попадающие в эту зону. К примеру схема базы данных, она максимально конкретна и к ней тянется множество зависимостей. Вот одна из причин, почему изменения схемы БД обычно связано с большой болью.

Зона бесполезности

Рассмотрим компоненты, располагающиеся вблизи точки (1, 1).

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

Как не попасть в зоны исключения?

Расположить компоненты поблизости с главной последовательностью, или на ней. Такие компоненты не “слишком абстрактны” для своей устойчивости и не “слишком неустойчивы” для своей абстрактности. Они не бесполезны и не доставляют особенной боли. От них зависят другие компоненты в меру их абстрактности, и сами они зависят от других в меру конкретности.
Самыми желательными позициями для компонентов являются конечные точки главной последовательности. Однако в больших системах всегда найдутся несколько компонентов недостаточно абстрактных и недостаточно устойчивых. Такие компоненты обладают великолепными характеристиками, когда располагаются на или вблизи главной последовательности.

Расстояние до главной последовательности

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

  • D: расстояние. D = |A+I–1|. Эта метрика принимает значения из диапазона [0, 1]. Значение 0 указывает, что компонент находится прямо на главной последовательности. Значение 1 сообщает, что компонент располагается на максимальном удалении от главной последовательности.

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

image

Заключение автора (Роберта Мартина)


«Метрики управления зависимостями, описанные в этой главе, помогают получить количественную оценку соответствия структуры зависимостей и абстракции дизайна тому, что я называю “хорошим” дизайном. Как показывает опыт, есть хорошие зависимости, есть плохие. Данная оценка отражает этот опыт. Однако метрики не являются истиной в последней инстанции; это всего лишь измерения на основе произвольного стандарта. Эти метрики несовершенны – в лучшем случае, но я надеюсь, что вы найдете их полезными».

Приятель, я рад если ты до сих пор со мной. На этом с первой частью пожалуй всё. Хоть и еще очень много тем, и развернутых мыслей автора остались не затронутыми. Всё-таки главная цель моей статьи заключается не в пересказе книги, иначе тебе однозначно приятней было бы прочитать оригинал. Я, походу, и так уже конкретно перегнул с объемом содержания, но падлой буду, старался не раздувать как мог. Раунд!

День второй. Эмм, т.е. Часть 2


Вступление


О чем я там тёр так долго? — А, ну да… Мы типа архитекторы, ну или очень хотим быть ими, верно?

Чё у нас имеется? — Какой-то жалкий проектик, а может монструозная система, ПРОЕКТИЩЕ, мать его, ну или ваще нифига нет еще, а мы только ладошки потираем, зырим в календарь и прикидываем, как знаатно ща придётся поговнокодить.

Нет никаких нас, я работаю один! А может быть мы с корешем напару, в гараже, собрались накодить новый Apple, почему-бы и нет? Ну или нас рать и все мы часть здоровенной компании, вкалываем изо всех, как домашние негры бедолаги (т.е. афроамериканцы конечно, ну ты понял), а может пинаем потихой, так чтобы только не уволили. За жирный оклад с премией, печеньками и видом из москва-сити или на голом энтузиазме, за идею и долю в стартапе, который естественно хрен когда взлетит.

На самом деле это неважно. Важно вот чё: все мы по итогу просто хотим, чтоб наше детище не загнулось преждевременно, ну хотя-бы раньше нашего ухода, а ведь на то есть все шансы. Да-да…

И чё теперь делать, спросишь ты? Так вот, если у тебя в конторе нет большого брата, тут братан без третьего глаза не обойтись.

Краткое содержание первой части


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

А — абстрактность компонента
I — неустойчивость компонента
D — расстояние до главной последовательности на графике A/I

Выявили эрогенные зоны зоны боли и бесполезности.

Пришли к выводу, что в идеале A должно увеличиваться и уменьшаться пропорционально уменьшению и соответственно увеличению I. Хорошо спроектированные компоненты должны находиться на или хотя-бы вблизи с главной последовательностью, т.е. D должно быть как можно ближе к нулю, а зависимости между компонентами должны быть ацикличны и направлены в сторону устойчивости.

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

Так что там про третий глаз-то? PHP Clean Architecture — инструмент появившийся в результате прочтения книги, спасибо дядюшке Бобу (Роберту Мартину) за ценные мысли, и бэшшаного желания всё это дело визуализировать, для удобства проведения анализа, и автоматизировать, для возможности включения различных проверок в CI/CD (на подобие PHPStan, Phan и др., только для выявления именно архитектурных проблем).

Инструмент и тестовый проект. Введение в тему


Фуф, с теорией вроде всё… Теперь можно и руки запачкать в деталях разобраться.
А для того, чтоб мне сейчас не пришлось про абстрактных коней в вакууме втирать, и у тебя к концу чтения было меньше оснований обозвать меня мудилой, из-за которого ты просрал потратил так много времени, мне пришлось просрать потратить немного своего, и подготовить демонстрационный проект, которому, собстна, мы с тобой сейчас и будем делать харакири. Для “супер одарённых” чуваков, которые несомненно будут и захотят похоливарить в коментах, по поводу его деталей, подчеркиваю — это ДЕМО-проект.
Накодил я его примерно за пол дня, грубо говоря, на коленке у подружки. Не могу ведь я взять и слить широкой публике детали боевых проектов, с которыми мне приходится работать… Пацаны на раёне не поймут, предъявят, а оно мне надо? Ну, а если ты простой хлопец, не из числа “особо выдающихся”, тогда сорян за такое долгое вступление, мне просто нужно было отвести риски и снизить потенциальную угрозу разведения в комментариях базаров ни о чём.

Так вот, сам проект лежит здесь github.com/Chetkov/php-clean-architecture-example-project.

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

Хотя всё не так плохо. В данный момент в нём есть: пользователь, продукт и заказ, а также сценарии: регистрация пользователя, смена пароля, добавление продукта и бронирование заказа.

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

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

Первоначальное состояние

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

Подключаем PHP Clean Architecture:
composer require v.chetkov/php-clean-architecture:0.0.3


Создаём и настраиваем конфигурационный файл

Визуализация для анализа


Выполняем в консоли (в корне проекта):

vendor/bin/phpca-build-reports
или
vendor/bin/phpca-build-reports /path/to/phpca-config.php

И в ответ получаем:

Report: /path/to/reports-dir/index.html

Отчет состоит из 3-х экранов:

1. Общая информация о системе

image

На этом экране отображается:

Тот самый A/I график, отображающий рассеянность компонентов:

image

График, отображающий удалённость компонентов от главной последовательности:

image

И график, отображающий взаимосвязи между компонентами:

image

Стрелки указывают направление зависимостей, а цифры на них — силу связи.

Т.к. современные IDE предоставляют лёгкий способ рефакторинга (сами находят и исправляют все участки, где явно вызывается метод или класс, допустим при переименовании или переносе в другой namespace), за силу связи я принял количество элементов модуля зависимости, используемых зависимым модулем.

К примеру: элементы модуля services знают про 12 элементов модуля model, а элементы infrastructure знают про 1 элемент services. И т.д.

Кстати, а что за *undefined*? Бро, вполне ожидаемый вопрос. Это модуль, создаваемый автоматически для всех элементов, которые в процессе анализа системы не удалось отнести к какому-либо из известных (т.е. перечисленных в конфигурационном файле).

Если-же ты поправишь конфиг таким образом:

image

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

Кстати, в vendor_path необходимо указать путь к директории vendor, а в excluded пути к директориям пакетов, которые мы хотим исключить (к примеру если мы какой-то пакет описываем в основном конфиге модулей для сканирования).

После включения vendor_based_modules граф изменился, хотя конфиг самих модулей мы не меняли

image

2. Детальная информация о компоненте

image

Здесь мы также видим:

  • Граф взаимосвязей, но уже не всей системы, а только участвующих компонентов
  • A/I график, на котором отражен только текущий компонент
  • Основные характеристики модуля

    image

а также
  • График исходящих зависимостей, отображающий количество и список файлов в текущем модуле, зависящих от файлов других модулей.
  • image

  • И график входящих зависимостей, отображающий количество и список файлов в других модулях, зависящих от текущего модуля.

    image

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

3. Детальная информация об элементе компонента

image

Здесь видно:

  • Основные характеристики элемента, такие как: тип, абстрактность, неустойчивость, примитивность, а также к какому компоненту принадлежит и модификатор доступа.

“Чё, модификатор доступа у класса в PHP?” — говоришь ты, просверливая пальцем дырку в районе виска, так ведь? Не спеши, я знаю что в пыхе нет никаких приватных классов, но круто ведь было бы, если бы они были? В общем, об этом тоже чуть позже.

  • Граф зависимостей, на котором отражено взаимодействие рассматриваемого элемента с другими элементами системы

  • И таблички с исходящими и входящими зависимостями

    image

С описанием экранов закончили, го конфиги ковырять и зырить, как это можно настраивать.

Варианты конфигурации


Ограничение максимально-допустимого расстояния до главной последовательности (для всех компонентов)

Для начала давай попробуем установить всем компонентам максимально допустимое расстояние до главной последовательности. Глупо будет думать, что все они лягут на главную диагональ, мир ведь не идеален, поэтому давай будем реалистами. Делаю 0.3

image

Перезапускаем команду генерации отчета и сразу на главной странице видим нарушение. Компонент model превышает допустимое расстояние на 0.192

image

Ограничение максимально-допустимого расстояние до главной последовательности (конкретному компоненту)

Замечательно, но давай предположим, что именно компоненту model можно нарушать это ограничение. Разное бывает… Мы ведь все зачастую уже имеем не идеальные системы, с которыми приходится работать, поддерживать их и улучшать. Любому понятно, что быстро сократить это расстояние ни у кого не получится, даже наверное у боженьки (надеюсь он меня простит).

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

Снова идём в конфиг, и устанавливаем max_allowable_distance, только уже для конкретного модуля. Фиксирую на его текущем значении 0.492

image

Збс, теперь снова всё норм.

image

Но если, какой-то с**ка Вася, снова чё-то запушит и станет хуже — руки ему отрубить! CI/CD включающий запуск команды phpca-check (о ней поговорим позже) отругает его, возможно даже матом, ну и естественно подсветит ошибки.

Контроль зависимостей компонентов

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

Как думаешь, может-ли самая центральная часть системы, так скажем domain, ну чтоб прям ваще по DDDовски, что-то знать про какую-то вонючую инфраструктуру?
Конечно же ДА, у некоторых это, прям, атрибут успеха (ну, я про тех кто HTML вперемешку с бизнес-логикой рендерит). Но ваще, как бэ, не должен… Я конечно хз, но всякие-разные умные дядьки в книжках поговаривают, что это не по пацански.

Тогда давай замутим так… Всё там же, в конфиге ограничений для model.

image

Хм… Пока всё норм, т.к. model про infrastructure действительно ничего и не знает…

Ну давай тогда приколемся… Тупость конечно редкостная, но ничего лучше я не придумал. Крч, в момент создания пользователя будем «слать» смс, а в момент создания заказа «отправлять» email(ы). Причем делать это через конкретные реализации уведомителей, которые лежат в infrastructure.

github.com/Chetkov/php-clean-architecture-example-project/commit/6bad365176b30999035967319a9c07fae69fabf9

Теперь зырим в отчет… Как мы видим, графы на разных страницах отчета, подсвечивают эту связь как нелегитимную.

Главная.

image

Отчет по компоненту.

image

На графиках “исходящих зависимостей” model и “входящих зависимостей” infrastructure сразу видим элементы, нарушающие введенное правило

image

В таблицах “исходящих зависимостей” элементов model и “входящих зависимостей” элементов infrastructure, аналогично.

PHPCAEP\Model\User\User

image

PHPCAEP\Infrastructure\Notification\Sms\SmsNotifier

image

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

image

Аналогичные действия можно проделать и с конфигом allowed_dependencies.

image

Результат будет полностью противоположный.

Можно компоновать конфиг указывая для модуля и forbidden, и allowed (если будет желание, подробнее обсудим в коментах). Указывать один компонент и в forbidden, и в allowed пока нельзя… Были мысли ввести конфиг для указания приоритета, но пока этого не сделал и не уверен что буду делать. Всё зависит в том числе и от твоей обратной связи. Может вообще впустую это всё?

Public/Private Class в PHP

Окей, научились значит круг знаний компонентам сужать и контролировать. Уже не плохо. Чё дальше?

А дальше еще одна проблема. Враг то не дремлет, хотя в большинстве случаев этот враг, это мы сами и есть, или наши кореша по офису.

Бывает ведь такое, ты с девочкой познакомился, пообщался, узнал про неё что-то, поухаживал даже может быть, но вот блин, не складывается у вас ничего. Мы друзья и всё такое… Она может чмокнуть тебя при встрече, может выслушать и поддержать добрым словом, но не даёт хоть убей ничего большего. Так о чем это я… А, вот… Всё дело в доступности.

Зато в коде, постоянно… Подключишь новый пакет, дня не прошло, а его уже используют, да прям во все места со всех сторон. На разные его классы уже подвязана половина твоей кодовой базы. Короче очень интимная у него связь с остальными получается. Может в каких-то ситуациях это и норма, но далеко не во всех, верно?

Ты: “Всё, хорош, растрынделся. Как решать проблему?”
Я: “Оставлять только дозволенные для взаимодействия классы, а к остальным ограничивать доступ.”
Ты: “Но в PHP ведь нет модификаторов доступа у классов.”
Я: “Согласен, зато в PHP Clean Architecture есть.”

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

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

Давай предположим, что в model, по какой-то причине, только 2 класса, о которых другим разрешено знать… User и Order, все остальные должны быть скрыты.

image

Что мы имеем теперь?

Граф на главной.

image

Сразу видно, какие компоненты требуют пересмотра.

Лезем смотреть, что не так с infrastructure.

image

На графике “исходящих зависимостей” видим элементы, нарушающие правило:

image

На странице информации об элементе, аналогично.

image

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

image

К примеру запрещаем всем знать про Fio.

Теперь на главной:

image

На странице информации о компоненте services:

image

И на странице информации об элементе PHPCAEP\Model\User\Fio:

image

Контроль нарушений принципов ADP и SDP

Еще, пожалуй, нужно подсветить возможность включения проверок:

  • на наличие нарушений принципа ADP, т.е. на существование циклических зависимостей в графе (check_acyclic_dependencies_principle)
  • и на наличие нарушений принципа SDP, т.е. на наличие зависимостей направленных из более устойчивых модулей в менее устойчивые (check_stable_dependencies_principle)

image

Но это пока работает только в phpca-check и в отчете не отображается. В дальнейшем думаю поправлю.

Автоматический контроль в CI/CD


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

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

Запускаем в консоли (в корне проекта):

vendor/bin/phpca-check

или

vendor/bin/phpca-check /path/to/phpca-config.php

Предполагается, что запуск этой команды будет добавлен в процесс CI/CD твоего проекта. В таком случае ошибка будет блокировать дальнейшее выполнение сборки.

Скрипт завершает работу с ошибкой, и вываливает что-то подобное:

image

Зуб даю, в реальном проекте список будет куда длиннее.

Разбор полётов

  1. Нарушение принципа ацикличности зависимостей (ADP)
  2. Аналогично, но цикл чуть короче
  3. 2 элемента model (перечислены) зависят от infrastructure, что не позволено конфигом
  4. Снова нарушение ADP
  5. Еще один цикл и нарушение ADP
  6. Нарушение SDP, т.к. более устойчивый model зависит от менее устойчивого infrastructure
  7. Опять циклическая зависимость, будь она неладна
  8. 1 элемент services (перечислен) зависит от непубличного Fio

Примерно такой результат мы имеем благодаря нашим стараниям и усердному говнокодингу.

Теперь давай вертать всё в зад. Ну, т.е. не совсем всё, конфиги оставляю естественно. Правлю только код.

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

Устранение проблем

Убрал ранее выдуманную зависимость.

github.com/Chetkov/php-clean-architecture-example-project/commit/f6bbbf713ebb4a22dfb6061f347ef007ba3b422f

Перезапускаю vendor/bin/phpca-check

image

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

image

Снова vendor/bin/phpca-check

image

И CI/CD поехал дальше!

На этом всё.

Послесловие


Лови пятюню, если дочитал до конца, если нет, чухай ногу об дорогу (всё-равно ты этого не увидел).

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

Еще раз прошу прощения, если что-то было не так.

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

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

Чистого тебе кода и гибкой архитектуры!
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 23

    +2
    Возможно кому-то моя манера изложения могла показаться чересчур вальяжной...

    Именно

      +1
      Возможно кому-то моя манера изложения могла показаться чересчур вальяжной...

      Все норм братюнь, держи пятюнь.
        0

        Вопрос про знания:
        Есть метод User::getFio(): Fio. Если Fio где-то внесён в приватные, но там используется этот метод без явного упоминания Fio, вычислить его система? А если явно тип возврата не определён, а есть аннотации? А если и их нет, сможет вывести тип?


        А так очень интересный пакет, надо поиграться на досуге.

          +1
          А если явно тип возврата не определён, а есть аннотации?

          Пока из анотаций умеет разбирать только
          /** @var ... */
          , в ближайшее время как раз планировал проработать моменты, касающиеся других анотаций, на случай отсутствия явно указанных типов в коде. Думаю в ближайших релизах это будет доработано.

          А если и их нет, сможет вывести тип?

          Нет, так пока не умеет

          Если Fio где-то внесён в приватные, но там используется этот метод без явного упоминания Fio, вычислить его система?

          Такого тоже не умеет, возможно со временем
            +1

            Как вариант, можно попробовать что-то вытащить через расширение php-ast
            https://github.com/nikic/php-ast

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

          Наверное "метрика I компонента должна быть меньше ..."?

            0
            Да, спасибо за замечание, сейчас исправлю
            0

            Вторая часть вкусная. Обязательно посмотрю техническую реализацию. Мы для Yii 3 активно используем https://pdepend.org/ и https://github.com/clue/graph-composer, но устали руками это всё делать и хотим запилить прямо в наших инструментах для разработки https://github.com/yiisoft/yii-dev-tool

              0
              Спасибо, если честно, не знаком с этими инструментами. Постараюсь посмотреть на досуге, возможно получится почерпнуть какие-то идеи)
              –2
              Хорошая книга, да. Рекомендую вместе с «Чистой Архитектурой» от Роберта Мартина еще книгу о DDD от Эрика Эванса почитать.
                0
                Посмотрел код в репозитории. Старая добрая схема Entities, Services, Repositories. Как деды завещали. Хороший код. Хорошая статья. Держите + в карму. Заслужили.
                  0

                  Спасибо! Ну да, рабочая схема, норм для начала, наверное, большинства проектов, но в какой то момент, с развитием и ростом системы, она может стать трудно поддерживаемой. Тогда уже необходимо ее пересматривать, делать реструктуризацию, выделять новые компоненты и т.д. Об этом, кстати, тоже говорится в обсуждаемой книге. Хотя, у большого числа проектов этот момент может и не наступить)

                    +1

                    А чем хороший, что все размазано? В одной папке Entity совершенно чужие друг другу классы, ровно как и в других...


                    Почему рядом с User будет находиться Product, а не UserRepository… Где тут логика?

                      +1

                      Оговорюсь, я имел ввиду не совсем Entities, Services, Repositories, а Model (в которой будут entities, value objects и т.д.), Services (слой логики приложения) и Infrastructure (вспомогательные механизмы: конкретные реализации всяких репозиториев, нотификаторов и т.д.)
                      А также, не имел в виду сваливать все подряд классы в эти 3 папки, естественно можно сделать в каждой из них структуру и каким-то образом группировать логически близкие друг к другу элементы.


                      Хороша эта схема тем, что она проста. Для декомпозиции системы на большое число правильных компонентов, однозначно, потребуется больше времени и усилий. А вероятность того, что в новом проекте, в котором кода написано с гулькин нос, получится с первого раза сделать правильное разделение на компоненты, очень не велика. При этом, далеко не все проекты дорастают до размера, когда в декомпозиции на большее число меньших по размеру компонентов появляется реальная необходимость. Т.о. можно начать с малого и усложнять систему постепенно, по мере роста, когда это становится нужно.

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

                    Вот одна из них:
                    Допустим есть компоненты A и B. В целом мы не против, чтоб А использовал В, но по ряду причин хотим ограничить эту связь, т.е. сделать её допустимой ТОЛЬКО для конкретной группы классов (на пример, чтоб зависимость не разрасталась очень сильно, мы реализуем в компоненте A что-то типа ACL (anti-corruption layer), только в миниатюре :) и для него разрешаем зависимость от В, остальные элементы компонента A должны взаимодействовать с B ТОЛЬКО через него, т.е. не на прямую.

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

                    Идея: доработать в конфиге компонента секции allowed_dependencies/forbidden_dependencies, чтоб помимо разрешенных/запрещенных компонентов, в них можно было перечислить классы или директории, в которых располагаются классы, которым можно/нельзя знать про задаваемый в конфиге сторонний компонент.

                    Что думаете по этому поводу?
                      0

                      А в чём минус решения для ввода в B класса фасада или нескольких, к которым только и разрешить доступ "извне"?

                        0
                        К примеру B это какой-то вендор… (ну т.е. пакет стороннего разработчика, мы его не поддерживаем). В таком случае этот вариант не реализуем.
                          0
                          Хотя нет, реализуем, но это будет выглядеть как костыль…
                          Разместить фасад где-то в нашем коде, но в конфиге, директорию в которой мы его разместили указать как второй корневой каталог компонента B.
                          Работать будет, но тоже добавляет нюансы в конфигурацию. Т.к. без этого костыля, мы вообще могли-бы для стороннего пакета В секцию в modules не описывать, а просто включить vendor_based_modules. А так нужно будет его отдельно описывать в modules + исключать в vendor_based_modules. Не то, чтоб это очень сложно, но как-то вроде не очень :)
                          +1

                          Технически это можно реализовать через friendly classes: https://3v4l.org/k1e2l

                            0

                            Как по мне, то для системных вещей типа либ или фреймворков пойдёт, но вот в "юзерспейсе" своём я бы такое видеть не хотел.

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

                          «Ограничение максмально-допустимого расстояния до главной последовательности (для всех компонентов)», можно «максмально» исправить на максИмально?
                            0
                            Спасибо! Ошибку подправил)

                          Only users with full accounts can post comments. Log in, please.