【问题标题】:Decorator in Node - is it possible to iterate through class methods inside the constructor in order to override / apply a decorator to themNode 中的装饰器 - 是否可以遍历构造函数中的类方法以覆盖/应用装饰器
【发布时间】:2022-01-14 11:18:09
【问题描述】:

我是 Node / Javascript 的新手。我想做的是使用装饰器功能将日志记录添加到我的存储库中。因此,我试图从构造函数内部迭代每个函数并用类似的东西覆盖它:“
Object.getOwnPropertyNames(Repository.prototype).forEach((func) => this.decorator(func));" 我的问题是“Object.getOwnPropertyNames”只返回函数名而不是实际函数。有没有办法将此装饰器应用于每个函数?

"use strict"
const db = require("./Database/db_operations");
const logger = require("./utils/logger")
const {createTables} = require("./Database/db_operations");
const loggingTypes = require("./utils/logginTypes")

class Repository {

    async saveTermin(Termin) {

    }


    async saveToDo(toDo) {
        return await db.saveToDo(toDo);
    }

    async saveAppointment(Appointment) {
        return await db.saveAppointment(Appointment);
    }

    async updateAppointment(Appointment) {
        return await db.updateAppointment(Appointment);
    }

    async deleteAppointment(uuid) {
        return await db.deleteAppointment(uuid);
    }

    async saveAppointmentParticipants(appointment) {
        return await db.saveAppointmentParticipants(appointment);
    }

    async saveAppointmentFiles(appointment) {
        return await db.saveAppointmentFiles(appointment)
    }

    async getAppointmentFiles(appointment) {
        return await db.getAppointmentFiles(appointment)
    }

    async deleteToDo(todo) {
        return await db.deleteToDo(todo)
    }

}



// All functions will be mapped to there type to optimize logging. If a function is not mapped to its type,
// it will be automaticly assigned to the "unspecified type". Logging will still work, but depending on what
// arguments are given and what is returned, the output might not perfectly fit

const funcMapping = new Map();

// GET
funcMapping.set(Repository.prototype.getAppointmentFiles, loggingTypes.GET);
funcMapping.set(Repository.prototype.getAllDatas, loggingTypes.GET);
funcMapping.set(Repository.prototype.getAllToDos, loggingTypes.GET);


//SAVE
funcMapping.set(Repository.prototype.saveToDo, loggingTypes.SAVE);
funcMapping.set(Repository.prototype.saveAppointment, loggingTypes.SAVE);
funcMapping.set(Repository.prototype.saveAppointmentParticipants, loggingTypes.SAVE);

//DELETE
funcMapping.set(Repository.prototype.deleteAppointment, loggingTypes.DELETE);
funcMapping.set(Repository.prototype.deleteToDo, loggingTypes.DELETE);


Object.getOwnPropertyNames(Repository.prototype)
    .forEach(name => {
        const func = Repository.prototype[name];
        // checking loggingTypes - if no type is assigned function will be assigned to "UNASSIGNED".
        // console.log(funcMapping.has(func) +" "+ func.name)
        if (!funcMapping.has(func)) {
            funcMapping.set(func, loggingTypes.UNASSIGNED);
        }
        // function will only be wrapped if logging is enabled.
        if (funcMapping.get(func)[1]) {
            Repository.prototype[name] = async function (...args) {
                // calls the original methode

                const returnValue = await func.apply(this, args);

                const argumentsInArray = Array.prototype.slice.call(args);

                // Put any additional logic here and it will be applied -> magic
                // Logging
                db.writeLogging(logger(func, returnValue, funcMapping.get(func)[0]), args).then(() => {
                    console.log(`Function "${name}()" was successfully logged and saved to Database`)
                }).catch(e => {
                    console.log(`Function "${name}()" could not be logged and saved to Database. ${func}`)
                    console.log(e)
                })
                return returnValue;
            }
        }
    });

module.exports = new Repository();


    const appointment_model = require('../models/Appointment');
    const contact_model = require('../models/Contact');
    const toDo_model = require('../models/ToDo');
    const file_model = require('../models/File');
    const loggingTypes = require("./logginTypes")
    
    
    function log() {
        // returns a function that returns an object. When this function is then called the object is returned
        return function decorator(funcToLog, returnValue, funcType, ...args) {
            // console.log("arguments in logger" + args);
    
            // create prototype for object that later will be passed to database
            const descriptor = function (user, change, changedAt) {
                this.user = user; // some user id
                this.change = change; //
                this.changedAt = changedAt; // date when changes occoured
                this.appointmentId = getUuid(appointment_model);
                this.todoId = getUuid(toDo_model);
                this.contactId = getUuid(contact_model);
                this.fileId = getUuid(file_model);
            };
    
    
            // contains all logging Data about the function beeing called -> name of function, usedArguments and returnValue
            function getChanges(func, funcType, returnValue, args) {
                let changes = null;
                switch (funcType) {
                    case loggingTypes.GET[0]:
                        changes = {
                            funcName: func.name, //
                            funcType: funcType, //
                            dataSetToChange: {...args},
                            newData: returnValue
                        }
                        break;
                    case loggingTypes.SAVE[0]:
                        changes = {
                            funcName: func.name, //
                            funcType: funcType, //
                            dataSetToChange: {...args}, // ?
                            newData: returnValue // could call function here
                        }
                        break;
                    case loggingTypes.UPDATE[0]:
                        changes = {
                            funcName: func.name, //
                            funcType: funcType, //
                            dataSetToChange: {...args},
                            newData: returnValue
                        }
                        break;
                    case loggingTypes.DELETE[0]:
                        changes = {
                            funcName: func.name, //
                            funcType: funcType, //
                            dataSetToChange: {...args},
                            newData: returnValue
                        }
                        break;
                    case loggingTypes.UNASSIGNED[0]:
                        changes = {
                            funcName: func.name, //
                            funcType: funcType, //
                            dataSetToChange: {...args},
                            newData: returnValue
                        }
                }
                return changes;
            }
    
            function getUuid(model_type) {
                let uuid = null;
                console.log(args)
                for (let i = 0; i < args.length; i++) {
                     console.log(args[i])
                     if (args[i] instanceof model_type) {
                         uuid = parseInt(args[i].uuid);
                    }
                    return uuid;
                }
            }
    
    
            return new descriptor("someUserId", JSON.stringify(getChanges(funcToLog, funcType, returnValue, args)), new Date())
        }
    }
    
    module.exports = log();


【问题讨论】:

  • 如果你想覆盖方法,使用继承更有意义。装饰器仅“包裹”整个类以向其添加内容。
  • 我真的不想覆盖它们,而是将它们包装在装饰器中以添加日志记录功能。我只是不知道如何正确使用/应用装饰器功能。
  • @Kokodoko 不,不要在这里使用继承。 OP 的意思是 overwrite,而不是 override(在单独的子类中)。
  • "它只返回函数名" - 好吧,你确实需要函数名来实际覆盖属性,例如Repository.prototype[name] = decorator(Repository.prototype[name])

标签: javascript node.js constructor decorator repository-pattern


【解决方案1】:

您可以使用中间步骤轻松地将函数名称映射到它们的值:

Object.getOwnPropertyNames(Repository.prototype)
.map(name => Repository.prototype[name])
.forEach((func) => this.decorator(func));

无论如何,构造函数并不是执行此操作的最佳位置,因为您最终会在每次创建类的新实例时应用装饰器。

我宁愿在类定义之后移动整个装饰器逻辑,在分配给module.exports之前。

Object.getOwnPropertyNames(Repository.prototype)
.forEach(name => {
    const func = Repository.prototype[name];
    Repository.prototype[name] = function (...args) {
        console.log("Decorator was called");
        const returnValue = func.apply(this, args);

        // Put additional logging logic here...

        return returnValue;
    }
});

更新

根据 cmets 中的说明,这里是上面代码的更健壮的版本,您可能需要也可能不需要额外的预防措施:

  • 保留非函数
  • 保留非值属性
  • 保留构造函数
  • 保留不可配置的属性
  • 包含带有符号键的属性
Reflect.ownKeys(Repository.prototype).forEach(key => {
    const descriptor = Reflect.getOwnPropertyDescriptor(Repository.prototype, key);
    if (!descriptor.configurable) return;
    const { value } = descriptor;
    if (typeof value !== 'function') return;
    if (value === Repository) return;
    descriptor.value = function (...args) {
        console.log("Decorator was called");
        const returnValue = value.apply(this, args);

        // Additional logging logic here...

        return returnValue;
    };
    Object.defineProperty(Repository.prototype, key, descriptor);
});

我遗漏的另一件事是附加逻辑,以确保 decorated 方法具有与原始函数相同的长度和名称属性以及相同的原型。当您在使用代码时发现其他要求时,您可能需要调整更多细节。

【讨论】:

  • 谢谢!我在这上面坐了好几个小时!开始质疑整个编程:D 希望有一天会变得更容易!
  • 还要确保添加检查以不覆盖非函数、getter/setter 或 constructor。
  • 再次感谢您,这真的很有帮助。但是我仍然有一个无法解决的问题。出于某种原因,我无法掌握“args”中的任何值。使用“value.apply(this, args)”时它们可以正常工作,但是当我将它们传递给任何其他函数时,它只会返回一个没有其他内容的对象。我认为这是由于这里描述的:educative.io/edpresso/…“如果我们在没有休息、默认或重组参数的情况下更改函数内部参数的值,它将在参数对象中更新,反之亦然”。跨度>
  • 还有其他办法吗?我要锁定的是在包装器内调用记录器并传递用于调用包装函数的参数以记录它们。
  • @lukas 答案的更新已经做到了。 Setter/getter 由 getOwnPropertyDescriptor 找到,.constructor 属性是不可配置的属性之一,但 if (key == 'constructor') 也可以轻松检测到
猜你喜欢
  • 2017-04-09
  • 2012-09-03
  • 2017-02-04
  • 2020-05-20
  • 2012-03-14
  • 2020-01-11
  • 2014-01-14
  • 2021-10-04
  • 2017-05-04
相关资源
最近更新 更多