Декаплинг и фреймворки / Блог им. Fiesta


Оригинал: Living Apart Together: Decoupling Code and Framework

Вы, конечно, используете в своей работе новейшие технологии и фреймворки. Более того, вы самостоятельно написали 2,5 фреймворка, ваш код  PSR-2 совместим, полностью юнит-тестирован, имеет сопровождающие PHPMD  и PHPCS конфигурации, и даже может поставляться с надлежащей документацией (на самом деле, она существует!). При выпуске новой версии вашего любимого фреймворка вы захотите побыстрее использовать его в своем ​​проекте и получите пару отчетов об ошибке. Вы, может быть, даже воспользуетесь модульным тестированием, чтобы определить ошибку и сделаете патч, чтоб исправить ее. Если это может помочь вам стать тем разработчиком, которым вы хотите быть, — пересмотрите отношения вашего кода и фреймворка.

Фреймворк и вы

Большая часть кода, написанная на профессиональном уровне, так или иначе зависит от использованного фреймворка. Разработчики не одиноки в этом мире, и повторно используя работу других программистов можно сэкономить много времени в долгосрочной перспективе. В интернете можно найти массу аргументов в защиту использования фреймворков. В этой статье я не собираюсь ничего доказывать, а просто рассматриваю это как должное. Поэтому сразу перейдем к делу, а именно разберемся, в чем проявляется зависимость кода от фреймворка?

Нерабочее время мне нравится проводить в IRC channel #zftalk на  irc.freenode.net и помогать другим. Когда Zend Framework 2 (ZF2) находился еще в процессе разработки, люди очень часто интересовались датой его выхода. Не потому, что они были готовы использовать его, а потому что они не хотели начинать новый проект на основе фреймворка ZF1, когда не горизонте уже появился ZF2. Достойный проект займет не меньше трех месяцев разработки, и если команда хочет получить код, который зависит от «последней и самой лучшей» версии, то разрабатывать под ZF1, а потом подстраиваться под ZF2 было бы огромной тратой времени. Мысль вполне понятна. Никто не хочет вкладывать кучу времени, усилий, денег во что-то, только чтобы потом узнать, что технологии устарели и продукт потерял половину своей стоимости. Если вы проводите 3 месяца за процессом кодирования, то, разумеется, хотите, чтобы результат получился самым лучшим на сегодняшний день.

Так может использовать Symfony (или любой другой фреймворк), а? Многие люди пошли по этому пути или даже полностью сменили языки программирования (Python на Ruby). И все потому что они не хотят откладывать свои проекты в долгий ящик. Другие полностью забыли про свои проекты до выхода ZF2. Хотя я считаю, что отсрочку проекта не стоит рассматривать как вариант, а переключение с фреймворка на фреймворк не должно ставить под удар ваш продукт. Позвольте мне сказать вам это прямо сейчас: вы должны разрабатывать, опираясь на ZF1, даже если ZF2 может выйти завтра. Поймите, то же самое неизбежно произойдет с ZF2 и ZF3. Если хотите, вставьте сюда ваш любимый фреймворк текущей и будущей версии.

Гордое одиночество

Давайте представим, что сейчас 2011 год и работа над ZF2 в процессе, но никакие сроки еще не определены. Продукт будет готов, когда будет готов.

Ваш код должен иметь возможность перехода с одного фреймворка на другой, которую будет возможно реализовать в течение нескольких дней. Пишите свой новый проект на ZF1, даже если ZF2 может выйти в следующем месяце. Если вы разрабатываете, не допуская ошибок, то у вас не возникнет неприятностей, даже если другие участники проекта решили, что он должен получить поддержку ZF2. В зависимости от количества компонентов фреймворка, которые вы используете, это изменение можно легко внести в течение недели. И с тем же успехом вы можете полностью переходить от одного вендора к другому и использовать Symfony, CakePHP, Yii или любой другой фреймворк. Если вы пишете код без связанных зависимостей, используя небольшие врапперы, которые взаимодействуют с фреймворком, ваша логика защищена от сурового внешнего мира, где фреймворки могут модернизироваться или меняться. Ваш код счастливо живет в своем собственном маленьком мире, где ничего не меняется и останется прежним.

Все это звучит очень хорошо в теории, но я понимаю, что это трудно представить, не имея некоторых примеров кода. Тем временем мы все еще в 2011 году, все еще ждем выхода ZF2, и у нас есть крутая идея компонента, который будет отвечать на главный вопрос жизни, вселенной и всего остального. Вычисление ответа не займет много времени, потому мы решили сохранить результат. И если вопрос когда-нибудь прозвучит снова, мы можем взять ответ из хранилища, а не ждать еще 7,5 миллионов лет, чтобы пересчитать его. Я хотел бы показать код, который вычисляет ответ. Я сосредоточился на части хранения данных:
<?php
$solver = new MyUltimateQuestionSolver();
$answer = $solver->compute();
// now that we have the answer, let's cache it
$db = Zend_Db::Factory('PDO_MYSQL', $config['db']);
$record = array('answer' => $answer);
$db->insert('cache', $record);
 

Обычно, доступно, работает, как задумано. Но не сработает, если мы поменяем ZF1 на ZF2, Symfony и т.д.

Обратите внимание, что мы использовали отделение вендора Zend_Db. Этот же код будет прекрасно работать и в других случаях, если мы просто поменяем значение PDO_MYSQL. Вызовы insert() и factory() все равно будут работать, даже если мы перейдем, скажем, на SQLite. Так почему бы не сделать то же самое для фреймворка?

Давайте поместим код в небольшую обертку:
<?php
class MyWrapperDb
{
    protected $db;
 
    public function __construct() {
        $this->db = Zend_Db::Factory('PDO_MYSQL', $config['db']);
    }
 
    public function insert($table, $data) {
        $this->db->insert($table, $data);
    }
}
 
// Business Logic
$solver = new MyUltimateQuestionSolver();
$answer = $solver->compute();
// now that we have the answer, let's cache it
$db = new MyWrapperDb();
$db->insert('cache', array('answer' => $answer));

Мы взяли специфические особенности фреймворка из бизнес-логики и теперь можем поменять фреймворк в любое время, только изменив обертку.

Остаемся в 2011 году. Теперь давайте предположим, что мы решили запустить продукт с поддержкой MongoDB, потому что сейчас это самое модное слово. Фреймворк ZF1 не поддерживает MongoDB изначально, поэтому мы используем специальное расширение PHP:
<?php
class MyWrapperDb
{
    protected $db;
 
    public function __construct() {
        $mongo = new Mongo($config['mongoDSN']);
        $this->db = $mongo->{$config['mongoDB']};
    }
 
    public function insert($table, $data) {
        $this->db->{$table}->insert($data);
    }
}
 
// Business Logic
$solver = new MyUltimateQuestionSolver();
$answer = $solver->compute();
// now that we have the answer, let's cache it
$db = new MyWrapperDb();
$db->insert('cache', array('answer' => $answer));

Рафинированная абстракция

Обратите внимание: ни один элемент из бизнес-логики не изменился, когда мы перешли на MongoDB. Вот именно об этом я пытаюсь вам сказать: при программировании бизнес-логика отделяется от фреймворка (будь то ZF1 в первом примере или MongoDB во втором примере) и остается той же. Не составит особого труда понять, как можно адаптировать обертки для всевозможных фреймворков без того, чтобы что-то менять в бизнес-логике. Таким образом, с выходом ZF2 вам не придется рассматривать под микроскопом каждую строку вашего приложения, чтобы увидеть следы использования ZF1. Все что вам нужно — это обновить ваши обертки — и готово!

Если вы используете Dependency Injection/Service Locator  или аналогичный шаблон дизайна, для вас не составит никакого труда оперативно поменять обертки. Вы делаете один интерфейс, а дизайн связываете с обертками того типа, которого они должны придерживаться. Вы даже можете написать простой макет обертки, придерживаясь такого же интерфейса, а модульное тестирование не займет много времени.

Давайте добавим интерфейс и макет обертки, а также обертку для долгожданного ZF2:
<?php
Interface MyWrapperDb
{
    public function insert($table, $data);
}
 
class MyWrapperDbMongo implements MyWrapperDb
{
    protected $db;
 
    public function __construct() {
        $mongo = new Mongo($config['mongoDSN']);
        $this->db = $mongo->{$config['mongoDB']};
    }
 
    public function insert($table, $data) {
        $this->db->{$table}->insert($data);
    }
}
 
class MyWrapperDbZf1 implements MyWrapperDb
{
    protected $db;
 
    public function __construct() {
        $this->db = Zend_Db::Factory('PDO_MYSQL', $config['db']);
    }
 
    public function insert($table, $data) {
        $this->db->insert($table, $data);
    }
}
 
class MyWrapperDbZf2 implements MyWrapperDb
{
    protected $db;
 
    public function __construct() {
        $this->db = new ZendDbAdapterAdapter($config['db']);
    }
 
    public function insert($table, $data) {
        $sql = new ZendDbSqlSql($this->db);
        $insert = $sql->insert();
        $insert->into($table);
        $insert->columns(array_keys($data));
        $insert->values(array_values($data));
        $this->db->query(
            $sql->getSqlStringForSqlObject($insert),
            $this->db::QUERY_MODE_EXECUTE);
    }
}
 
class MyWrapperDbTest implements MyWrapperDb
{
    public function __construct() { }
 
    public function insert($table, $data) {
        return ($table === 'cache' && $data['answer'] == 42);
    }
}
 
// -- snip --
 
public function compute(MyWrapperDb $db) {
    // Business Logic
    $solver = new MyUltimateQuestionSolver();
    $answer = $solver->compute();
    // now that we have the answer, let's cache it
    $db->insert('cache', array('answer' => $answer));
}

При использовании интерфейса в точке инъекции зависимостей вводится правило, применимое к оберткам: они должны придерживаться интерфейса — в противном случае код вызовет ошибку. Это означает, что обертки должны реализовывать метод insert(), иначе они не будут удовлетворять правилу. Наша бизнес-логика может спокойно полагаться на этот метод и можно особо не париться насчет деталей реализации. Будь то ZF1 или ZF2, расширение MongoDB, WebDAV — модуль загрузки на удаленный сервер — бизнес-логику это не волнует. И как вы видите в последнем примере, мы можем даже написать небольшой макет обертки, внедрив тот же интерфейс. Если мы используем макет Dependency Injection/Service Locator в модульном тестировании, то можно надежно проверить бизнес-логику без необходимости какой-либо формы хранения данных. Все, что нам действительно нужно, так это интерфейс.

Заключение

Даже если ваш код не настолько сложен, чтобы занять 7500 тысяч лет разработки, вы все равно должны разработать его таким образом, чтобы он не зависел от какого-либо фреймворка. Сложно предположить, что случится с вашими любимыми фреймворками, и останется ли обратная совместимость. Фреймворк — это лишь деталь реализации, он должен быть отделен от кода. Так вы всегда сможете поддерживать свое гениальное приложение на высшем уровне. Реальная логика будет жить счастливо в своем маленьком пузыре, созданном обертками, защищенном от коварных деталей реализации и сердитых зависимостей. Поэтому, когда анонсируется ZF3/Symfony3/что-угодно-еще, не прекращайте писать код, не изучайте новые фреймворки, потому что должны (достойная причина — желание узнать больше). Пишите обертки для очередного релиза, как только разберетесь со старым.