Определение конкретной функциональности класса с использованием декораторов в качестве информации о переопределениях классов

Я реализовал функцию декоратора класса 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;
}

Пожалуйста, опубликуйте код (хотя бы) декоратора, который сохраняет классы и сравнивает приоритеты в самом вопросе, а не просто ссылку на внешний сайт.

Bergi 18.05.2024 22:28

Я отредактировал свой вопрос, был добавлен код декоратора.

Darko Mitic 19.05.2024 03:04

«Если недавно декорированный класс имеет меньший приоритет по сравнению с сохраненным в данный момент классом с наивысшим приоритетом, этот новый класс должен стать этим сохраненным классом». - правильно ли я понимаю, что вы хотите, чтобы декоратор фактически заменял объявляемый класс (каким-то другим, хранимым классом), а не просто регистрировал объявляемый класс?

Bergi 19.05.2024 03:15

Да. Я хочу полностью заменить этот класс, поэтому декоратор возвращает новый класс, расширенный из сохраненного конструктора класса с максимальным приоритетом.

Darko Mitic 19.05.2024 11:33

Это довольно странно и, вероятно, вызовет много сюрпризов. Или вы хотите иметь только один предопределенный резервный вариант и, возможно, одно переопределение, указанное разработчиком, для каждого имени? Тогда вам, вероятно, не следует использовать систему с произвольным приоритетом.

Bergi 19.05.2024 18:15

Где (и когда) используются эти классы? Как в целом обеспечить загрузку всех классов с декораторами перед использованием реестра?

Bergi 19.05.2024 18:16

Планируется использовать его в нескольких игровых средах. Я хочу определить некоторые базовые функции в общей части игрового «движка», а затем добавить определенные функции к некоторым из этих классов движка, не меняя эти классы напрямую. Это внедрение зависимостей без инжектора и вызовов «provideClass» — все автоматически на основе декоратора.

Darko Mitic 19.05.2024 18:21

Я бы настоятельно рекомендовал не смешивать «provideClass» с функциональностью «useClass». Также я до сих пор не понимаю, зачем для этого нужна сложная система приоритетов.

Bergi 19.05.2024 18:29

«Или вы хотите иметь только один предопределенный резервный вариант и, возможно, одно переопределение, указанное разработчиком, для каждого имени?» Да. Один базовый класс и, возможно, одно переопределение, указанное разработчиком. Например, я бы получил класс «Speech» и производный класс «MaleSpeech», оба с приоритетом по умолчанию 0. Когда разработчик создает новую игру, он может реализовать класс «FemaleSpeech» и украсить его приоритетом 1. Теперь для этой игры , вместо этого движок будет использовать этот новый класс. Для меня DI — гораздо более сложная система, и поэтому я пытаюсь добиться простоты.

Darko Mitic 19.05.2024 18:41

На самом деле для простоты вам следует отказаться от этого приоритета. Пусть разработчик напишет только import { Speech, Yield } from 'engine'; @Yield('Speech') class FemaleSpeech extends Speech { … }. Затем, когда вашему движку понадобится речевой экземпляр, он может проверить, зарегистрировал ли разработчик для этого собственный класс, или вместо этого вернуться к MaleSpeech.

Bergi 19.05.2024 19:13

Вам не нужна никакая логика, чтобы @Yield('Speech', 0.5) class FemaleSpeech { … } автоматически выводил Speech или возвращал MaleSpeech, если приоритет был слишком низким.

Bergi 19.05.2024 19:14

Я это понимаю, мне нравится идея не иметь приоритета и просто прямо сказать, что вам нужно переопределить. Я мог бы даже использовать в качестве параметра сам класс вместо строки «Речь». Единственная проблема заключается в том, что я не хочу усложнять задачу наличием создателя экземпляра - я хочу, чтобы «new Speech()» работал как обычно, а внутренние элементы класса Speech на самом деле были FemaleSpeech или MaleSpeech.

Darko Mitic 19.05.2024 20:32

Вы не можете использовать Speech как фабрику, производящую экземпляр определенного класса, и как суперкласс конкретных реализаций. Это даже не имеет ничего общего с внедрением зависимостей, оно также не будет работать с обычно определенными классами. Может быть, наследование в любом случае является неправильным выбором? Но на самом деле я думаю, что нет ничего плохого в том, чтобы иметь отдельного «создателя экземпляров». Вы все еще можете написать это как const MySpeech = instanceMaker('speech'); new MySpeech().

Bergi 19.05.2024 20:44

Я понимаю, что не могу переопределить суперкласс. Это заставило бы класс быть производным от самого себя. Проверьте комментарии в примере StackBlitz: меня устраивает отсутствие переопределения суперклассов, и это поведение по умолчанию, поскольку этот суперкласс всегда будет определен первым, перед производным классом, и он никогда даже не будет рассматриваться. для переопределения. В данном примере базовым классом будет класс MaleSpeech.

Darko Mitic 19.05.2024 21:14

Прошу прощения за спам и за замешательство, которое я вызвал. Я придумал способ сначала принудительно объявить класс Yield, что также вынуждает разработчика использовать декоратор Yield только одним и предполагаемым способом. Если вам интересно, проверьте это на StackBlitz. Кроме того, спасибо за ваш вклад, это заставило меня больше задуматься о простоте использования!

Darko Mitic 19.05.2024 23:21
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
0
15
58
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я заставил его работать так, как задумано, с помощью некоторой логики проверки декоратора. Разработчик должен определить все ожидаемые классы переопределения, и все эти классы должны быть установлены как переопределения, чтобы код мог работать. Переопределенный и переопределенный классы должны быть оформлены с использованием функций декоратора 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 {}

Другие вопросы по теме