Введение
Перед нами стоит задача встроить возможность отправки SMS. Мы могли бы написать класс для работы с конкретным провайдером (gate) или взять уже написанный класс самим провайдером. Но нам говорят, что в будущем возможна смена смс провайдера. Не беда, первая мысль — написать компонент, в котором за несколько часов мы потом сможем сменить реализацию отправки SMS. А теперь давайте забудем эту мысль и реализуем это более красиво, не привязываясь к провайдерам и с возможностью быстро переключаться с одного провайдера на другой.
Чтобы лучше понимать данную концепцию, я рекомендую рассматривать SMS провайдера как драйвер для отправки SMS. Переключение должно происходить так же безболезненно, как отключить ваш старый монитор и подключить новый или поменять клавиатуру. Рассматривайте этот компонент системы как физическое устройство. Да и вообще, ваше приложение — это некий компьютер (устройство компонент), к которому подсоединяются различные компоненты, как в конструкторах Lego. Как мне кажется, рассматривая свое приложение таким образом, у вас получится наиболее эффективно подойти к дизайну архитектуры.
Реализация
Все классы для SMS я буду помещать в папке `app\Acme\Sms` и зарегистрирую под PSR-0 в composer.json:
"psr-0": { "Acme": "app" }
Для начала нам нужно описать интерфейс, который будут использовать все смс драйвера и перечислить методы, которые нам нужны.
<?php namespace Acme\Sms; interface SmsGateInterface { /** * @param SmsRecipient $recipient * @param string $text */ public function send(SmsRecipient $recipient, $text); }
Нам потребуется пока только 1 метод `send`, который будет отправлять SMS. Класс `SmsRecipient` хранит в себе данные по получателю:
<?php namespace Acme\Sms; class SmsRecipient { public $phone; }
Установим класс для работы с провайдером SmsOnline в composer:
"require": { "laravel/framework": "4.0.*", "kkamkou/sms-online-api": "dev-master" }
Теперь нам нужно написать драйвер этого провайдера и реализовать интерфейс, который мы описали выше:
<?php namespace Acme\Sms; use SmsOnline\Api as SmsOnlineApi; class SmsOnlineGate implements SmsGateInterface { private $api; public function __construct(SmsOnlineApi $api) { $this->api = $api; } /** * @param SmsRecipient $recipient * @param string $text */ public function send(SmsRecipient $recipient, $text) { $this->api->send($recipient->phone, $text); } }
Но DI класса `SmsOnline\Api` провести так просто у нас не получится, т.к. конструктор класса `SmsOnline\Api` принимает массив с конфигурацией. Создадим конфигурационный файл для нашего SMS компонента (`app/config/sms.php`), и заодно поставим драйвер по-умолчанию `SmsOnlineGate`:
<?php return [ 'default' => 'Acme\Sms\SmsOnlineGate', 'drivers' => [ 'Acme\Sms\SmsOnlineGate' => [ 'user' => '', 'secret_key' => '', ], ], ];
Теперь дело за IoC. Создадим файл `app/bindings.php`, где мы будем настраивать IoC:
<?php $smsConfig = Config::get('sms'); $smsGate = $smsConfig['default']; App::bind('Acme\Sms\SmsGateInterface', $smsGate);
Мы получаем драйвер для SMS по-умолчанию и говорим IoC, что когда приложение хочет `SmsGateInterface` отдай ему `SmsOnlineGate`. Кстати, если вы уже PHP до версии 5.5, то рекомендую код переписать следующим образом:
app/config/sms.php
<?php use Acme\Sms\SmsOnlineGate; return [ 'default' => SmsOnlineGate::class, 'drivers' => [ SmsOnlineGate::class => [ 'user' => '', 'secret_key' => '', ], ], ];
app/bindings.php
<?php use Acme\Sms\SmsGateInterface; $smsConfig = Config::get('sms'); $smsGate = $smsConfig['default']; App::bind(SmsGateInterface::class, $smsGate);
Это удобно тем, что при рефакторинге мы сможем легко менять названия классов, а IDE, в свою очередь, заменит эти строки включительно.
Далее нам нужно прописать конфигурацию для `SmsOnline\Api`, дополнив app/bindings.php
<?php use Acme\Sms\SmsGateInterface; use Acme\Sms\SmsOnlineGate; // указываем текущий драйвер $smsConfig = Config::get('sms'); $smsGate = $smsConfig['default']; App::bind(SmsGateInterface::class, $smsGate); // настраиваем класс "SmsOnline" App::bind(SmsOnline\Api::class, function ($app) { $gateConfig = Config::get('sms'); $gateConfig = $gateConfig['drivers'][SmsOnlineGate::class]; return new SmsOnline\Api($gateConfig); });
Теперь когда приложение потребует объект класса `SmsOnline\Api` — оно получит сконфигурированный экземпляр.
Используя данный дизайн в вашем приложении вы сможете легко переключаться между провайдерами — вам будет достаточно написать драйвер для него и поменять конфигурацию, как например, во время разработки мы не хотим отправлять SMS через провайдера, поэтому мы можем записывать куда-нибудь в БД или даже в файл. Для этого мы напишем драйверы `DatabaseSmsGate` и `FileSmsGate` по тому же принципу. Самое время перейти к самой «вкусной части» — покрытие кода тестами.
Тестирование
Собственно, это самый главный плюс в DI: удобное тестирование в полнейшей изоляции. Вместо того, чтобы прососывать настоящие объекты с рабочими методами — в тестах вы создаете Mock объекты с методами заглушек и проверяете, то что метод был вызван n раз с ожидаемыми аргументами и в определенном порядке. Давайте рассмотрим как протестировать наш код, написанный выше.
Для начала мне нужно установить phpunit и mockery. Ставлю так же через composer:
"require-dev": { "phpunit/phpunit": "3.8.*@dev", "mockery/mockery": "dev-master" }
Во время тестирования каждого класса я хочу чтобы мои тесты выполнялись в полной изоляции. Например, когда вы тестируете класс `SmsOnlineGate`, в его методе `send` вызывается метод `send` из `SmsOnlineApi`, но он не должен вызываться физически. То есть вы проверяете только то, что метод `send` из `SmsOnlineApi` был вызван, но никак не физически. Для этого мы будем использовать Mock объекты. Рассмотрим как будет выглядеть наш тест:
<?php use Acme\Sms\SmsOnlineGate; use Acme\Sms\SmsRecipient; class SmsOnlineGateTest extends TestCase { /** * @var SmsOnline\Api */ private $api; /** * @var SmsOnlineGate */ private $gate; /** * @var SmsRecipient */ private $recipient; public function setUp() { parent::setUp(); $this->api = Mockery::mock(SmsOnline\Api::class)->makePartial(); $this->recipient = Mockery::mock(SmsRecipient::class); $this->gate = new SmsOnlineGate($this->api); } public function test_send() { $text = 'текст смс'; $this->api->shouldReceive('send') ->withArgs([$this->recipient->phone, $text]) ->once(); $this->gate->send($this->recipient, $text); } }
Тест заключается в том, что мы проверяем, что метод send в `SmsOnline\Api` был действительно вызван один раз с требуемыми параметрами. На самом деле он не был вызван, вместо этого был вызван метод из нашего Mock объекта, и в этом нам помог Mockery.
Нам нужен еще один тест, чтобы убедиться, что когда приложение хочет получить `SmsGateInterface`, IoC возвращает нам `SmsOnlineGate`, т.к. он прописан у нас в конфиге по умолчанию:
<?php use Acme\Sms\SmsGateInterface; use Acme\Sms\SmsOnlineGate; class SmsGateTest extends TestCase { public function test_instance() { $instance = App::make(SmsGateInterface::class); $this->assertInstanceOf(SmsOnlineGate::class, $instance); } }
На этом все, что я хотел рассказать. Здесь я не рассматривал инъекцию объектов в IoC через ServiceProvider, что является более правильным решением.
Я надеюсь, что я достаточно подробно расписал мое видение в пользе DI и IoC на примере компонента отправки смс. Помните, что разработка должна приносить удовольствие, а вы себя должны чувствовать художником, который рисует механизм c прекрасным внутренним устройством. Если у вас все еще остались вопросы — спрашивайте в комментариях, я с удовольствием на них отвечу.
Список полезной литературы:
- What is Dependency Injection? автор Fabien Potencier
- Документация по Laravel IoC контейнеру
- Mockery: A Better Way Nettuts+
- Laravel Testing Decoded — лучшая книга по введению в TDD и лучшим практикам в unit тестировании, написанная Jeffrey Way. Относится не только к Laravel.
- Описание стандарта PSR-0
- Laravel: From Apprentice To Artisan — книга, написанная автором фреймфорка Laravel — Taylor Otwell, по дизайну архитектуры приложения и возможностям Laravel.
- Laracasts — видеоуроки по Laravel. Новые появляются почти каждый день. Есть бесплатные, а есть и по подписке всего за 9$ в месяц.
ссылка на оригинал статьи http://habrahabr.ru/post/206442/
Добавить комментарий