Как создать универсальный метод фабрики, который обращается к статическим членам создаваемого им типа класса - PullRequest
1 голос
/ 05 июля 2019

Я использую Typescript для записи конечных точек API REST в качестве функций над Firebase, и все методы следуют похожему шаблону: проверьте на request.body, извлеките соответствующие данные из этих данных тела, поместите их в строготипизированный объект, используйте этот объект для передачи данных в базу данных через некоторый код доступа к данным.После того, как я несколько раз написал одну и ту же базовую логику извлечения данных для работы с request.body, я решил, что должен быть способ абстрагировать эту работу.У меня есть три требования для этого: (1) метод должен работать для извлечения данных из request.body для любой из моих моделей данных.(2) Модели данных должны быть полностью информативными, чтобы они не только описывали свойства, которыми должны обладать данные, но и могли относиться, когда требуется определенный набор свойств.(3) Метод должен иметь возможность определять из моделей данных, какие свойства требуются, и выполнять некоторую проверку данных, передаваемых через request.body.

В качестве примера # 2, а модели являются самостоятельными-описательный: учтите, например, что при создании новой записи данных мне не требуется идентификатор, поскольку, если его нет, я могу создать его в функции и передать обратно.С другой стороны, свойство "name" - это , необходимое в этом случае.В отличие от этого, метод update требует идентификатора записи (поэтому он знает, какую запись обновлять), но не не требует «имени», если только это не то, что на самом деле изменяется.

Мой подход состоял в том, чтобы использовать (1) метод статической фабрики в отдельном классе, который принимает тип класса для модели данных, которую необходимо создать;предполагаемая операция (то есть создать , прочитать , обновить или удалить );и тело запроса.(2) Набор классов моделей данных, которые в основном просто описывают данные и включают в себя небольшую логику проверки, где это необходимо, но также включают в себя (статический) список имен полей и связанных значений требований (сохраняются в виде четырех битов, где каждая позиция представляет одиниз четырех операций CRUD.) (3) Общий интерфейс, так что метод статической фабрики знает, как обращаться с различными объектами данных, чтобы получить эти имена полей и флаги использования.

Вот мой метод статической фабрики:

static create<T extends typeof DataObjectBase>(cls: { new(...args: any[]): T; }, intendedOperation: number, requestBody: any) : T {
        let dataObject : T = null;
        const sourceData = {};
        const objFields = cls.fieldNames;
        const flagCollection = cls.requiredUseFlags();
        const requiredFields = flagCollection.getFieldsForOperation(intendedOperation);
        if (requestBody) {
            // parse the request body
            // first get all values that are available and match object field names
            const allFields = Object.values(objFields); // gets all properties as key/value pairs for easier iteration
            // iterate through the allFields array
            for (const f in allFields) {
                if (requestBody.hasOwnProperty(f)) {
                    // prop found; add the field to 'sourceData' and copy the value from requestBody
                    sourceData[f] = requestBody[f];
                } else if (requiredFields.indexOf(f)>-1) {
                    // field is required but not available; throw error
                    throw new InvalidArgumentError(`${cls}.${f} is a required field, but no value found for it in request.body.`, requestBody);
                }
            }
            dataObject = (<any>Object).assign(dataObject, sourceData);
        } else {
            throw new ArgumentNullError('"requestBody" argument cannot be null.', requestBody);
        }
        return new cls();
    }

Вот пример класса модели данных:

export class Address extends DataObjectBase {
    constructor(
        public id         : string,
        public street1    : string,
        public street2    : string = "",
        public city       : string,
        public state      : string,
        public zip        : string) {
        // call base constructor
        super();
    }

    static fieldNames = {
        ID      = "id",
        STREET1 = "street1",
        STREET2 = "street2",
        // you get the idea...
    }

    static requiredUseFlags() {
        ID = READ | UPDATE | DELETE,
        STREET1 = 0,
        // again, you get the idea...
        // CREATE, READ, UPDATE, DELETE are all bit-flags set elsewhere
    }
}

Я хочу иметь возможность вызывать вышеуказанный метод create следующим образом:

const address = create<Address>(Address, CREATE, request.body);

Первоначально я пробовал подпись, подобную этой:

static create<T extends typeof DataObjectBase>(cls: T, intendedOperation: number, requestBody: any) : T

Однако, когда я сделал это, я получил ошибку, что «Адрес является типом, но используется как значение».Как только я изменил его на то, что у меня было выше, я перестал получать эту ошибку и начал получать Property 'fieldNames' does not exist on type 'new (...args: any[]) => T'

Примечание: я также попробовал хитрость с использованием двух интерфейсов для описания (в первом) методов экземпляраи (во втором) статические методы, а затем статический интерфейс расширяют интерфейс экземпляра, а базовый класс реализует статический интерфейс и т. д., как описано здесь , здесь и здесь .Меня это тоже не дошло.

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

1 Ответ

2 голосов
/ 05 июля 2019

Вы можете использовать this внутри статического метода для ссылки на текущий класс (что позволяет вам написать new this() для создания экземпляра класса).

Что касается ввода этого таким образом, чтобы иметь возможность конструировать объекты и иметь доступ к статике, самое простое решение состоит в том, чтобы иметь сигнатуру конструктора в том виде, в каком вы ее определили, и добавить обратно статику, используя пересечение с Pick<typeof DataObjectBase, keyof typeof DataObjectBase>. Это сохранит статические члены, но удалит все сигнатуры конструктора базового класса.

Также T должно расширяться DataObjectBase (тип экземпляра), а не typeof DataObjectBase (тип класса)

type FieldsForOperation = {getFieldsForOperation(intendedOperation: number): string[] }
class DataObjectBase {
static fieldNames: Record<string, string>
static requiredUseFlags():FieldsForOperation { return null!; }
static create<T extends DataObjectBase>(this: (new (...a: any[]) => T) & Pick<typeof DataObjectBase, keyof typeof DataObjectBase> , intendedOperation: number, requestBody: any) : T {
        let dataObject : T = null;
        const sourceData = {};
        const objFields = this.fieldNames;
        const flagCollection = this.requiredUseFlags();
        // rest of code
        return new this();
    }
}

export class Address extends DataObjectBase {
    constructor(
        public id         : string,
        public street1    : string,
        public street2    : string = "",
        public city       : string,
        public state      : string,
        public zip        : string) {
        // call base constructor
        super();
    }

    static fieldNames = {
        "": ""
    }

    static requiredUseFlags(): FieldsForOperation {
        return null!;
    }
}

Address.create(0, {})

Примечание : Просто исправление ТС не приведет к выводу мнения о сверхинженерной части ?

...