Если вы хотите писать приложения, которые можно поддерживать в долгосрочной перспективе, вам нужно абстрагироваться от вашего фреймворка, ORM, HTTP-клиента и т. д. потому что ваше приложение переживет их всех.
Три простых правила
Чтобы выполнить абстрагирование от фреймворка вам нужно всего лишь следовать этим простым правилам:
- Все ваши сервисы должны получать зависимости и настройки через конструктор. Когда зависимость использует Ввод-Вывод, вы должны создать для нее абстракцию и вынести код IO туда.
- Различные типы объектов не должны зависеть от сервисов.
- Контекст всегда должен передаваться в качестве аргументов метода.
Объясню подробнее
Правило 1
Наши сервисы никогда не должны знать о том, как были созданы и переданы его зависимости. Если мы будем использовать сервис контейнер внутри нашего сервиса, например Container::get(UserRepository::class)
, то в этом не будет смысла. Т.к. этот контейнер часть фреймворка и нам необходимо от него отвязаться. То же самое верно и для выборки значений конфигурации (например Config::get('email.default_sender')
).
Иногда зависимость использует Ввод-Вывод, то есть она общается с базой данных, файловой системой и т. д. В этом случае вы должны создать абстракцию для зависимости и вынести этот код в нее. Если вы зависите от конкретного класса, этот класс будет работать только с конкретной библиотекой или фреймворком, с которым вы работаете прямо сейчас, поэтому для того, чтобы оставаться отделенным, вы должны использовать свою собственную абстракцию в сочетании с реализацией, которая использует вашу текущую библиотеку/фреймворк.
Правило 2
Помимо сервисов будет существовать несколько других типов объектов, таких как сущности (Entity), объекты значений (Value Object, VO), доменные события (Domain Events) и объекты передачи данных (DTO), все эти объекты не должны иметь никаких обязанностей, никакой бизнес логики. Иначе это означает, что они либо будут вызывать сервисы через какой-то глобальный статический объект контейнера, либо им нужна специальная настройка фреймворка/ORM, что означает, что они не могут использоваться изолированно и не переживут серьезного обновления фреймворка или переключения на другой. Примером объекта, который не следует правилу 2, является ActiveRecord, которая может выглядеть как сущность, но способна сохранять сама себя, что на самом деле является служебной обязанностью.
Правило 3
Контекст запроса обычно всегда передается аргументом в методы. Например, имеется некий метод в сервисе доставки еды. Этот метод имеет контекст – номер пользователя, от лица которого выполняется этот метод. Вместо того, чтобы внутри этого метода обращаться к функционалу фреймворка для получения текущего пользователя (например Auth::getUser()->getId()
), мы можем передавать этот id в качестве аргумента метода.
Вместе взятые, правила 1, 2 и 3 гарантируют, что для каждого метода совершенно ясно, что он делает, какие данные ему нужны и на какие зависимости сервисов он полагается для выполнения своей работы. Кроме того, ни одна из зависимостей или аргументов метода не будет зависеть от фреймворка или библиотеки, а это означает, что ваш код приложения будет эффективно абстрагирован от фреймворка.
Вывод
Эти правила действительно очень просты и они не требуют никакой дополнительной работы в сравнении с тем, что пришлось бы делать при плотной связанностью кода. Почему бы не пользоваться этими правилами, если вы считаете, что ваш проект точно проживет более трех лет?
Следование этим правилам дает вам гораздо больше, чем абстрагирование от фреймворка: все становится тестируемым изолированно, тесты полностью детерминированы и поэтому очень стабильны и они действительно быстры в выполнении.