Добра!
Моя прошлая статья по модульной структуре имела некоторый резонанс - были как противники так и сторонники этой методики. Как и в прошлый раз, я подчеркну, что не считаю этот путь единственно верным, и не буду пытаться завлечь кого-то на "темную сторону". Вместо этого, я расскажу вам, как я "живу" с модульной структурой, и как решаю некоторые проблемы.
Прежде всего, хотелось бы вернуться к тому, как устроены модули (они же домены, или области ответственности). В прошлый раз, я говорил о том, что можно просто удалить любой модуль и приложение продолжит свою работу. В действительности, это не совсем так. Дело в том, что модули очень похожи на пакеты composer (собственно ими они и могут являться). Что это значит? Это значит, что модули, подобно пакетам, имеют зависимости. Например, модуль Blog
может иметь в зависимостях модуль User
- ведь у блога должен быть автор. Модуль Image
(который отвечает за работу с изображениями) может иметь в зависимостях модуль Storage
(который отвечает за работу с файлами). И так далее...
Таким образом, мы приходим к такой структуре, когда мы имеем не плоский каталог модулей, а дерево зависимостей, хотя сами модули продолжают лежать в каталоге на одном уровне, как и в уже упомянутом composer.
После множества съеденных собак раздумий над тем, как устанавливать и удалять модули, я пришел к выводу, что инсталляторы в чистом виде (которые я изначально использовал) - это зло. Потому, как они не вписываются в поток миграций - а значит невозможно поддерживать актуальную структуру данных в различных средах, при коллективной разработке. Теперь, вместо инсталляторов, я использую инсталляторы миграций. При использовании команды artisan some-modlue:install
модуль копирует из папки своих стабов миграцию в основной поток и устанавливает на ней текущую дату. А при команде artisan some-module:uninstall
, копируется "реверсивная" миграция, которая делает обратное действие. Однако, не все так просто...
Дело в том, что простая инсталляция модуля может привести к чему нибудь не очень хорошему, если зависимости для установки модуля не разрешены (то есть один из модулей, от которых зависит текущий, еще не установлен). Таким образом, команда artisan some-module:install
должна приводить к запуску установки всех зависимостей. Установщики зависимостей, в свою очередь должны выполнить проверку, и если они уже установлены, то проигнорировать эту команду. А во время выполнения команды artisan some-module:uninstall
все зависимые модули так же должны быть удалены. Как же разрешить эту проблему? Если в первом варианте все понятно - модуль сам знает, от чего он зависит, и перечислить зависимости, которые необходимо установить не составит труда, то во втором варианте модуль понятия не имеет о зависящих от него других модулях. И здесь нам на помощь спешит система событий...
Многие путают события и команды. Я не буду гадать имеет ли читатель представление о различиях, и просто опишу суть.
А сводится все к тому, что события вызываются в одном месте, а обработаны могут быть во многих. В то время, как команды наоборот -обрабатываются в одном месте, а вызваны могут быть из многих.
Вернемся к нашим модулям. При инсталляции любой модуль, даёт команду всем своим зависимостям установиться, все его зависимости перед установкой так же вызывают эти команды для своих зависимостей. Это связано с тем, что модуль знает все свои зависимости, и знает о существовании этих команд. А вот при удалении модуль не знает о том, какие модули от него зависят, и модуль поджигает событие я_собираюсь_удалиться
, а уже зависящие от него модули, подписываются на это событие, и удаляются, перед этим сами поджигая событие я_собираюсь_удалиться
. Таким образом всё дерево (или ветка) модулей может быть установлено рекурсивно снизу вверх, или удалено рекурсивно сверху вниз.
И так, мы пришли к пониманию того как должны взаимодействовать модули между собой. Для себя я выработал несколько простых правил:
- Зависимые модули могут вызывать команды зависимостей.
- Зависимости вызывают события на которые могут быть подписаны зависимые.
- Зависимость не может рассчитывать на исполнение обработки события - она не знает о наличии или отсутствии обработчиков.
- Зависимость обязуется исполнять команды вызванные зависимыми модулями.
Хороший и качественный модуль должен поставлять команды для взаимодействия, и поджигать события в ключевых моментах своей работы. Таким образом он становится гибким и "социальным", а приложение наполненное таким модулями, будет долго жить, цвести, и пахнуть фиалками.
Надеюсь, что заметка была интересной ;-)