![](https://habrastorage.org/getpro/habr/post_images/e9b/e7f/932/e9be7f932020e1643ee99083552c6eac.jpg)
Представим себе компанию, которая реализует целый ряд собственных продуктов — как для внешних пользователей, так и для внутренних. Скорее всего, каждый продукт не сможет существовать по отдельности, как сферический конь в вакууме, а в какой-то степени будет интегрирован с другими. В итоге все они вместе образуют некий слой взаимосвязанных между собой и при этом самостоятельно развивающихся организмов. И скорее всего, их развитие осуществляется совершенно разными командами.
Хорошие примеры такой среды — это Яндекс (поиск, Директ, карты, почта, вертикальные и внутренние сервисы) или Google. Понятно, что у перечисленных гигантов технологии в каждом продукте свои, но если взять компанию поменьше и работающую в более узкой предметной области, то можно предположить, что веб-продукты будут выполнены на одних технологиях (языках программирования, фреймворках и.т.д).
Именно об опыте в организации архитектуры всей линейки продуктов у такой компании я и хочу рассказать.
Наши внешние веб-продукты — это онлайн-версия, открытый справочный API и картографический, сервис отзывов. Внутренние: CRM, биллинг, алгоритмические вычисления, системы статистики и
экспорта-импорта данных.
Выведем два понятия:
Веб-инфраструктура — совокупность веб-продуктов компании, связанных между собой и работающих в близких предметных областях.
Технологическая база — единая кодовая база для веб-инфраструктуры компании. Содержит набор программных блоков с высоким и низким уровнем абстракции. Высокоуровневые блоки — это уже наработанные командой разработчиков сущности в предметной области компании. Низкоуровневые блоки — это, например, набор библиотек или фреймворков.
Наверняка не раз каждый из вас работал над несколькими проектами и реализовывал модули, которые потом переиспользовал в следующем проекте. Не всегда это выходит гладко: образуется совершенно отдельная версия модуля в отдельной ветке SVN, а потом в третьем проекте необходимо применить нечто среднее между первыми двумя версиями модуля и т.д и т.п. Другими словами, если не перечислять всех проблем, это критерии качества технологической базы. Давайте обозначим их:
- Переиспользование модулей и компонентов между проектами;
- расширямость функционала уже написанных модулей без переписывания всей системы;
- единая схема деплоя продуктов;
- масштабируемость по нагрузке.
Нашей целью было создание такой веб-инфраструктуры. Первое, с чего мы начали — подготовка технологической базы. Нами были выбраны следующие фреймворки и технологии:
- PHP;
- фреймворк для приложения — yii;
- фреймворк для деплоя и развертывания приложения — phing;
- фреймворк для написания unit-тестов — phpUnit;
- система непрерывной интеграции — Jenkins;
- функциональное и UI-тестирование — Selenium;
- СУБД Postgres;
- key-value хранилища memcached, redis;
- веб-сервер nginx.
(Почему выбраны именно эти системы, обсуждать не будем, это тема для отдельной публикации.)
Для обеспечения критериев качества технологической базы мы решили заложить слоистую архитектуру.
Слоистая архитектура — это такая архитектура системы, при которой система состоит из некоторой упорядоченной совокупности программных подсистем, называемых слоями, такой, что:
- на каждом слое ничего не известно о свойствах (и даже существовании) последующих (более высоких) слоев;
- каждый слой может взаимодействовать по управлению (обращаться к компонентам) с непосредственно предшествующим (более низким) слоем через заранее определенный интерфейс, ничего не зная о внутреннем строении всех предшествующих слоев;
- каждый слой располагает определенными ресурсами, которые он либо скрывает от других слоев, либо предоставляет непосредственно последующему слою (через указанный интерфейс) некоторые их абстракции.
Таким образом, в слоистой программной системе каждый слой может реализовать некоторую абстракцию данных. Связи между слоями ограничены передачей значений параметров обращения каждого слоя к смежному снизу слою и выдачей результатов этого обращения от нижнего слоя верхнему. Недопустимо использование глобальных данных несколькими слоями. Более полное определение можно найти в работах Фаулера.
Не рассматривая сеть и сервера, которые сами по себе являются отдельными слоями, рассмотрим устройство технологичекой платформы. В нёй можно выделить три слоя:
![](https://habrastorage.org/getpro/habr/post_images/ae3/704/60b/ae370460bff69e72aaabacc772a75432.jpg)
Рис. 1. Слои архитектуры технологической платформы
На рис. 1 выделены три слоя:
- Core layer — слой ядра. Здесь располагаются общие низкоуровневые библиотеки и фреймфорки. Например, yii.
- Shared layer — слой модулей, реализующих какой-то функционал системы, которые могут быть использованы в нескольких проектах.
- Application layer — слой приложений. На этом уровне располагаются все приложения с их специфичными модулями.
При этом важным моментом является то, что модули должны иметь возможность легко перемещаться из слоя приложения в уровень общих модулей. Объясняется это очень просто: когда стартует новый проект, обычно реализуется самый необходимый функционал. По мере развития проекта, он начинает обрастать связями с другими продуктами (решили, например, подтягивать отзывы с одного проекта к кафешкам на другом нашем проекте), и, возможно, часть модулей может потребоваться для других проектов. Либо проект разделяется на части, и модули разделяются по нескольким приложениям.
Рассмотрим более детально файловую организацию приложения в Application layer с учетом специфики yii.
application framework - сюда деплоится фреймворк lib - компоненты из core и shared. Для этой папки в конфиге Yii заведен отдельный alias components extensions ... protected components extensions ... public themes config
рис 2. Файловая организация yii приложения
Как мы видим, компоненты и расширения есть как на уровне приложения, так и на общем уровне (shared). Здесь представлена структура уже собранного из репозитария приложения. Конечно, в системе контроля версий организовать можно все иначе. У нас, например, есть два режима сборки, когда все заливается в папку приложения и когда делаются симлинки на библиотеку расширений. Первый необходим для изоляции разных приложений, когда мы не можем отдельно обновить расширения без обновления всех приложений, и для развертывания нескольких инстансов на одной машине. Или развертывания разных веток на одном сервере. Второй удобен разработчикам при работе с кодом.
Итак, мы задали гибкую основу для нашей веб-инфраструктуры, но достаточно ли этого? Нет. Не хватает двух важных вещей:
- архитектура приложения должна быть со слабым зацеплением и разделяемой;
- система деплоя и сборки продукта.
Теперь по каждому пункту подробней.
Архитектура приложения
В основу архитектуры приложения также положим слои. Выделим следующие уровни:
![](https://habrastorage.org/getpro/habr/post_images/972/36f/a6a/97236fa6a623091310c91f08c5cdf273.jpg)
рис 3. Слои yii-приложения
Слой «тонких» контроллеров содержит минимум логики и оперирует API расширений. Слой бизнес-логики состоит из уровня расширений и уровня моделей данных. Слой Yii-расширений и модели данных имеют очень сильную степень связанности, на диаграмме это показано более жирной стрелкой.
Наибольшая степень связанности существует между приложением и «тонкими» контроллерами. Вероятность переиспользования минимальна. А вот связанность между слоем «тонких» контроллеров и слоем бизнес-логики нужно сделать как можно меньше, так как бизнес-логику мы можем и должны переиспользовать в других наших приложениях. И сделать мы это можем при помощи гибкости Yii и его конфига.
Конфиг осуществляет подключение нужного нам набора компонент и расширений. Пример подключение расширения:
'geoip' => array('class' => 'application.extensions.GeoIP.CGeoIP'),
К инициализируемому расширению в приложении можно будет обращаться по ключу geoip. Например:
Yii::$app->geoip;
При первом обращении к компоненту произойдет инициализация класса CGeoIP. Гибкость заключается в наличии ключа :-) Мы можем в любой момент через конфиг подменить реализацию, а приложение будет работать по прежнему по ключу geoip.
Основываясь на этой гибкости, мы и сможем легко управлять связанностью между приложением и слоем бизнес-логики.
Группируя логику работы с некой сущностью приложения в расширения, мы делаем ее отделяемой. Все модели таким образом инкапсулированы в расширении и контроллер приложения работает с данными через API расширения.
Расширения — это дополнительный уровень абстракции для нас, за которым мы можем скрыть источники данных, необходимые для внутренней работы тех или иных методов API расширения.
Давайте немного пофантазируем и придумаем приложение:
![](https://habrastorage.org/getpro/habr/post_images/6a8/1f3/96d/6a81f396d15f3d4f239907b338162764.jpg)
рис 4. Пример архитектуры yii-приложения
Наше приложение работает с несколькими расширениями. Расширения оперируют набором данных и тесно связаны с слоем моделей. Важный момент: степень связанности меду слоями растет сверху вниз. Тесней всего связаны между собой модели данных.
Теперь давайте представим, что возникла необходимость в создании единого сервиса авторизации пользователей для всех продуктов. Мы можем легко реализовать такой проект в виде отдельного приложения. Для работы с новым приложением пишем расширение — rest-клиента UserServiceRestClient. Клиент будет иметь такое же API, что и UserExtension API. Смотрим рисунок для наглядности:
![](https://habrastorage.org/getpro/habr/post_images/1fa/e94/aa0/1fae94aa0a7738b67b1add0e3f081cf7.jpg)
рис. 5
UserServiceRestClient помещаем в shared-уровень и меняем в конфиге приложения класс, который нужно использовать. Так как API одинаковое, подмена реализации прошла без изменения кода. Очень гибко! Все остальные приложения так же работают с сервисом пользователей, используя UserServiceRestClient.
Итак, с архитектурой разобрались, но с таким подходом конфиг приложения будет немаленький. Руками прописывать все зависимости для приложения — совсем не по-джедайски. Тем более, когда проект живет и появляются еще и миграции баз данных. Все эти вопросы можно решить с помощью своей системы сборки и депллоя продуктов.
Система деплоя и сборки
Что должна делать система деплоя:
- осуществлять доставку кода в указанную директорию на сервере;
- подтягивать все зависимости приложения (расширения уровня shared и фреймворк);
- генерировать конфиг приложения;
- выполнять sql-миграции баз данных.
А ещё — задачи запуска unit-тестов, сборка debug-версий проекта и т.д Все не перечисляю, это дело вкуса, да и внимания всем этим операциям уделено уже достаточно много в интернете. К примеру, у нас система деплоя конфигурирует сопутствующее ПО типа nginx, Sphinx, RabbitMQ и создаетдокументацию. Удобно!
В целом же задачи обычные: файловые операции, работа с системами контроля версий, запуск сторонних утилит (PHPUnit, например, или Doxygen) и т.д Внимание стоит обратить на генерацию конфига. Мы сделали её по шаблону с мета-конфигурационным файлом:
application protected config main.php.template - шаблон конфига yii public themes build.prop - мета-конфигурационный файл build.xml - конфиг phing’a
рис. 6
При генерации конфига Phing пропарсивает шаблон и заменяет все placeholder’ы на соответствующие параметры, заданные в мета-конфигурационном файле. Получилось очень удобно. Все зависимости приложения задаем мета-конфиг:
## List of extensions, components and etc. to install ##
EXTENSIONS = myExt, myExt2
COMPONENTS = component1, component2
HELPERS = myHeper, myTextHelper
COMMANDS =
Так же в мета-конфиге прописаны базы данных, пути, хосты — в общем, все. Дополнительное удобство для автоматизации опять же в том, что все эти параметры можно передать через командную строку Phing’у и переопределить. А в будущем, например, наладить сборку пакетов под используемую вами *nix ОС.
Таким образом, мета-конфигурационный файл — это слой абстракции от формата и количества конфигов в нашем приложении.
В итоге гибкость конфигурации Yii + система сборки = легко конфигурируемые и собираемые продукты.
Итог
Итак, что нам показал опыт использования такой схемы работы в течение года?
- Если возникнет необходимость переиспользовать какие-то расширения, то это не станет проблемой, процесс непрерывной интеграции выстроился тоже без особых проблем.
- Есть необходимый запас гибкости, позволяющий легко развивать и наращивать функционал приложений. Функциональные блоки можно делать крайне быстро, не задумываясь о производительности и качестве кода.
- Если интерфейс продуман, всегда можно безбоязненно доделывать функционал, улучшать производительность, менять хранилище данных и проводить рефакторинг.
- Продуманная система деплоя приложения позволяет очень быстро разворачивать систему для тестирования и минимизирует ошибки при развертывании на боевых серверах, связанные с невнимательностью человека.
- Благодаря изолированности отдельных функциональных модулей всегда можно выделить отдельную группу разработки для работы с ним. Это позволяет решать проблемы роста отдела разработки и не превращаться в большой неуправляемый колхоз, где все работают над всем, и тем самым только мешают друг другу.
P.S. И да, мы ищем yii-разработчиков в Новосибирске и Киеве. Приходите! :-)