Среди всего прочего в 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".