Как проверить значение переменной, что в ней хранится класс который был наследован не формируя его экземпляр

Рейтинг: 1Ответов: 1Опубликовано: 29.03.2023

Давайте представим себе такую абстракцию, где у нас есть базовый класс сущности AppEntity от него могут наследоваться всё остиальные, а также коллекция которая использует метод search для получения нужных сущностей по API, а далее лепить слепки данной сущности на основе переданного определенного класса в данной коллекции.

Загадка в том, что не совсем понятно как в JS правильно определить, что в свойстве экземпляра Collection храниться именно унаследованный класс от определенной сущности.

Исходные данные

1. Определения исходного класса коллекции:

class Collection {
    /**
     * @type {AppEntity|null} Класс сущности коллекции.
     */
    entityClass = null

    /**
     * Выполняет запрос на получение данных коллекции
     *
     * @return {Promise<this>}
     */
    async search() {
        this.checkEntityClassIsDefined();
        //Скрытый процесс получения данных
        
        return this;
    }

    /**
     * Проверяет, определен ли класс сущности.
     *
     * @return {void}
     */
    checkEntityClassIsDefined() {
        if (this.entityClass === null || !(typeof this.entityClass === 'function')) {
            throw new Error(`Класс сущности не определен.`);
        }
    }

    /**
     * Устанавливает сущность коллекции.
     *
     * @param {AppEntity} entityClass Сущность коллекции.
     *
     * @return {this} Экземпляр текущей коллекции.
     */
    forEntity(entityClass) {
        this.entityClass = entityClass;

        return this;
    }
}

2. Определения исходного/общего класса сущности:


class AppEntity {
    /**
     * @type {{}} Атрибуты сущности и их значения.
     */
    attributes: {}

    /**
     * Заполняет данные в сущность (переопределяя текущие).
     *
     * @return {this}
     */
    fill();

    /**
     * Возвращает значения сущности..
     *
     * @return {object}
     */
    pull();

    /**
     * Очищает атрибуты.
     *
     * @return {this}
     */
    purge();
}

3. Формирование цепочки наследования от исходного/общего класса сущности:

class PostEntity extends AppEntity {}

Вопрос звучит следующим образом:

Как правильно определить, что в свойстве entityClass экземпляра Collection хранится класс который в свою очередь был наследован от AppEntity в методе checkEntityClassIsDefined?

Желательно не прибегая к формированию экземпляра сущности и проверки его через instanceof?

  • Исполнения кода который не должен вызвать проблем, однако он не даёт уверенности, что мы действительно передали нужный нам класс, да и вообще если в переменную определить обычную функцию, то код исполнится:
const collection = (new Collection()).forEntity(PostEntity);
collection.search();
  • Исполнения кода, где определено в место класса обычная стрелочная функция.
const collection = (new Collection()).forEntity(() => null);
collection.search();
  • Исполнения кода, где не определен класс сущности в коллекции и ожидаем исключение.
const collection = (new Collection());
collection.search();

Ответы

▲ 1

Кропотливый анализ позволил сделать вывод, что есть 2 способа для получения прототипа объекта, связанного с переменной. Если полученный прототип является классом, то мы можем быть уверены, что переменная хранит в себе класс ну и в таком духе.

  1. Object.getPrototypeOf(variable) === AppEntity
  2. AppEntity.isPrototypeOf(variable)

А вот и тесты которые подтверждают данную теорию:

class PostEntity extends AppEntity {};
const VAR_POST_CLASS = PostEntity;

const VAR_POST_CLASS_INSTANCE = new VAR_POST_CLASS();
const VAR_POST_CLASS_EXTENDS_APP_ENTITY = class PostEntity extends AppEntity {};
const VAR_POST_CLASS_EXTENDS_APP_ENTITY_INSTANCE = new VAR_POST_CLASS_EXTENDS_APP_ENTITY();
const VAR_NULL = null;
const VAR_STRING = '';
const VAR_OBJ = {};
const VAR_NUMBER = 1;
const VAR_ARRAY = [];
const VAR_FUNCTION = () => {};

describe('Проверка Object.getPrototypeOf(variable) === AppEntity', () => {
    let variableIsPrototypeOfAppEntity = (variable) => Object.getPrototypeOf(variable) === AppEntity;

    it("Должно вернуть true, если переменная в которой храниться 'class PostEntity' наследуется от 'AppEntity'", () => {
        expect(variableIsPrototypeOfAppEntity(VAR_POST_CLASS_EXTENDS_APP_ENTITY)).toBe(true);
    });
    it("Должно выбросить исключение типом 'TypeError', если значение переменной является 'null'", () => {
        expect(() => variableIsPrototypeOfAppEntity(VAR_NULL)).toThrow(TypeError);
    });

    describe("Должно вернуть false, если в переменной не храниться class который наследуется от 'AppEntity'", () => {
        it("Проверяет экземпляр класса который не наследуется от 'AppEntity'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_POST_CLASS_INSTANCE)).toBe(false);
        });
        it("Экземпляр класса который наследуется от 'AppEntity'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_POST_CLASS_EXTENDS_APP_ENTITY_INSTANCE)).toBe(false);
        });
        it("Значение переменной является типом 'string'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_STRING)).toBe(false);
        });
        it("Значение переменной является типом 'object'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_OBJ)).toBe(false);
        });
        it("Значение переменной является типом 'number'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_NUMBER)).toBe(false);
        });
        it("Значение переменной является типом 'array'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_ARRAY)).toBe(false);
        });
        it("Значение переменной является типом 'function'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_FUNCTION)).toBe(false);
        });
    });
});

describe('Проверка AppEntity.isPrototypeOf(variable)', () => {
    let variableIsPrototypeOfAppEntity = (variable) => AppEntity.isPrototypeOf(variable);

    it("Должно вернуть true, если переменная в которой храниться 'class PostEntity' наследуется от 'AppEntity'", () => {
        expect(variableIsPrototypeOfAppEntity(VAR_POST_CLASS_EXTENDS_APP_ENTITY)).toBe(true);
    });

    describe("Должно вернуть false, если в переменной не храниться class который наследуется от 'AppEntity'", () => {
        it("Проверяет экземпляр класса который не наследуется от 'AppEntity'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_POST_CLASS_INSTANCE)).toBe(false);
        });
        it("Экземпляр класса который наследуется от 'AppEntity'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_POST_CLASS_EXTENDS_APP_ENTITY_INSTANCE)).toBe(false);
        });
        it("Значение переменной является 'null'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_NULL)).toBe(false);
        });
        it("Значение переменной является типом 'string'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_STRING)).toBe(false);
        });
        it("Значение переменной является типом 'object'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_OBJ)).toBe(false);
        });
        it("Значение переменной является типом 'number'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_NUMBER)).toBe(false);
        });
        it("Значение переменной является типом 'array'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_ARRAY)).toBe(false);
        });
        it("Значение переменной является типом 'function'", () => {
            expect(variableIsPrototypeOfAppEntity(VAR_FUNCTION)).toBe(false);
        });
    });
});