Я реализовал функцию декоратора класса TypeScript, которая позволяет мне украшать однотипные классы. Если Decorator присутствует над каким-либо классом, этот класс должен просмотреть сохраненные значения Decorator для определения класса с наивысшим приоритетом такого типа. Если недавно декорированный класс имеет меньший приоритет по сравнению с сохраненным в данный момент классом с наивысшим приоритетом, этот новый класс должен стать этим сохраненным классом.
Другими словами, все украшенные классы того или иного типа должны стать такими же, как класс с наивысшим приоритетом.
Допустим, у меня есть GeneralClassA (приоритет 0),SpecificClassA(приоритет 1) и ClassA(приоритет 0). Все классы должны указывать на один и тот же класс, а именно наSpecificClassA, поскольку он имеет наивысший приоритет.
План состоит в том, чтобы общие классы уже были предопределены и оформлены, но позволили разработчику определить конкретный класс и оформить его как класс с более высоким приоритетом.
Все работает правильно, но проблема в том, что декорированный класс с наивысшим приоритетом должен быть объявлен ПЕРЕД всеми остальными декорированными классами. В противном случае декорированные классы, объявленные ДО класса с наивысшим приоритетом, не будут переопределены.
Есть ли способ задним числом переопределить все декорированные классы (я могу сохранить их конструкторы), как только класс с более высоким приоритетом будет объявлен и оформлен? Или как-то перед всем объявить все конкретные классы?
Я повторил свою проблему здесь: StackBlitz
Я попытался сохранить все мои декорированные классы, и как только класс с более высоким приоритетом был декорирован, я попытался Object.assign один конструктор и прототип другому. Это не сработало, а даже если бы и сработало, что, если бы я где-то уже использовал класс с более низким приоритетом еще до того, как объявил класс с более высоким приоритетом (объявление класса происходит только тогда, когда класс где-то используется, при импорте).
Я хотел бы получить предложение, как инициализировать (объявить) все конкретные классы перед всем, возможно, даже до того, как файл main.js когда-либо будет выполнен.
Рассматриваемый декоратор:
export const YIELD_LIST: {
[name: string]: { ctor: any; priority: number };
} = {};
type Constructable<T> = new (...args: any[]) => T;
export function Yield<T extends Constructable<any>>(
name: string,
priority: number = 0
) {
return (ctor: T) => {
if (!YIELD_LIST[name]) {
YIELD_LIST[name] = { ctor, priority };
} else {
if (YIELD_LIST[name].priority <= priority) {
YIELD_LIST[name] = { ctor, priority };
}
}
const maxPriorityCtor = YIELD_LIST[name].ctor as T;
return class extends maxPriorityCtor {
constructor(...args: any[]) {
super(...args);
}
};
};
}
Использование:
@Yield('CLASS_A', 1)
export class SpecificClassA extends GeneralClassA {
public override value = 10;
}
Я отредактировал свой вопрос, был добавлен код декоратора.
«Если недавно декорированный класс имеет меньший приоритет по сравнению с сохраненным в данный момент классом с наивысшим приоритетом, этот новый класс должен стать этим сохраненным классом». - правильно ли я понимаю, что вы хотите, чтобы декоратор фактически заменял объявляемый класс (каким-то другим, хранимым классом), а не просто регистрировал объявляемый класс?
Да. Я хочу полностью заменить этот класс, поэтому декоратор возвращает новый класс, расширенный из сохраненного конструктора класса с максимальным приоритетом.
Это довольно странно и, вероятно, вызовет много сюрпризов. Или вы хотите иметь только один предопределенный резервный вариант и, возможно, одно переопределение, указанное разработчиком, для каждого имени? Тогда вам, вероятно, не следует использовать систему с произвольным приоритетом.
Где (и когда) используются эти классы? Как в целом обеспечить загрузку всех классов с декораторами перед использованием реестра?
Планируется использовать его в нескольких игровых средах. Я хочу определить некоторые базовые функции в общей части игрового «движка», а затем добавить определенные функции к некоторым из этих классов движка, не меняя эти классы напрямую. Это внедрение зависимостей без инжектора и вызовов «provideClass» — все автоматически на основе декоратора.
Я бы настоятельно рекомендовал не смешивать «provideClass» с функциональностью «useClass». Также я до сих пор не понимаю, зачем для этого нужна сложная система приоритетов.
«Или вы хотите иметь только один предопределенный резервный вариант и, возможно, одно переопределение, указанное разработчиком, для каждого имени?» Да. Один базовый класс и, возможно, одно переопределение, указанное разработчиком. Например, я бы получил класс «Speech» и производный класс «MaleSpeech», оба с приоритетом по умолчанию 0. Когда разработчик создает новую игру, он может реализовать класс «FemaleSpeech» и украсить его приоритетом 1. Теперь для этой игры , вместо этого движок будет использовать этот новый класс. Для меня DI — гораздо более сложная система, и поэтому я пытаюсь добиться простоты.
На самом деле для простоты вам следует отказаться от этого приоритета. Пусть разработчик напишет только import { Speech, Yield } from 'engine'; @Yield('Speech') class FemaleSpeech extends Speech { … }. Затем, когда вашему движку понадобится речевой экземпляр, он может проверить, зарегистрировал ли разработчик для этого собственный класс, или вместо этого вернуться к MaleSpeech.
Вам не нужна никакая логика, чтобы @Yield('Speech', 0.5) class FemaleSpeech { … } автоматически выводил Speech или возвращал MaleSpeech, если приоритет был слишком низким.
Я это понимаю, мне нравится идея не иметь приоритета и просто прямо сказать, что вам нужно переопределить. Я мог бы даже использовать в качестве параметра сам класс вместо строки «Речь». Единственная проблема заключается в том, что я не хочу усложнять задачу наличием создателя экземпляра - я хочу, чтобы «new Speech()» работал как обычно, а внутренние элементы класса Speech на самом деле были FemaleSpeech или MaleSpeech.
Вы не можете использовать Speech как фабрику, производящую экземпляр определенного класса, и как суперкласс конкретных реализаций. Это даже не имеет ничего общего с внедрением зависимостей, оно также не будет работать с обычно определенными классами. Может быть, наследование в любом случае является неправильным выбором? Но на самом деле я думаю, что нет ничего плохого в том, чтобы иметь отдельного «создателя экземпляров». Вы все еще можете написать это как const MySpeech = instanceMaker('speech'); new MySpeech().
Я понимаю, что не могу переопределить суперкласс. Это заставило бы класс быть производным от самого себя. Проверьте комментарии в примере StackBlitz: меня устраивает отсутствие переопределения суперклассов, и это поведение по умолчанию, поскольку этот суперкласс всегда будет определен первым, перед производным классом, и он никогда даже не будет рассматриваться. для переопределения. В данном примере базовым классом будет класс MaleSpeech.
Прошу прощения за спам и за замешательство, которое я вызвал. Я придумал способ сначала принудительно объявить класс Yield, что также вынуждает разработчика использовать декоратор Yield только одним и предполагаемым способом. Если вам интересно, проверьте это на StackBlitz. Кроме того, спасибо за ваш вклад, это заставило меня больше задуматься о простоте использования!






Я заставил его работать так, как задумано, с помощью некоторой логики проверки декоратора. Разработчик должен определить все ожидаемые классы переопределения, и все эти классы должны быть установлены как переопределения, чтобы код мог работать. Переопределенный и переопределенный классы должны быть оформлены с использованием функций декоратора Yield и Yieldable соответственно.
export const YIELD_LIST: {
[token: string]: { overrideCtor: any };
} = {};
const EXPECTED_YIELDERS: string[] = [];
const DEFINED_YIELDERS: string[] = [];
function checkYielders() {
if (!DEFINED_YIELDERS.every((v) => EXPECTED_YIELDERS.includes(v))) {
throw new Error(
'Unexpected Yielder was defined. Please add all expected Yielders in main.ts.'
);
}
if (!EXPECTED_YIELDERS.every((v) => DEFINED_YIELDERS.includes(v))) {
throw new Error(
'Not all expected Yielders from main.ts were defined. Please Yield all expected Yielders.'
);
}
}
export function expectYieders<T extends Constructable<any>>(...ctors: T[]) {
EXPECTED_YIELDERS.push(...ctors.map((x) => x.name));
return { checkYielders };
}
type Constructable<T> = new (...args: any[]) => T;
export function Yield<T extends Constructable<any>>(token: string) {
return (ctor: T) => {
if (!YIELD_LIST[token]) {
YIELD_LIST[token] = {
overrideCtor: ctor,
};
DEFINED_YIELDERS.push(ctor.name);
}
};
}
export function Yieldable<T extends Constructable<any>>(token: string) {
return (_ctor: T) => {
if (YIELD_LIST[token]) {
return class extends (YIELD_LIST[token].overrideCtor as T) {
constructor(...args: any[]) {
super(...args);
}
};
}
};
}
Добавьте это перед всем, чтобы обеспечить правильное поведение:
expectYieders(SpecificClassA).checkYielders();
Теперь можно определить базовый класс, наряду с классами Yield и Yieldable:
//-------class-a.ts--------
export const CLASS_A = 'CLASS_A_YIELD_TOKEN';
export class GeneralClassA {
public value = 5;
public showValue() {
console.info(this.value);
}
}
//---specific-class-a.ts---
@Yield(CLASS_A)
export class SpecificClassA extends GeneralClassA {
public override value = 10;
}
//----general-class-a.ts---
@Yieldable(CLASS_A)
export class ClassA extends GeneralClassA {}
Пожалуйста, опубликуйте код (хотя бы) декоратора, который сохраняет классы и сравнивает приоритеты в самом вопросе, а не просто ссылку на внешний сайт.