082 Вводное видео
Декораторы используются для некоторого аннотирования классов, методов, свойств, или параметров, чтобы использовать некоторое метапрограммирование.
Декораторы - это функции, которые оборачивают нужный нам функционал и обогащают логику работы программы (или данного функционала)
083 Паттерн декоратора
Паттерн декоратора - это определённая методология, которая предлагает нам оборачивать методы, классы, свойства и параметры в функции, которые модифицируют поведение этих объектов. При этом можно производить композицию данных декораторов. Сама функциональность декораторов в языке просто позволяет удобнее использовать данный паттерн.
Паттерн представляет собой использование функции, в которую вкладывается тип объекта (в примере - интерфейс) и ретёрнется этот же объект, но модифицированный.
По сути своей, работаем извне мы уже над модифицированным объектом
interface IUserService {
users: number;
getUsersInDatabase(): number;
}
class UserService implements IUserService {
public users: number = 1000;
getUsersInDatabase(): number {
return this.users;
}
}
function nullUser(obj: IUserService) {
obj.users = 0; // Какая-то логика...
return obj;
}
// Вывод обычного инстанса
console.log(new UserService().getUsersInDatabase());
// Вывод задекорированного инстанса
console.log(nullUser(new UserService()).getUsersInDatabase());
А вот и объяснение, почему декораторы на классах работают снизу вверх: верхний декоратор покрывает выполнение нижнего
function logUser(obj: IUserService) {
console.log('Users: ' + obj.users);
return obj;
}
console.log(new UserService().getUsersInDatabase());
console.log(nullUser(new UserService()).getUsersInDatabase());
console.log(logUser(nullUser(new UserService())).getUsersInDatabase()); // 'Users: 0'
Декораторы, в своей роли, выступают некоторым синтаксическим сахаром, который позволяет более удобно реализовывать данный паттерн
084 Декоратор класса
Чтобы начать использовать декораторы, нужно включить их в ТС
- Сама функция декоратора принимает в себя таргет, типом которого является функция.
- Чтобы изменить значения объекта, нужно обратиться к нему через прототип (потому что таргет - это функция)
- Декораторы используются до того, как создастся объект, поэтому если внутри класса будет присвоение числа, то оно сохранится и не изменится декоратором. Поэтому все изменения и присвоения значений должны происходить внутри декоратора
@nullUser // Объявление декоратора
class UserService implements IUserService {
public users: number = 1000;
getUsersInDatabase(): number {
return this.users;
}
}
function nullUser(target: Function) { // Функция декоратора
target.prototype.users = 0; // Изменение значения
}
// Результат = 1000
console.log(new UserService().getUsersInDatabase());
@nullUser // Объявление декоратора
class UserService implements IUserService {
public users: number;
getUsersInDatabase(): number {
return this.users;
}
}
function nullUser(target: Function) { // Функция декоратора
target.prototype.users = 0; // Изменение значения
}
// Результат = 0
console.log(new UserService().getUsersInDatabase());
Так же у нас есть второй вариант создания функции-декоратора - это объявление конструируемого класса (дженерик T) и возвращение анонимного класса, который расширяется от получаемого класса Этот вариант имеет больший приоритет над первым вариантом и оригинальным объектом. Тут мы непосредственно работаем с переменными декорируемого класса и эти изменения будут задаваться даже поверх тех значений, что находятся внутри класса (в отличие от первого способа, где приоритет оригинального класса выше изменения прототипов функции)
@numUserAdvanced
class UserService implements IUserService {
public users: number = 1000;
getUsersInDatabase(): number {
return this.users;
}
}
function numUserAdvanced<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
users = 3;
}
}
// Результат = 3
console.log(new UserService().getUsersInDatabase());
085 Фабрика декораторов
Фабрика декораторов - это функция, которая производит нужные нам декораторы.
@setUsers(2) // !
class UserService implements IUserService {
public users: number;
getUsersInDatabase(): number {
return this.users;
}
}
function setUsers(users: number) { // !
// стрелочная функция, чтобы не терять контекст
return (target: Function) => {
target.prototype.users = 0;
}
}
console.log(new UserService().getUsersInDatabase()); // = 2
Так же в фабрику мы можем превратить нашу функцию для конструирования класса
// Обычный декоратор
function numUserAdvanced<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
users = 3;
}
}
// Фабрика декораторов
function setNumUsersAdvanced(users: number) {
return <T extends {new(...args: any[]):{}}> (constructor: T) => {
return class extends constructor {
users = users;
}
}
}
@setNumUsersAdvanced(4) // !
class UserService implements IUserService {
public users: number = 1000; // !
getUsersInDatabase(): number {
return this.users;
}
}
function setNumUsersAdvanced(users: number) { // !
return <T extends {new(...args: any[]):{}}>(constructor: T) => {
return class extends constructor {
users = users;
}
}
}
// Результат 4
console.log(new UserService().getUsersInDatabase());
И тут стоит упомянуть, в каком порядке выполняются декораторы. Порядок инициализации - прямой, а уже исполнение происходит в обратном порядке.
@setUsers(2)
@log()
class UserService implements IUserService {
public users: number = 1000;
getUsersInDatabase(): number {
return this.users;
}
}
function setUsers(users: number) {
console.log('setUsers init');
return (target: Function) => {
console.log('setUsers run');
target.prototype.users = 0;
}
}
function log() {
console.log('log init');
return (target: Function) => {
console.log('log run');
console.log(target);
}
}
086 Упражнение - Декоратор CreatedAt
Нам нужно создать декоратор CreatedAt
который будет добавлять в класс дату создания его инстанса
interface IUserService {
users: number;
getUsersInDatabase(): number;
}
@CreatedAt
class UserService implements IUserService {
users: number;
getUsersInDatabase(): number {
return this.users;
}
}
function CreatedAt<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
createdAt: Date = new Date();
}
}
// Выведет количество пользователей и дату
console.log((new UserService());
Однако мы можем встретиться с такой проблемой, что обратиться непосредственно к сгенерированному свойству или методу у нас не получится извне (так как ТС ориентируется ещё и на рантайм-код)
Поэтому, чтобы решить проблему с недоступными извне свойствами, нам нужно создать отдельный тайп, который будет явно указывать, что у нас есть нужное нам свойство или метод в инстансе класса
interface IUserService {
users: number;
getUsersInDatabase(): number;
}
type CreatedAt = { // !
createdAt: Date;
}
@CreatedAt
class UserService implements IUserService {
users: number;
getUsersInDatabase(): number {
return this.users;
}
}
function CreatedAt<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
createdAt: Date = new Date();
}
}
// !
console.log((new UserService() as IUserService & CreatedAt).createdAt); // OK
087 Декоратор метода
Структура декоратора метода выглядит таким образом:
- Сначала принимает в себя метод
- Потом наименование метода
- Потом дескриптор метода (который хранит всю важную для нас информацию)
- Далее возвращаем дескриптор или войд от декоратора
- И уже дальше можем прописать дополнительную логику, которая должна выполняться до самого метода (выполнится декоратор потом метод)
class UserService implements IUserService {
public users: number = 1000;
@Log
getUsersInDatabase(): number {
return this.users;
}
}
function Log(
// Объект, к которому относится метод
target: Object,
// Название метода
propertyKey: string | symbol,
// Дескриптор, который принимает в себя функцию
descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
// Вот так выглядит корректная типизация декоратора метода:
): TypedPropertyDescriptor<(...args: any[]) => any> | void {
console.log(target);
console.log(propertyKey);
console.log(descriptor);
}
Что мы можем менять через дескриптор? Мы можем задать модификаторы для метода: его конфигурируемость, читабелность, поменять значения, задать get
/set
Так же мы можем менять логику выполнения функции. Через обращение к значению дескриптора, мы можем задать новое значение для нашей функции и логика выполнения этой функции так же поменяется.
На примере можно увидеть, что теперь вместо Error
функция выдаёт 'no error'
class UserService implements IUserService {
public users: number = 1000;
@Log
getUsersInDatabase(): number {
throw new Error('Ошибка'); // ! Выкинет ошибку
}
}
function Log(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
): TypedPropertyDescriptor<(...args: any[]) => any> | void {
console.log(target);
console.log(propertyKey);
console.log(descriptor);
descriptor.value = () => {
console.log('no error'); // ! Заменим функцию на другую
}
}
Мы можем не только переопределять значения функции, но и дополнять их. Можно заранее сохранить значение дескриптора и вызвать его во время изменения, чтобы дополнить логику нашего метода (а не перетереть её)
function Log(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
): TypedPropertyDescriptor<(...args: any[]) => any> | void {
console.log(target);
console.log(propertyKey);
console.log(descriptor);
const oldValue = descriptor.value; // !
descriptor.value = () => {
oldValue(); // !
console.log('no error'); // ! Дополним функцию
}
}
Так же ничто нам не мешает сделать фэктори декоратор и для метода вот таким вот образом:
class UserService implements IUserService {
public users: number = 1000;
@Log()
getUsersInDatabase(): number {
throw new Error('Ошибка');
}
}
// Factoory Decorator
function Log() {
return (
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
): TypedPropertyDescriptor<(...args: any[]) => any> | void => {
console.log(target);
console.log(propertyKey);
console.log(descriptor);
}
}
088 Упражнение - Декоратор перехвата ошибок
Нам нужно создать декоратор, который будет обрабатывать ошибку и выводить её в зависимости от значения аргумента rethrow
interface IUserService {
users: number;
getUsersInDatabase(): number;
}
class UserService implements IUserService {
users: number;
@Catch({rethrow: true})
getUsersInDatabase(): number {
throw new Error('Ошибка'); // Выводим ошибку
}
}
function Catch({ rethrow }: { rethrow: boolean } = {rethrow: false}) {
return (
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
): TypedPropertyDescriptor<(...args: any[]) => any> | void => {
// Сохраняем оригинальный метод
const method = descriptor.value;
// Пишем логику для нового метода
descriptor.value = async (...args: any[]) => {
try { // Если ошибкинет, то
// Вызываем срабатывание оригинального метода
return await method?.apply(target, args);
} catch(e) { // Если ошибка есть, то
if (e instanceof Error) { // Если е это Error, то
console.log(e.message); // Выводим сообщение
// Если rethrow=true, то выводим ошибку
if (rethrow) {
throw e; // Выводим существующую ошибку
}
}
}
}
}
}
И тут сразу хочется отметить. что если мы оставим async
/await
, то у нас в выводе будет фигурировать Promise
089 Декоратор свойства
Декораторы свойств уже представляют из себя функции, которые позволят нам валидировать входящие значения, вводить новые ограничения для свойств, назначать свои геттеры и сеттеры и в принципе дополнять логику для обычных свойств таким образом, чтобы мы могли явно регулировать их.
Декоратор свойства отличается от декоратора метода только тем, что в него мы уже не передаём дескрипторы (их нужно будет редактировать через defineProperty
).
Цель такая: нам нужно написать декоратор свойства, который задаст максимальное значение для свойства = 100
interface IUserService {
users: number;
getUsersInDatabase(): number;
}
class UserService implements IUserService {
@Max(100) // !
public users: number = 1000; // !
getUsersInDatabase(): number {
throw new Error('Ошибка');
}
}
function Max(max: number) {
return (
target: Object, // UserService
propertyKey: string | symbol // users
) => {
}
}
Первым делом, нам нужно описать геттеры и сеттеры для свойства, чтобы была возможность регулировать задание нового значения и получение
function Max(max: number) {
return ( target: Object, propertyKey: string | symbol ) => {
let value: number;
const setter = function (newValue: number): void {
if (newValue > max) {
console.log(`Число не может быть больше ${max}`);
} else {
value = newValue;
}
}
const getter = function (): number {
return value;
}
}
}
Так же мы можем через defineProperty поменять дескрипторы для свойств и заменить стоковые методы (get
/set
) и модификаторы (читабельность, изменяемость и так далее) для этих свойств
function Max(max: number) {
return ( target: Object, propertyKey: string | symbol ) => {
let value: number;
const setter = function (newValue: number): void {
if (newValue > max) {
console.log(`Число не может быть больше ${max}`);
} else {
value = newValue;
}
}
const getter = function (): number {
return value;
}
// ! Переопределение геттеров и сеттеров для свойства
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter
});
}
}
Теперь мы спокойно можем пользоваться нужным нам функционалом и значение больше 100 задать не сможем - получим предупреждение
const userService = new UserService(); // Error потому что изначально стоит 1000
userService.users = 1;
console.log(userService.users); // 1
userService.users = 101;
console.log(userService.users); // error
090 Декоратор accessor
Декоратор accessor используется не так часто, как другие декораторы. Конкретно он покрывает выполнение геттеров и сеттеров. Нам достаточно написать данный метод один раз над одним аццессором и он будет покрывать сразу оба (на оба навешивать не надо - это будет ошибкой)
interface IUserService {
getUsersInDatabase(): number;
}
class UserService implements IUserService {
private _users: number = 1000;
@Log()
set users(users: number) {
this._users = users;
}
get users() {
return this._users;
}
}
function Log() {
return (
target: Object, // UserService
_: string | symbol, // users
descriptor: PropertyDescriptor //
) => {
}
}
Дескриптор хранит ту же самую информацию, что и дескриптор для методов
И конкретно тут мы описали логику модификации сеттера (геттер работает по прежнему)
set?.apply(target, args)
- применяем сеттер на таргете (нашем классе) и вкладываем в качестве аргумента принимаемые значения
interface IUserService {
getUsersInDatabase(): number;
}
class UserService implements IUserService {
private _users: number = 1000;
@Log()
set users(users: number) {
this._users = users;
}
get users() {
return this._users;
}
getUsersInDatabase(): number {
throw new Error('Ошибка');
}
}
function Log() {
return (
target: Object, // UserService
_: string | symbol, // users
descriptor: PropertyDescriptor //
) => {
// Сохраняем старый сеттер
const set = descriptor.set;
// Переопределим сеттер, добавив ему логику
descriptor.set = (...args: any) => {
console.log(args); // [ 1 ]
set?.apply(target, args); // !
}
}
}
const userService = new UserService();
userService.users = 10;
console.log(userService.users); // 1
091 Декоратор параметра
Декораторы параметров зачастую используются для метапрограммирования
interface IUserService {
getUsersInDatabase(): number;
}
class UserService implements IUserService {
private _users: number;
getUsersInDatabase(): number {
return this._users;
}
setUsersInDatabase(
@Positive() users: number,
@Positive() __: number
): void {
this._users = users;
}
}
function Positive() {
return (
target: Object, // UserService - класс
propertyKey: string | symbol, // setUsersInDatabase - метод
parameterIndex: number, // индекс среди аргументов
) => {
console.log(target);
console.log(propertyKey);
console.log(parameterIndex);
}
}
const userService = new UserService();
092 Метаданные
Для начала нам нужно активировать метаданные в ТС, и уже затем после компиляции мы будем получать нативный JS с метаданными, которые б
И далее нужно установить саму библиотеку для работы с метаданными
npm init
npm i reflect-metadata
И через Reflection
мы получаем возможность использовать методы для обработки метаданных. Метод getOwnMetadata()
принимает в себя три аргумента: тип запрашиваемых метаданных, класс и наименование метода внутри этого класса. Типы метаданных мы можем найти в документации.
Используется для валидации типов или для связывания компонентов (dependency injection)
import 'reflect-metadata'; // !
interface IUserService {
getUsersInDatabase(): number;
}
class UserService implements IUserService {
private _users: number;
getUsersInDatabase(): number {
return this._users;
}
setUsersInDatabase(@Positive() users: number): void {
this._users = users;
}
}
// MetaProgramming
function Positive() {
return (
target: Object, // UserService - класс
propertyKey: string | symbol, // setUsersInDatabase - метод
parameterIndex: number, // 0 - первый среди аргументов
) => {
// !
console.log(Reflect.getOwnMetadata('design:type', target, propertyKey));
console.log(Reflect.getOwnMetadata('design:paramtypes', target, propertyKey));
console.log(Reflect.getOwnMetadata('design:returntype', target, propertyKey));
}
}
//
const userService = new UserService();
console.log(userService);
Метод Positive
навешивает в метаданные каждого аргумента метку, что значение позитивно и оно нам подходит
function Positive() {
return (
target: Object, // UserService - класс
propertyKey: string | symbol, // setUsersInDatabase - метод
parameterIndex: number, // 0 - первый среди аргументов
) => {
// Добавляем символ позитивности в метаданных для аргументов
let existParams: number[] = Reflect.getOwnMetadata(POSITIVE_METADATA_KEY, target, propertyKey) || [];
existParams.push(parameterIndex);
Reflect.defineMetadata(POSITIVE_METADATA_KEY, existParams, target, propertyKey)
}
}
И тут уже показана реализация валидации аргументов метода. Сначала мы присваиваем марку валидируемости для одного значения, а уже через Validate
делаем общую проверку валидных значений
import 'reflect-metadata';
const POSITIVE_METADATA_KEY = Symbol('POSITIVE_METADATA_KEY');
interface IUserService {
getUsersInDatabase(): number;
}
class UserService implements IUserService {
private _users: number;
getUsersInDatabase(): number {
return this._users;
}
@Validate() // !
setUsersInDatabase(@Positive() users: number): void { // !
this._users = users;
}
}
function Positive() { // 1
return (
target: Object, // UserService - класс
propertyKey: string | symbol, // setUsersInDatabase - метод
parameterIndex: number, // 0 - первый среди аргументов
) => {
console.log(Reflect.getOwnMetadata('design:type', target, propertyKey));
console.log(Reflect.getOwnMetadata('design:paramtypes', target, propertyKey));
console.log(Reflect.getOwnMetadata('design:returntype', target, propertyKey));
// Добавляем символ позитивности в метаданных для аргументов
let existParams: number[] = Reflect.getOwnMetadata(POSITIVE_METADATA_KEY, target, propertyKey) || [];
existParams.push(parameterIndex);
Reflect.defineMetadata(POSITIVE_METADATA_KEY, existParams, target, propertyKey)
}
}
function Validate() { // !
return (
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
) => {
// Уже тут валидируем, помеченные как позитивные, данные
let method = descriptor.value;
descriptor.value = (...args: any) => {
let positiveParams: number[] = Reflect.getOwnMetadata(POSITIVE_METADATA_KEY, target, propertyKey) || [];
if (positiveParams) {
for (let index of positiveParams) {
if (args[index] < 0 ) {
throw new Error('Number must be bigger then 0');
}
}
}
return method?.apply(this, args);
} }
}
const userService = new UserService();
console.log(userService.setUsersInDatabase(10));
console.log(userService.setUsersInDatabase(-1));
093 Порядок декораторов
- Первыми у нас всегда срабатывают свойства (вне зависимости от положения в коде).
- Дальше срабатывает инициализация метода и его параметров. А уже вызов происходит в обратном порядке.
- Дальше происходит тот же самый порядок со статиками.
- И уже в конце инициализируется класс с его параметром. В обратном порядке они выполняются
Порядок расположения кода влияет только на одинаковые уровни инициализации - они выполняются по порядку в коде
function Uni(name: string): any {
console.log(`Инициализация: ${name}`);
return function () {
console.log(`Вызов: ${name}`);
}
}
@Uni('Класс')
class MyClass {
@Uni('Свойство')
props?: any;
@Uni('Статическое свойство')
static prop?: any;
@Uni('Метод')
method(@Uni('Параметр') _: string) { }
@Uni('Статический метод')
static methodStatic(@Uni('Параметр статического метода') _: string) { }
constructor(@Uni('Параметр конструктора') _: string) {
}
}