Статьи

Жить отдельно друг от друга: разделение кода и структуры

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

Рамки и Вы

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

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

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

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

Ради аргумента, давайте представим, что сейчас 2011 год, и работа над ZF2 продолжается, но пока нет определенной временной шкалы; это будет сделано, когда это будет сделано.

Хотя удивительно, что вы повторно используете столько кода, сколько может предложить ваша любимая инфраструктура, ваш код должен иметь возможность переключать платформы в течение нескольких дней. Вы мастер ZF1? Затем напишите свой новый проект на 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() Так почему бы не сделать то же самое для самой платформы?

Давайте переместим код в небольшую оболочку:

 <?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, а затем реорганизовать его для использования ZF2; все, что вам нужно обновить, это ваши обертки, и все готово.

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

Вывод

Даже несмотря на то, что ваш код, вероятно, не настолько сложен, что его запуск занимает 7,5 миллионов лет, вы все равно должны спроектировать его так, чтобы он был переносимым на тот случай, если земля будет разрушена вогонами, и вам придется переместить его на другую планету (или фреймворк). , Вы не можете предполагать, что ваш любимый фреймворк всегда будет обратно совместимым или даже будет существовать вечно. Фреймворки, даже поддерживаемые крупными компаниями, являются деталями реализации и должны быть отделены как таковые. Таким образом, ваше классное гениальное приложение всегда может поддерживать новейшие и лучшие приложения. Настоящая логика будет счастливо жить в маленьком пузыре, созданном упаковщиками, защищенным от всех злых деталей реализации и злых зависимостей. Поэтому, когда объявляется ZF3 / Symfony3 / whichever-else-big-вещь: не прекращайте писать код, не изучайте новые фреймворки, потому что вам нужно (хотя вы должны это делать, потому что вы хотите узнать больше), быть продуктивным внутри пузырь и напишите обертки для следующей большой вещи, как только следующая большая вещь будет выпущена.

Изображение через Fotolia