Использование интерфейсов в сторонних пакетах

Источник: «Using interfaces in third-party packages»
Как эффективно использовать интерфейсы, чтобы сделать PHP-пакет более удобным в работе и более настраиваемым, чем когда-либо прежде.

Оглавление

Недавно я работал над Pull Request, чтобы сделать The OG более настраиваемым, и у меня возникли некоторые мысли по поводу использования интерфейсов вместо конкретных классов или перечислений в сторонних пакетах.

Допустим, вы открываете перечисление ColorOption, чтобы другие пользователи могли изменять цвет элемента пользовательского интерфейса.

enum ColorOption: string
{
case Red = 'red';
case Green = 'green';
case Blue = 'blue';
}

function setColor(ColorOption $color)
{
// ...
}

setColor(ColorOption::Red);

По определению, перечисления (появившиеся ещё в PHP 8.1) не являются расширяемыми и делают ваш код довольно жёстким. Это хорошо и, скорее всего, желательно, когда возможности ограничены по своей сути. Но это становится невозможным, если вы используете их для чего-то, что пользователь вашего пакета может захотеть настроить под себя.

Что же произойдёт, если кто-то захочет установить жёлтый цвет? Они не смогут этого сделать, потому что ваш API указывает на перечисление ColorOption! Вы можете решить эту проблему, обратившись к интерфейсу. Давайте разберёмся в этом.

Знакомство с интерфейсом

Начните с определения минимального интерфейса, необходимого для работы функции setColor. Пока что для демонстрации достаточно функции name(). Но может хватить и пустого интерфейса.

interface Color
{
public function name(): string;
}

Вам также нужно обновить функцию setColor, чтобы она принимала этот новый интерфейс Color вместо перечисления ColorOption:

function setColor(Color $color)
{
// ...
}

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

class Red implements Color
{
public function name(): string
{
return 'red';
}
}

class Green implements Color
{
public function name(): string
{
return 'green';
}
}

class Blue implements Color
{
public function name(): string
{
return 'blue';
}
}

Теперь вы и потребители пакета можете передать в функцию экземпляр любого класса, реализующего этот интерфейс:

setColor(new Red());
setColor(new Green());
setColor(new Blue());

Тот, кто использует ваш пакет, может написать собственный класс Yellow для использования в своём коде:

class Yellow implements Color
{
public function name(): string
{
return 'yellow';
}
}

setColor(new Yellow());

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

$blue = new Blue($opacity);

setColor($blue);

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

Сохранение перечисления

Есть способ продолжить использование перечисления, чтобы облегчить потребителям выбор из встроенных опций, не жертвуя при этом возможностью кастомизации. Вы можете позволить своему перечислению реализовать интерфейс Color. До недавнего времени я даже не знал, что такое возможно! Считайте, что у нас есть тот же интерфейс Color, что и выше:

enum ColorOption: string implements Color
{
case Red = 'red';
case Blue = 'blue';
case Green = 'green';

public function name(): string
{
return $this->value;
}
}

После этого пользователи смогут использовать ваши опции так же, как и раньше:

setColor(ColorOption::Red);
setColor(ColorOption::Green);
setColor(ColorOption::Blue);

Это позволяет по-прежнему использовать интерфейс и предоставлять пользователю предопределённый набор значений под вашим контролем — централизованный в перечислении — и позволяет потребителю настраивать его по своему усмотрению:

// С собственным классом "yellow":
class Yellow implements Color
{
public function name(): string
{
return 'yellow';
}
}

setColor(new Yellow());

// Или с собственным перечислением:
enum CustomColor: string implements Color
{
case Cyan = 'cyan';
case Magenta = 'magenta';
case Yellow = 'yellow';

public function name(): string
{
return $this->value;
}
}

setColor(CustomColor::Cyan);
setColor(CustomColor::Magenta);
setColor(CustomColor::Yellow);

Один из недостатков этого метода заключается в том, что перечисления в PHP не могут иметь состояния. Это означает, что они не могут иметь свойств и других динамических значений. Это вполне логично, но стоит иметь в виду, если вы решите использовать этот подход.

Заключение

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

В конечном счёте, использование интерфейса вместо конкретного класса или перечисления всегда предпочтительнее, так как это позволяет сохранить связь между компонентами, но при этом расширить функциональность.

Дополнительные материалы

Предыдущая Статья

Улучшение UX форм с CSS свойством field-sizing

Следующая Статья

Автоматическое хэширование значений моделей кастом "Hashed"