Шаблон

Шаблоны проектирования необходимы каждому веб-разработчику, поскольку они значительно повышают качество кода. В этой статье будет представлен шаблон “Декоратор”, который используется в TypeScript-проектировании.

В повседневной деятельности TS-разработчика очень полезен компонент logging (журналирования). На стадии проектирования при отладке системных функций компонент logging может предоставить подробную информацию. На этапе запуска он будет записывать текущий процесс, рабочий статус и информацию об исключениях, помогая быстро обнаружить проблемы в системе. Сосредоточимся на том, как с помощью паттерна “Декоратор” элегантно расширить функциональность компонентов logging.

Начнем с определения простого компонента logging:

class DLogger {
log(msg: string): void {
console.log(`DLog: ${msg}`);
}
}

const logger = new DLogger();
logger.log("Hello Decorator!");

В приведенном выше коде определен класс DLogger, который содержит компонентный метод log, используемый для вывода определенной log-информации. После успешного выполнения приведенного выше кода в консоль будет выведено следующее:

DLog: Hello Decorator!

Теперь возникает проблема: для вывода log-сообщений в нижнем регистре необходимо модифицировать метод log класса DLogger.

class DLogger {
log(msg: string): void {
console.log(`DLog: ${msg.toLowerCase()}`);
}
}

Чтобы продолжить изменять выводимую информацию, например добавить несколько звездочек до и после тела сообщения, нужно продолжить модифицировать метод log класса DLogger:

class DLogger {
log(msg: string): void {
console.log(`DLog: ****** ${msg.toLowerCase()} ******`);
}
}

После обновления класса DLogger проверим его функциональность:

const logger = new DLogger();

logger.log("Hello Decorator!");

После успешного выполнения приведенного выше кода в консоли будет выведено следующее:

DLog: ****** hello decorator! ******

Хотя в итоге получена необходимая функциональность, нам пришлось внести 2 изменения в метод log в классе DLogger. Кроме того, был модифицирован оригинальный метод log. А можно ли расширить функциональность метода log без его модификации?

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

Шаблон “Декоратор” заключает в себе 4 основные роли.

  • Компонент: абстрактный интерфейс компонента, определяющий общий интерфейс между объектом-декоратором и декорируемым объектом.
  • Конкретный компонент: класс, соответствующий декорируемому объекту, который реализует абстрактный интерфейс компонента.
  • Декоратор: абстрактный класс-декоратор, содержащий ссылку на конкретный декорируемый объект.
  • Конкретный декоратор: декоратор, реализующий определенные функции (используется для добавления дополнительных функций к декорированному объекту).

Далее поговорим о том, как использовать шаблон “Декоратор” для расширения функциональности оригинального logging-компонента DLogger. Но прежде, чтобы лучше понять приведенный ниже код, взгляните на соответствующую UML-диаграмму.

Сначала определим абстрактный интерфейс компонента в шаблоне “Декоратор”. Поскольку нам нужно разработать компонент logging, определим интерфейс Logger, который обладает методом log для вывода log-информации. Интерфейс Logger представляет собой интерфейсно-ориентированное программирование. Его преимущество в удобном расширении различных log-компонентов.

interface Logger {
log(msg: string): void;
}

Теперь обновим предыдущий класс DLogger, чтобы он реализовал интерфейс Logger, определенный ранее. Этот класс соответствует роли “конкретный компонент” в шаблоне “Декоратор”.

class DLogger implements Logger {
log(msg: string): void {
console.log(`DLog: ${msg}`);
}
}

Экземпляр компонента logging, созданный с помощью класса DLogger, является объектом, который необходимо декорировать. Определим абстрактный класс-декоратор log, хранящий внутри себя ссылку на конкретный декорируемый объект:

abstract class LoggerDecorator implements Logger {
constructor(public logger: Logger) {}
abstract log(msg: string): void;
}

С помощью абстрактного класса LoggerDecorator реализуем декораторы с различными функциями в соответствии с вышеупомянутыми требованиями:

// Декоратор, преобразующий сообщения в нижний регистр
class LowerCaseDecorator extends LoggerDecorator {
constructor(logger: Logger) {
super(logger);
}

log(msg: string): void {
let outputText = msg.toLowerCase();
this.logger.log(outputText);
}
}

// Декоратор, добавляющий звездочку к сообщению
class AddAsterisksDecorator extends LoggerDecorator {
constructor(logger: Logger) {
super(logger);
}
log(msg: string): void {
let outputText = `****** ${msg} ******`;
this.logger.log(outputText);
}
}

Проверим их функциональность декораторов, содержащих определенные функции.

const logger = new DLogger();
const decoratedLogger = new AddAsterisksDecorator(
new LowerCaseDecorator(logger)
);

decoratedLogger.log("Hello Decorator!");

После успешного выполнения приведенного выше кода в консоль будет выведено следующее:

DLog: ****** hello decorator! ******

Из приведенного выше результата видно, что использование шаблона “Декоратор” позволяет легко расширить функцию метода log без его модификации в классе DLogger. В данном примере можно избежать введения абстрактного класса-декоратора. Просто реализуйте интерфейс Logger непосредственно при определении конкретного класса-декоратора:

class LowerCaseDecorator implements Logger {
constructor(public logger: Logger) {}
log(msg: string): void {
let outputText = msg.toLowerCase();
this.logger.log(outputText);
}
}

class AddAsterisksDecorator implements Logger {
constructor(public logger: Logger) {}
log(msg: string): void {
let outputText = `****** ${msg} ******`;
this.logger.log(outputText);
}
}

В дополнение к шаблону “Декоратор” можно использовать функции декоратора, поддерживаемые TypeScript, для достижения той же функциональности. Различают декораторы классов, декораторы свойств, декораторы методов и декораторы параметров.

Чтобы расширить функцию метода log в классе DLogger, нужно использовать декоратор методов. Его тип объявляется следующим образом:

// node_modules/typescript/lib/lib.es5.d.ts
declare type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

interface TypedPropertyDescriptor<T> {
enumerable?: boolean;
configurable?: boolean;
writable?: boolean;
value?: T;
get?: () => T;
set?: (value: T) => void;
}

Как видно из объявления типа MethodDecorator, суть декоратора методов  —  это функция, которая поддерживает три параметра.

  • target  —  объект, который нужно декорировать.
  • propertyKey  —  имя метода, который нужно декорировать.
  • descriptor  —  объект описания свойства.

Для интуитивного понимания параметров создадим декоратор методов LowerCase и выведем значения этих параметров в теле функции декоратора:

function LowerCase(
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(target);
console.log(propertyKey);
console.log(descriptor);
}

С декоратором методов LowerCase можно использовать синтаксис @LowerCase для применения декоратора:

class DLogger implements Logger {
@LowerCase
log(msg: string): void {
console.log(`DLog: ${msg}`);
}
}

const logger = new DLogger();
logger.log("Hello Decorator!")

Как видно на изображении выше, параметр target указывает на объект DLogger.prototype, значение параметра propertyKey равно “log”, а свойство value параметра descriptor указывает на метод log в классе DLogger.

Теперь, понимая суть декоратора методов, определим декораторы методов LowerCase и AddAsterisks:

// Декоратор, преобразующий сообщения в нижний регистр
function LowerCase(
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
let originalMethod = descriptor.value;
descriptor.value = function (msg: string) {
return originalMethod.call(this, msg.toLowerCase());
};
}

// Декоратор, добавляющий звездочку к сообщению
function AddAsterisks(
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
let originalMethod = descriptor.value;
descriptor.value = function (msg: string) {
return originalMethod.call(this, `****** ${msg} ******`);
};
}

Внутри двух вышеприведенных декораторов методов сначала получаем исходный метод с помощью descriptor.value, а затем используем отдельную функцию для замены значения descriptor.value. Проверим функции этих двух декораторов методов:

class DLogger {
@LowerCase
@AddAsterisks
log(msg: string): void {
console.log(`DLog: ${msg}`);
}
}

const logger = new DLogger();
logger.log("Hello Decorator!");

После успешного выполнения приведенного выше кода консоль выдаст следующее:

DLog: ****** hello decorator! ******

Чтобы поддерживать параметры конфигурации, для декораторов методов LowerCase и AddAsterisks необходимо использовать фабрику декораторов.

Наконец, подытожим сказанное, определив сценарии использования шаблона “Декоратор”.

  • Динамическое добавление дополнительной функциональности к объекту без изменения его структуры.
  • Создание комбинации различных моделей поведения для объектов путем компоновки и сочетания различных декораторов.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Bytefer: Design Patterns: Decorator Pattern in TypeScript

Предыдущая статьяKubernetes: установка MicroK8s на локальном компьютере за 5 минут
Следующая статьяВзгляд в будущее: перспективы развития и влияния ИИ на изобразительное искусство и повседневную жизнь