Уменьшаем связанность пакетов

2015-10-27_120755

Существуют не так много вариантов того, как мы можем разъединить пакеты, к тому же сделать это не так-то просто. Поэтому давайте рассмотрим, какие варианты лучше других.

Для примера скажем, что вы пишите пакет или библиотеку для ответа на HTTP запросы (такой пакет можно было бы считать базой веб-фреймворка). Как вы справитесь с маршрутизацией?

Если вы напишите свой пакет Router независимым (и он будет хорош: маленькие и специализированные пакеты более универсальны и сопровождаемы), вы бы не хотели связывать его с HTTP пакетом, оставив выбор маршрутизатора на усмотрение пользователя.

Итак, как, по-вашему, сделать HTTP и Router пакеты не связанными друг с другом?

События

Первый вариант – это обратиться к событийно-ориентированному программированию. Опираясь на библиотеку/пакет «Менеджер событий», у вас может быть свой пакет, генерирующий определённые события в стратегических точках кода. Эти события могут влиять на потоки кода.

Такое решение выбрано Symfony, чтобы решить проблему маршрутизации, представленную ранее. Их компонент HttpKernel отделяется от компонента Routing через события.

Вот упрощённый кусок кода их класса HttpKernel (который является базой для HTTP приложения):

$eventDispatcher->dispatch(
    KernelEvents::REQUEST,
    new GetResponseEvent($this, $request, $type)
);

$controller = $request->attributes->get('_controller');

if (! $controller) {
    throw new NotFoundHttpException();
}

Здесь происходит следующее:

  • HttpKernel генерирует событие «Kernel Request»;
  • затем он ожидает, что слушатель установил контроллер c ключом «_controller» в запросе.

Значит, так HttpKernel разъединяется от Router? Да. Слушатель, устанавливающий контроллер, может быть фактически чем-угодно.

Как подчёркивалось выше, проблема здесь в том, что HttpKernel ожидает что-то конкретное и определённое от слушателей. Целое приложение зависит от неизвестного слушателя, фактически зарегистрированного для конкретного события, и следует за его неопределённым поведением.

Я полагаю, что события должны использоваться для присоединения в главный логический поток с целью расширения того. Но главный логический поток должен быть линейным, а не полагаться на возможные побочные эффекты события.

Есть и другие проблемы, которые мы можем обнаружить при использовании такого решения:

  • пакет в итоге связан с Менеджером событий (вы просто заменили одну зависимость другой…);
  • код не линеен, от чего труднее внести свой вклад в проект разработчикам;
  • поведение разъединённых пакетов не определяется договорами.

Относительно доводов «за»:

  • не определяемое поведение может принести и пользу, ведь не ограничиваются варианты реализации;
  • такой подход может быть куда проще, чем другие варианты.

<оговорка> Просто, чтобы было ясно, Symfony является исключительным фреймворком, и я люблю его. Я уверен, что решение выбрать такой вариант был тщательно продуман, и важно помнить, что он работает. Symfony, вероятно, самый популярный современный PHP фреймворк, и данная статья не изменяет этого факта. Я просто использую его здесь в качестве примера и хочу представить альтернативные варианты, обсудить плюсы и минусы.</оговорка>

Интерфейсы и адаптеры

Я упоминал специфичное поведение с договорами. В PHP (как и в большинстве других ОО языках), вы можете реализовать их с помощью интерфейсов.

Если мы возьмём предыдущий пример, HTTP-пакет может содержать RouterInterface для определения того, как компонент маршрутизации должен вести себя (это очень простой пример):

namespace Acme\Http;

interface RouterInterface {
    /**
     * @return callable The controller to use for this request
     */
    public function route(Request $request);
}

Вы заметите, что не всё точно определено, например, тип возврата. Надеюсь, PHP в следующих версиях позволит это (уже скоро релиз PHP 7! прим. перев.), но до тех пор единственным решением будет использовать документацию.

В классе HttpApplcation мы можем использовать интерфейсы в подсказке типа, чтобы принять любую реализацию (классическое внедрение зависимостей). И вот то, на что была бы похожа логика кода:

$controller = $router->route($request);

if (! $controller) {
    throw new NotFoundHttpException();
}

Поток кода линеен и абсолютно явен. И HTTP пакет отделён от любого маршрутизатора!

Единственная проблема: пакеты маршрутизатора будут вынуждены реализовывать Acme\Http\RouterInterface, если они захотят использовать HTTP пакет. Из-за этого они в конечном итоге связываются с ним… Так как же мы отделим пакеты маршрутизатора от HTTP пакетов?

Способ обойти эту проблему есть — использование адаптеров:

namespace \MyApplication\Adapter;

class HttpRouterInterfaceAdapter implements \Acme\Http\RouterInterface {
    private $router;

    public function __construct(\Acme\Router\Router $router) {
        $this->router = $router;
    }

    public function route(Request $request) {
        $this->router->route($request);
    }
}

Благодаря такому адаптеру, класс Router не должен реализовывать RouterInterface. Таким образом, он полностью отделён от HTTP пакета.

Однако, как вы видите, потребуется писать адаптер каждый раз, когда вы захотите «соединить» отсоединённые пакеты.

Доводы «за»:

  • линейный и явный поток кода;
  • определённое поведение (использование интерфейсов).

Доводы «против»:

  • требуется написание интерфейсов;
  • требуется написание адаптеров.

Эта стратегия интерфейсов недавно использовалась в Laravel. Для 5.0 версии (IIRC), Laravel опубликует пакет illuminate/contracts, содержащий все интерфейсы, используемые другими пакетами, что позволит разъединить пакеты Laravel, не прибегая к адаптерам: пакеты смогут реализовать интерфейсы на очень выгодных условиях, т.к. пакет illuminate/contracts будет очень лёгким и содержать только интерфейсы.

Стандартизированные интерфейсы

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

Хороший пример этого, очевидно, логирование. Раньше было множество различных библиотек логирования для PHP. Тогда группа PHP-FIG работала над созданием стандарта логера PSR-3.

Сегодня многие пакеты логирования реализуют Psr\Log\LoggerInterface и самые современные фреймворки имеют подсказку-типа этого интерфейса вместо конкретных реализаций. Это означает, что пользователи могут выбрать любой PSR-3 совместимый логер и фреймворк будет использовать его.

Конечно, это идеальная ситуация: никаких связей, никаких усилий! Но логирование было своего рода лёгкой темой. Очень трудно придумать стандарты для всех других компонентов, которым нужны интерфейсы, в основном потому, что реализации зачастую сильно различаются.

PHP-FIG работает уже несколько лет над кэшем и HTTP сообщениями и, надеюсь, они будут выпущены когда-нибудь (PSR-7 — HTTP message interfaces уже принят, прим. перев.). Тем временем, проект container-interop стремится обеспечивать интерфейсы для стандартизации использования контейнеров внедрения зависимостей.

Вывод

Я бы хотел закончить на идее, предложенной приблизительно год назад во внутренней почтовой рассылке PHP: «слабые интерфейсы». Это такие интерфейсы, определяющие поведение, но которые не должны быть реализованы классами. Они смешивают принцип статической типизации с утиной:

«Если это выглядит как утка, плавает как утка и крякает как утка, то, вероятно, это утка.»

Что на самом деле хорошо здесь, так это то, что пакетам позволяется определять свои интерфейсы и подсказку типа, не требуя фактической реализации их. Пока объекты совместимы с интерфейсом, он работает. Это своего рода, когда тот факт, что класс X реализует Y, определяется во время исполнения. Пример:

interface FooInterface {
    public function hello();
}

// Foo does not implement FooInterface
class Foo {
    // But this method makes it compatible with FooInterface
    public function hello()
    {
        return 'Hello world';
    }
}

// That pseudo-syntax tells that this is a weak-interface type-hinting
function run(<<FooInterface>> $foo) {
  echo $foo;
}

// It works because Foo is compatible with the interface
run(new Foo);

// Error, stdClass is not compatible with FooInterface
run(new stdClass);

Данный пример представлен забавы ради, но я хочу, чтобы такая возможность появилась в PHP (вместе со статистическим типом возврата). Это сильно помогло бы функциональной совместимости пакета и разъединению.

Обновление: то, что я назвал слабым интерфейсом, его автор в оригинальном RFC, Энтони Феррара, связал со мной в соответствующей теме: PHP RFC: Structure Type Hinting.

На что в комментариях также добавили, что тот же Энтони Феррара написал и пользовательскую реализацию: ProtocolLib.

Примечание

Это авторский перевод статьи «Decoupling packages — Matthieu Napoli» на русский язык.

Комментарии

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *