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

Для решения задачи добавления оборудования, такого как серверы, в Private Network наша команда производственно-технического обеспечения использует класс под названием AddEquipmentService. Изначально мы поддерживали только серверы, но позже добавили поддержку колокации (Colocation), а за последние несколько месяцев добавили поддержку еще нескольких типов оборудования. Приоритетом для нашей команды является как можно более быстрая доставка решений для бизнеса, поэтому мы продолжали использовать и расширять тот же класс.

Поддерживаемые в настоящее время типы оборудования включают выделенные серверы (Dedicated Servers), выделенные серверные стойки (Dedicated Racks), колокацию, эластичные вычисления (Elastic Compute) и облако (Cloud). Со временем этот класс стал очень большим и сложным в обслуживании, так как он поддерживает сразу несколько типов оборудования, а каждый тип имеет свою бизнес-логику. Чтобы вернуть хоть какую-нибудь степень обслуживаемости нашему коду и сделать поддержку новых типов оборудования, таких как хранилища данных (Dedicated Storage), брандмауэры (Firewalls) и балансировщики нагрузки (Load Balancers), не такой болезненной, мы решили провести рефакторинг кода с использованием сервис-тегов (фичей фреймворка Symfony) в сочетании с паттерном проектирования программного обеспечения стратегия.

В рамках этого рефакторинга мы создали интерфейс с двумя методами: addEquipment и support. Метод addEquipment используется для добавления оборудования в частную сеть клиента, в то время как метод support проверяет, подходит ли данный тип оборудования для конкретного класса, реализующего интерфейс.

<?php

namespace App\Service;

use App\Entity\PrivateNetwork;
use App\Entity\PrivateNetworkEquipment;
use App\Service\EquipmentService\Equipment;

interface AddEquipmentInterface
{
    public function addEquipment(PrivateNetworkEquipment $privateNetworkEquipment, PrivateNetwork $privateNetwork, Equipment $equipment): PrivateNetworkEquipment;

    public function supports(string $equipmentType): bool;
}

Мы также создали класс-сервис (служебный класс) для каждого поддерживаемого типа оборудования, такого как выделенные серверы, выделенные серверные стойки, коллокации, эластичные вычисления и облако, о которых мы уже упоминали выше. Каждый класс-сервис реализует AddEquipmentInterface и содержит логику для добавления определенного типа оборудования в частную сеть.

<?php

// Класс-сервис, который мы создали для поддержки сервера

namespace App\Service;

use App\Entity\PrivateNetwork;
use App\Entity\PrivateNetworkEquipment;
use Doctrine\ORM\EntityManagerInterface;
use App\Processor\QueueProcessor;
use App\Service\EquipmentService\Equipment;
use Psr\Log\LoggerInterface;

class AddServerToPrivateNetworkService implements AddEquipmentInterface
{
    protected $logger;
    protected $queueProcessor;
    protected $om;

    public function __construct(LoggerInterface $logger, QueueProcessor $queueProcessor, EntityManagerInterface $om)
    {
        $this->logger = $logger;
        $this->queueProcessor = $queueProcessor;
        $this->om = $om;
    }

    public function addEquipment(PrivateNetworkEquipment $privateNetworkEquipment, PrivateNetwork $privateNetwork, Equipment $server)
    {
        // Логика добавления сервера в частную сеть...
    }

    public function supports(string $equipmentType): bool
    {
        return PrivateNetworkEquipment::EQUIPMENT_TYPE_DEDICATED_SERVER === $equipmentType;
    }
}
<?php

// Класс-сервис, который мы создали для поддержки колокации 

namespace App\Service;

use App\Entity\PrivateNetwork;
use App\Entity\PrivateNetworkEquipment;
use Doctrine\ORM\EntityManagerInterface;
use App\Processor\QueueProcessor;
use App\Service\EquipmentService\Equipment;
use Psr\Log\LoggerInterface;

class AddColocationToPrivateNetworkService implements AddEquipmentInterface
{
    protected $logger;
    protected $queueProcessor;
    protected $om;

    public function __construct(LoggerInterface $logger, QueueProcessor $queueProcessor, EntityManagerInterface $om)
    {
        $this->logger = $logger;
        $this->queueProcessor = $queueProcessor;
        $this->om = $om;
    }

    public function addEquipment(PrivateNetworkEquipment $privateNetworkEquipment, PrivateNetwork $privateNetwork, Equipment $colocation)
    {
        // Логика добавления колокации в частную сеть...
    }

    public function supports(string $equipmentType): bool
    {
        return PrivateNetworkEquipment::EQUIPMENT_TYPE_COLOCATION === $equipmentType;
    }
}

Тегирование интерфейса в services.yaml

Все классы-сервисы, которые являются инстансами AddEquipmentInterface будут протегированы автоматически, что на практике означает, что классы, которые мы определили выше, могут быть извлечены из сервис-контейнера (Service Container) без предварительного связывания их вручную.

services:
  _instanceof:
    App\Service\AddEquipmentInterface:
      tags: [ 'app.add_equipment_interface' ]

Внедрение сервиса в класс контекста

Далее мы создадим класс контекста AddEquipmentService. Нам нужно будет перебрать множество сервисов, чтобы найти, какие из них подходят для добавления оборудования в частную сеть.

Мы можем легко внедрить эти сервисы в конструктор класса, указав в services.yaml имя переменной и тег интерфейса, который мы установили ранее.

App\Service\AddEquipmentService:
    arguments:
      $addEquipmentServices: !tagged_iterator app.add_equipment_interface

Создание класса контекста

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

<?php

namespace App\Service;

use App\Entity\PrivateNetwork;
use App\Entity\PrivateNetworkEquipment;
use App\Exception\BadRequestException;
use App\Service\EquipmentService\Equipment;
use Psr\Log\LoggerInterface;

class AddEquipmentService
{
    /**
     * @var AddEquipmentInterface[]
     */
    public iterable $addEquipmentServices;

    protected $auditLogger;

    public function __construct(LoggerInterface $auditLogger, iterable $addEquipmentServices)
    {
        $this->auditLogger = $auditLogger;
        $this->addEquipmentServices = $addEquipmentServices;
    }

    public function addEquipment(PrivateNetworkEquipment $privateNetworkEquipment, PrivateNetwork $privateNetwork, Equipment $equipment, int $desiredLinkSpeed, bool $allServers = false): PrivateNetworkEquipment
    {
        foreach ($this->addEquipmentServices as $addEquipmentService) {
            if ($addEquipmentService->supports($equipment->getType())) {
                $privateNetworkEquipment = $addEquipmentService->addEquipment($privateNetworkEquipment, $privateNetwork, $equipment);

                $this->auditLogger->info('private_network_equipment_added', [
                    'customerId' => $privateNetwork->getCustomerId(),
                    'equipmentId' => $privateNetworkEquipment->getEquipmentId(),
                    'equipmentType' => $privateNetworkEquipment->getEquipmentType(),
                ]);

                return $privateNetworkEquipment;
            }
        }

        throw new BadRequestException("Equipment {$equipment->getType()} is not supported");
    }
}

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

Заключение

Структура каталогов

Рефакторинг класса AddEquipmentService позволил команде разработчиков сделать код более удобным для сопровождения и легким в добавлении поддержки новых типов оборудования. Мы можем просто создать новый сервис-класс, реализующий addEquipmentInterface. Добавление контекстного класса AddEquipmentService позволило находить правильный сервис-класс для использования и обработки служебных функций среди доступных типов оборудования. В целом, этот рефакторинг, вероятно, приведет к более легкому  и эффективному процессу добавления оборудования в частную сеть.


Приглашаем всех желающих на открытое занятие «Мониторинг исключений с помощью Graphite и Grafana». Что участников ожидает на уроке:

  • сбор статистики с помощью Graphite,

  • построение графиков и настройка алертов в Grafana,

  • как обрабатывать исключения, которые не являются ошибками

Регистрация на вебинар открыта на странице онлайн-курса "Symfony Framework".

Комментарии (0)