company_banner

Теория программирования: пакетные принципы и метрики



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

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


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



    Наши проблемы идут от мозга и от того, как устроена наша память. В отличие от хорошего хранилища долговременной памяти, кратковременная устроена по типу 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 июня. Первые доклады уже приняты в программу!

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

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

    Похожие публикации

    Комментарии 11

      0
      Sorry, может вне темы статьи.
      Всё так же «плохо» с переводом Yii2 cookbook на русский язык?
      Как работать над переводом на русский

      P.S. Отсюда Переводим Yii 2.0 на русский
        0

        Переводом на русский сейчас никто не занимается. Ранее я это делал, но потом начал больше заниматься Yii 2, позже Yii 3 и времени на сам перевод не осталось. То есть перевод нормальный, но, вероятно, несколько устарел по-сравнению с оригиналом.

          +1
          Хороший повод подтянуть английский чтобы читать cookbook не ожидая перевода
          +1
          скобки потерялись в формуле расчета абстрактности
            0

            Точно! Поправлю картинку.

            0
            Как-то все очень сложно описано. Примерно понимаю о чем написано, но человеку, который пока пишет код на уровне Yii2 эта статья, как мне кажется, никак не поможет научиться писать код лучше.

            А что поможет? Мне сильно помог такой вариант:

            1. Берем, и начинаем писать в свое свободное время какой-то большой проект-монолит. В свободное время — чтобы вас никто не подгонял со сроками, монолит — потому что только в монолите с сотнями моделей и сложной логикой хорошее ООП становится обязательным, а не желаемым. Что-то небольшое (микросервис) можно написать тяп-ляп и готово. Разработка большого монолита по принципам тяп-ляп быстро приводит к тому, что развивать его становится невозможно (думаю все с таким сталкивались на рабочих проектах)

            2. Пишем проект как вам хочется. Это очень важно — писать не так, как кто-то в интернете написал, или в книжке, а как вам хочется. Хочется без SOLID и тестов — пишите. Здесь важно именно наступить на грабли и получить по лбу в тех моментах, в которых вы по лбу еще не получали. Чтобы прийти к тому же SOLID или тестам не по тому, что об этом Вася в интернете написал, а потому что вы сами, на личном опыте, к этому пришли.

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

            4. В процессе переписывания вы на личном опыте доходите до таких вещей, которые умные дяди пишут в умных книжках — например, хорошо бы какой-то функционал выносить в полностью независимый модуль (привет DDD), чтобы в следующем переписывании можно было просто взять его, и скопировать в новый проект (в новое «переписывание с нуля»). В этом же процессе вы обязательно придете к авто-тестам, и не только к ним, а ощутите те приятные моменты, когда возвращаясь к проекту через пол года, уже забыв что там как работает, и работает ли вообще, вы выполняете тесты, они проходят, и вы себе говорите «о, все работает, супер».

            5. И так далее — новый функционал, новые переписывания, новый опыт — процесс бесконечный.

            Конечно, чтобы годами писать какой-то домашний сложный проект нужно любить программирование. Ну и быть без детей, наверное :) Так что кто еще молодой и без семьи — ловите момент.
              0

              Можно и так, но долго.

              0
              Как правильно формировать пакеты

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

              Потому что использовать интерфейсы вида:
              $response = $object->handle($request) // handle(RequestInterface $request): ResponseInterface

              Сильно удобнее, чем что-нибудь вроде:

              $object->setParam1(...)
              $object->setParam2(...)
              $object->setParam3(...)
              $result = $objcet->getResult()

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

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

              Еще раз убеждаюсь, как же сложно написано. Моя аналогия, которая, мне кажется, намного понятнее:

              • Ситуация №1: Неопытный программист пишет код как получается: получаем лапша-код, объекты, которые не объекты, а просто классы по которым как-то распиханы данные и методы для работы с этими данными. Его код плохой, но понятно как его рефакторить (потому что все когда-то так писали, и переписывали свой код)
              • Ситуация №2: Неопытный программист начитался умных книжек про паттерны, DDD и прочее, и начинает внедрять их на рабочем проекте. В результате его код получается сильно хуже, и намного более переусложненным, чем в ситуации с первым программистом.


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

              P.S. И как хорошо где-то сказано: задача тимлида не в том, чтобы говнокода в проекте не было, а в том, чтобы этот говнокод не растекался — т.е. создать такие условия, чтобы говнокод каждого конкретного рядового программиста был как-то изолирован в себе (через вынесение в микросервис, или в модуль с отдельным неймспейсом, с указанным тимлидом интерфейсом использования, или как-то по другому — уже не суть)
                0
                1. Не совсем понял, при чём тут front controller.
                2. Ваша аналогия неплохая, но она не про количество слоёв. Я хотел указать именно на это.
                0
                «Classes that change together are packaged together — изменение в пакете должно затрагивать весь пакет»

                Что считается изменением?
                Фикс бага в одном классе:
                — выносим класс в отдельный пакет?
                — вносим незначимые правки в остальные классы?
                — третий вариант?

                Не очень корректно, на мой взгляд, расшифрована идея
                  0

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


                  На самом деле SOLID и пакетные принципы довольно расплывчаты на тему определений и их трактования. На данный момент я думаю что понял их правильно, но если нет — поправьте.

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

                Самое читаемое