Паттерн «Адаптер» в Laravel

Паттерн Адаптер в Ларавел

В этой статье мы рассмотрим, как можно использовать шаблон проектирования «Адаптер» в Laravel и его преимущества на примере из реальной жизни.

Одну вещь мы точно не можем игнорировать, при создании веб-приложения — оно будет меняться. Возможно это будет из-за поступающих требований или из-за подключаемых сторонних API. Вы можете считать, что ваша система спроектирована идеально, но всегда есть вероятность, что вам будет необходимо так изменить код, что разрушит весь ваш продуманный дизайн.

Паттерн «Адаптер» — это шаблон проектирования, который имеет дело с изменениями кода.

Шаблон Проектирования

Прежде чем продолжим, давайте выясним, что же такое шаблон дизайна. Согласно Википедии:

Шаблон проектирования или паттерн (англ. design pattern) в разработке программного обеспечения — повторяемая архитектурная конструкция,
представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста.

Проще говоря, шаблон проектирования — это подход для решения часто возникающих проблем при разработке программ.

Что такое Паттерн «Адаптер»?

Этот паттерн (также называемый паттерном Обертки) подпадает под категорию структурных шаблонов (Structural Patterns), которые учат нас, как создавать классы и как их нужно структурировать, чтобы ими можно было легко управлять или расширять.

В паттерне Adapter мы трансформируем интерфейс класса в такой, который будет понятен клиенту. Он позволяет классам работать вместе, несмотря на несовместимые интерфейсы.

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

Паттерн имеет следующих участников:

  • Client: клиент — это класс или объект, который хочет использовать открытый API Adaptee.
  • Adapter: адаптер, обеспечивает общий интерфейс между Adaptee и Client.
  • Adaptee: это объект из другого модуля или сторонней библиотеки.

Самым большим преимуществом шаблона адаптера является то, что он позволяет нам отделить наш клиентский код от реализации адаптера.

Шаблон можно использовать, когда ваш код зависит от какого-либо внешнего API или любого другого класса, который часто меняется.

Одним из реальных примеров использования этот паттерна могут быть зарядные устройства для смартфонов. Все мы знаем, что не получится использовать зарядное устройство от iPhone с телефонами Samsung и наоборот. Если вы хотите использовать одно зарядное устройство для обоих типов телефона, то вам понадобится адаптер, как показано ниже.

Паттерн Адаптер

Проблема

Хорошо бы попробовать практический сценарий для понимания процесса и компонентов шаблона адаптера, поэтому предположим, что у нас есть общий интерфейс для проверки ассортимента продукции для нашего веб-сайта.

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

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

// StockCheckController.php
use Illuminate\Http\Request;
use App\Services\DatabaseStockCheck;

class TestController extends Controller
{
    public function index(Request $request, DatabaseStockCheck $databaseStockCheck)
    {
        $sku = $request->input('sku');

        $stock = $databaseStockCheck->getStock($sku);

        return response()->json($stock);
    }
}

Сервисный класс DatabaseStockCheck

// DatabaseStockCheck.php
namespace App\Services;

use App\Product;

class DatabaseStockCheck
{
    public  function getStock($sku)
    {
        // Какой-то запрос к базе данных для поиска товара на складе
        $product = Product::whereSku($sku)->first();

        return $product->qty;
    }
}

Я использую очень простой пример, в реальном приложении все может быть сложнее.

Как вы видите, в методе index мы берем SKU (складская учётная единица) продукта из запроса и передаем его в сервисный DatabaseStockCheck, чтобы получить текущий уровень запасов данного продукта. Наш сервисный класс DatabaseStockCheck находит продукт и возвращает его текущее количество. Проще простого.

Но менеджер попросил меня добавить новый функционал, для проверки запасов товара по базы ERP (Enterprise Resource Planning = планирование ресурсов предприятия). Как только я получил это задание, то понял — будут проблемы с имеющимся кодом.

  • Во-первых, мы должны внести изменения в наш контроллер, чтобы сделать запрос к базе данных ERP.
  • Я не знаю, реализует ли API базы данных ERP метод getStock(). И нет возможности кого-то попросить сделать в нем изменения.
  • Поскольку нужно добавить другой сервис, то надо изменить класс DatabaseStockCheck.
  • Мы не знаем, предоставит ли API ERP нужный нам тип данных.

Можно решить все вышеперечисленные проблемы, используя паттерн «Адаптер». Предположим, что ниже расположен вендорный класс API ERP, который нужно использовать и у нас нет возможности, что-либо в нём менять.

// Фиктивный вендорный класс

namespace App\Vendor;

class Erp
{
    protected $sku;

    public function __construct($sku)
    {
        $this->sku = $sku;
    }

    public function checkStock()
    {
        // Волшеный API, возвращающий данные
        return [
            'sku'    => $this->sku,
            'status' => true,
            'qty'    => 101
        ];
    }
}

Как вы видете, этот класс использует метод checkStock() и возвращает массив. То, как данные передаются в этот класс и как возвращаются, полностью зависит от этого API. Невозможно обеспечить совместимость с нашим сервисным классом, так как оба реализуют совершенно разные методы.

Давайте изменим наше приложение, используя «Адаптер».

Реализация паттерна «Адаптер» в Laravel

Что мы, на данный момент, знаем о проверке запасов:

  • Нужно получить товар по артикулу (sku).
  • Количество товара, возвращаемое из базы данных или API, должно быть в правильном формате.

Прежде всего, создадим новый интерфейс с именем StockCheckerInterface, как показано ниже:

namespace App\Interfaces;

interface StockCheckerInterface
{
    public function getStock($sku);
}

Теперь создадим новый класс DatabaseStockCheckAdapter, который реализует интерфейс StockCheckerInterface, как показано ниже.

namespace App\Services;

use App\Product;
use App\Interfaces\StockCheckerInterface;

class DatabaseStockCheckAdapter implements StockCheckerInterface
{
    public  function getStock($sku)
    {
        // Какой-то запрос к базе данных для поиска товара на складе
        $product = Product::whereSku($sku)->first();

        return $product->qty;
    }
}

Следующим шагом создадим новый класс ErpStockCheckAdapter, который также реализует интерфейс StockCheckerInterface и будет использовать сторонний класс Erp для запроса к API, как показано ниже:

namespace App\Services;

use App\Vendor\Erp;
use App\Interfaces\StockCheckerInterface;

class ErpStockCheckAdapter implements StockCheckerInterface
{
    public function getStock($sku)
    {
        // Создаем экземпляра класса
        $erp = new Erp($sku);
        // Делаем запрос для получения запаса через сторонний метод
        $result = $erp->checkStock();
        // Возвращаем количество нужного товара
        return $result['qty'];
    }
}

Теперь у нас есть два класса, реализующих общий интерфейс. Нам нужно привязать к ним интерфейс в AppServiceProvider.

namespace App\Providers;

use App\Services\ErpStockCheckAdapter;
use App\Interfaces\StockCheckerInterface;
use App\Services\DatabaseStockCheckAdapter;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton(StockCheckerInterface::class, function ($app) {
            switch ($app->make('config')->get('services.stock-checker')) {
                case 'database':
                    return new DatabaseStockCheckAdapter;
                case 'erp':
                    return new ErpStockCheckAdapter;
                default:
                    throw new \RuntimeException("Unknown Stock Checker Service");
            }
        });
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

Мы связываем StockCheckerInterface с классами DatabaseStockCheckAdapter и ErpStockCheckAdapter на основе конфигурации.

Настроим конфигурацию в файле config/services.php:

'stock-checker' => 'database',

Настроим контроллер для использования интерфейса StockCheckerInterface.

// StockCheckController.php
use Illuminate\Http\Request;
use App\Interfaces\StockCheckerInterface;

class TestController extends Controller
{
    public function index(Request $request, StockCheckerInterface $stockChecker)
    {
        $sku = $request->input('sku');

        $stock = $stockChecker->getStock($sku);

        return response()->json($stock);
    }
}

Как вы можете видеть в методе контроллера, мы внедряем интерфейс StockCheckerInterface, используя контроль типа, а затем просто вызываем метод getStock() чтобы получить данные в формате JSON.

Если мы в конфигурации изменим значение stock-checker с database на erp, то все равно получим корректные данные.

После внесения всех этих изменений и реализации паттерна «Адаптер» в приложении Laravel:

  • Нам не нужно менять код контроллера.
  • Легко могут быть добавленые новые проверки запасов товара.
  • Все сервисы реализовывают общий интерфейс.

Заключение

Паттерн «Адаптер» также известен как паттерн Обертки, поскольку он оборачивает существующий интерфейс интерфейсом, ожидаемым клиентом.

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

Одним из недостатков этого шаблона является то, что если у вас есть пара класса, реализующих очень много методов, то их будет трудно адаптировать, не сломав сам паттерн. Паттерн должен использоваться всегда, когда у Adaptee и Client есть общая цель.

 

Автор: Larashout
Перево: Demiurge Ash