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

Что есть абстракция?


Это обобщение существенного и удаление несущественного, так как мир настолько сложен, что запрограммировать удаётся только его существенные части. Если попытаемся запрограммировать всё — мы потонем, поэтому абстракция помогает нашему мозгу «впихнуть невпихуемое», как это умеют делают военные (а программисты — пока нет):



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

  • Есть входы и выходы;
  • TTL объекта около 20 секунд;
  • Удерживается очень мало объектов. Раньше считали, что оперативно человек оперирует 7±2 объектами (George Miller, 1989). Потом поняли, что это число еще меньше: 4±1 объект (Cowan, 2001) и вообще зависит от объектов.
  • Если мы что-то хотим держать в памяти дольше, то нам нужно сконцентрироваться и использовать повторение:



Еще мы используем Chunking (группировку) всякий раз, когда важно запомнить что-то большое. Например, чтобы запомнить число 88003334434, мы разделим его на группы по типу телефонного номера: 8-800-333-44-34. Для нашего мозга получится 5 объектов, которые запомнить легче, чем пытаться удержать число целиком или отдельно каждую его часть.

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

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

Но как построить абстракцию, не сделав хуже?


Существует два понятия: cohesion (связность) и coupling (связанность). Они относятся в первую очередь к классам, но в целом и ко всем остальным сущностям. Разницу мало кто видит, так как звучат они почти одинаково.

И, хотя оба означают связь, coupling понимают в негативном ключе. Один объект завязан на другой в плохом смысле, если, ломая один из объектов, ломается всё остальное по цепочке. Cohesion несёт в себе позитивную ноту. Это группировка, в которой то, что близко по смыслу — лежит в одном месте и взаимодействует примерно с теми же местами, опять же близкими по смыслу.

Для того, чтобы понять, coupling у вас или cohesion, существуют проверочные правила. Их сформулировал инженер и специалист в области информатики Роберт Мартин еще в 2000 году, и это — принципы SOLID:

  • SRP;
  • OCP;
  • LSP;
  • ISP;
  • DIP.

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

Что есть пакет?


Пакет — это группа единиц кода. Причем, пакеты – это не обязательно пакеты Maven или Composer, или npm. Это программные модули, — то, что вы выделяете в namespaces или иным способом группируете.

Обычно имеются в виду классы, но это могут быть и библиотеки, и модули (не те, которые в фреймфворках, а которые изначально описывали в объектно-ориентированном программировании, то есть группы относящихся друг к другу классов в отдельном namespace). И даже микросервисы можно назвать пакетами — если это настоящие микросервисы, а не те макро-, на которые частенько распиливают монолиты.

И того, кто эти пакеты пилит, обычно волнуют два вопроса: Как правильно формировать пакеты и как работать с зависимостями пакетов?

Как их правильно формировать?


Собственно, cohesion и coupling, как основополагающие принципы, отлично работают и для пакетов. Но сработает ли SOLID для пакетов?

Да, но не совсем. Оказалось, что существуют ещё 6 принципов от того же Роберта Мартина, сформулированные в том же году. Часть из них относится к cohesion, и это о том, как дизайнить код: REP, CCP, CRP. Другая часть — это coupling (то есть использование пакетов): ADP, SDP, SAP — и это о том, как сделать так, чтобы один пакет не завязался на другой и чтобы всё нормально работало:



1 принцип – REP (Reuse-Release Equivalency Principle)


На сегодняшний день этот принцип выглядит до смешного очевидным, но не забываем, что сформулирован он в 2000 году, когда не было таких замечательных штук, как Maven, Composer и прочих, а пакетные релизы были не частыми.

Принцип гласит: «The granule of reuse is the granule of release. Only components that are released through a tracking system can effectively be reused. This granule is the package. — что переиспользуем, то и релизим. Эффективно переиспользовать можно только компоненты, релизнутые через системы версионирования. Такие компоненты называются пакетами». То есть упаковывайте то, что переиспользуется в отдельные пакеты и релизьте это через любимый пакетный менеджер, версионируя по SemVer.

2 принцип – CCP (Common Closure Principle)


«Classes that change together are packaged together — изменение в пакете должно затрагивать весь пакет». Этот принцип очень похож на SOLID-ский OCP. Классы, которые изменяются по одной и той же причине, должны упаковываться в один пакет. Что логично.

Нормальный пример: адаптеры. Библиотека, допустим, кеш. Если мы запихиваем в один пакет тучу адаптеров: для файлов, memcached, Redis, то при попытке изменить один какой-то адаптер мы нарушаем два принципа. Во-первых, принцип REP (начинаем релизить один из адаптеров, а релизить приходится все). А во-вторых — принцип CCP. Это когда классы для адаптера под Redis изменяются, а все остальные адаптеры в пакете —нет.

3 принцип – CRP (Common Reuse Principle)


«Classes that are used together are packaged together — Пакеты должны быть сфокусированными. Использоваться должно всё». То есть классы, которые используются вместе — упаковываем вместе. Проверочное правило здесь такое: смотрим, используется ли в нашем софте всё из того пакета, который к нему подключен. Если используется чуть-чуть, значит, скорее всего, пакет спроектирован неверно.

Эти три принципа дают понимание, как пакеты дизайнить. И казалось бы, нормально делай — нормально будет. Однако реальность сурова, и я сейчас объясню — почему. Вспомним треугольник от Артемия Лебедева, который вершины «быстро», «дёшево» и «качественно» обозначил несколько другими словами. Такой же треугольник нарисовали и для пакетных принципов в Институте Макса Планка:



Получается, эти принципы конфликтуют, и в зависимости от того, какие стороны треугольника мы выбираем, вылезают соответствующие косяки:

  • Если мы группируем для пользователей (REP) и для мейнтенера (CCP), то получаем множество ненужных релизов: новые версии пакетов начинают вылетать как из пулемета. И пакет как тот же Chromе достигает версии 46 за полгода, когда все остальные браузеры выпускают одну мажорную версию раз в 7 лет.
  • Если мы группируем для пользователей (REP) и выделяем классы в пакеты по признаку переиспользования (CRP), у нас получаются изменения в туче пакетов. А это неудобно мейнтенеру, потому что приходится лезть в каждый из пакетов, и не получается релизить их по отдельности. Это дикая головная боль.
  • Если мы группируем для мейнтенера, то есть соблюдаем CCP и CRP, то получается всё круто для человека, который поддерживает этот пакет, но не круто для юзера, потому что переиспользовать такие пакеты получается плохо: они выходят как всякие мелкие штучки, которые собрать вместе просто нереально.

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

Теперь переходим к принципам использования.

4 принцип – ADP (Acyclic Dependencies Principle)


«The dependency graph of packages must have no cycles — Если есть циклические зависимости, то проблема вызывает лавину». Если есть циклы, то есть зависимость пакета зависит от самого пакета прямо или косвенно, то косяк в одном пакете вызывает лавину во всех остальных пакетах, и ломается абсолютно всё. К тому же, такие пакеты очень тяжело релизить.

Поэтому надо проверять, есть ли циклы. Для этого необходимо строить направленный граф зависимостей и смотреть на него. Руками это делать не очень удобно, поэтому для PHP есть библиотека clue/graph-composer, которой скармливаешь пакет, и она строит гигантский граф со всеми зависимостями. Смотреть на это, конечно, невозможно, поэтому надо зайти в PR#45, зачекаутить его и выбрать возможность исключать зависимости, которые не интересны. Допустим, если вы пишите фреймворк, то вам скорее всего интересны зависимости на свои пакеты, а чужие — не так сильно, ведь свои косяки поправить можем, чужие — тяжелее. И получается вот такой граф:



Если мы видим — как здесь — что циклических зависимостей нет, то всё отлично. Если есть, надо исправлять. Чем меньше зависимостей, тем проще.

Как разорвать цикл?


  1. DIP — использовать инверсию зависимостей через интерфейсы. Мы должны ввести интерфейс и на него завязаться вместо зависимости на конкретные реализации.
  2. CRP — выделить общий пакет. Например, есть кеш и с адаптерами. Чтобы развязать между собою Redis, базу и так далее — выделяем драйверы в отдельные пакеты и выделяем сам общий пакет, в котором лежит только сам интерфейс. Это выглядит ужасно — получается такой «бесполезный» пакет. Но с точки зрения DIP и CRP это будет правильно. И помимо того, что реально не будет ломаться, еще и даст нам крутой профит — мы можем писать под этот пакет свои реализации.
  3. Переделать...

5 принцип – SDP (Stable Dependencies Principle)


Это принцип стабильных зависимостей: «Depend in the direction of stability — Не получится строить стабильное на нестабильном». Нестабильность считается так:



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

6 принцип – SAP (Stable Abstraction Principle)


Принцип стабильных абстракций гласит «A package abstractness should increase with stability — Стабильные пакеты абстрактны / Гибкие конкретны». То есть абстрактность должна возрастать со стабильностью. Стабильность здесь — то, как часто нам приходится менять части пакета: классы, интерфейсы, или что-либо ещё. Абстрактные пакеты должны быть стабильны, чтобы безболезненно на них завязываться. В примере с тем же кэшем пакет с интерфейсом будем сверхстабильным, потому что менять интерфейс, про который мы договорились и хорошо над ним подумали — скорее всего, не придётся. Если мы, конечно, абстрагируем не СУБД.

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

Можно ли измерить абстрактность?


Конечно. Абстрактность — это число абстрактных классов и интерфейсов в пакете, деленное на общее число классов и интерфейсов в этом самом пакете:


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

  • Если стабильно, но не абстрактно — это подозрительно.
  • Если дико нестабильное и очень абстрактное — значит кто-то играется с интерфейсами и делает нашу жизнь адом.
  • Но иногда бывает 0,0 — когда супер-неабстрактно и супер-стабильно, как в случае с хелперами или стандартными библиотеками PHP типа stdlib, strings, arrays — и это будет нормально.



Линия посередине называется главной линией, и если классы и интерфейсы попадают на неё или выстраиваются вдоль — это тот случай, когда всё отлично. По сути, D-метрика — это дистанция от главной линии, поэтому 0 в этом случае — это хорошо, а 1 — плохо. Но, как правило, ни то, ни другое не случается — значения плавают в диапазоне от 0 до 0,9-0,7. Считается метрика так:


Для PHP есть 2 инструмента для того, чтобы посмотреть метрику своих пакетов:

  • PHP_Depend;
  • PhpMetrics.

Конечно, если вы захотите ими прогнать корневой пакет со своим приложением, вы увидите не 5 точек, как на схеме — у вас вся схема будет в точках. Но из этой массы у вас будут выпрыгивать «рыжие» точки, при наведении на которые будет понятно, что это за классы — вот с ними, скорее всего, и будут некоторые проблемы.

Как и SOLID, все эти дополнительные принципы и метрики — не догма, но могут быть весьма полезными.

Резюме


Правильное проектирование пакетов вызывает взрывное дробление. Выглядит это страшно. Когда мы начали разрабатывать фреймворк Yii3 и выделять пакеты, у нас через некоторое время количество пакетов с 20 штук перевалило за сотню. Ппоначалу подобная ситуация настороживает, но позже становится понятно что всё, тем не менее, работает стабильно. Роберт Мартин был прав, и его принципы работают.

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

Данные принципы же позволяют не скатываться в монолит или в left-pad из npm. С left-pad была в свое время история — его создали для добавления символа в конце строки, так как в JavaScript есть традиция дробить пакеты вообще в пыль. А потом на этот пакет завязалось практически всё — вплоть до пакетных менеджеров и самых крутых фреймворков. В какой-то момент автор обиделся на всех и выпилил left-pad из системы — после чего, как вы понимаете, сломалось всё. Рассмотренные принципы, в том числе, позволяют уменьшить вероятность такого сценария.
Единственная конференция по PHP в России PHP Russia 2021 пройдет в Москве 28 июня. Первые доклады уже приняты в программу!

Купить билеты можно тут.

Хотите получить материалы с предыдущей конференции? Подписывайтесь на нашу рассылку.