Javascript 处理对象属性之间的复杂依赖关系(自动更新相关属性)

Javascript 处理对象属性之间的复杂依赖关系(自动更新相关属性),javascript,algorithm,oop,design-patterns,dependencies,Javascript,Algorithm,Oop,Design Patterns,Dependencies,我有一个对象的树结构,它们的属性对周围对象有非常复杂的依赖关系,这取决于它们在树中的位置。我已经硬编码了很多这些依赖项,并尝试创建某种更新循环(如果一个属性根据设计得到更新,那么依赖它的所有属性都会按照正确的顺序得到更新),但我希望以更通用/抽象的方式处理它,而不是硬编码一堆对不同对象的更新调用 比方说,我有1个超类,3个子类,然后是一个单独的容器对象 形状 属性:父容器、索引、左侧、顶部、宽度、高度 方法:updateLeft(),updateTop(),updateWidth(),updat

我有一个对象的树结构,它们的属性对周围对象有非常复杂的依赖关系,这取决于它们在树中的位置。我已经硬编码了很多这些依赖项,并尝试创建某种更新循环(如果一个属性根据设计得到更新,那么依赖它的所有属性都会按照正确的顺序得到更新),但我希望以更通用/抽象的方式处理它,而不是硬编码一堆对不同对象的更新调用

比方说,我有1个超类,3个子类,然后是一个单独的容器对象

形状
属性:父容器、索引、左侧、顶部、宽度、高度
方法:updateLeft(),updateTop(),updateWidth(),updateHeight()

方形继承自形状
三角形继承自形状
圆继承自形状

形状容器
属性:形状
方法:addShape(形状,索引),removeShape(索引)

我将给出一个伪代码示例更新方法来说明这些依赖关系是如何产生的:

Square.updateTop() {
    var prevShape = null;
    if (this.index != 0) {
        prevShape = this.parentContainer.shapes[this.index - 1];
    }
    var nextSquareInContainer = null;
    for (var i = this.index; i < this.parentContainer.shapes.length; i++) {
        var shape = this.parentContainer.shapes[i];
        if(shape instanceof Square) {
            nextSquareInContainer = shape;
            break;
        }
    }
    var top = 0;
    if (prevShape != null && nextSquareInContainer != null) {
        top = prevShape.top + nextSquareInContainer.width;
    } else {
        top = 22;
    }
    this.top = top;
}  
所以,我想问题的关键是,如果我更新上面圆的顶值,我希望square1的顶值自动更新(因为square1的顶值和圆的顶值之间存在单向依赖关系)。因此,我可以这样做的一种方法(我一直在这样做,结合我的问题领域的一些其他特定知识来简化调用),是将类似于以下的代码添加到Circle的updateTop方法中(实际上它必须添加到每个形状的updateTop方法中):

这种类型的设计适用于对象之间的一些简单依赖关系,但我的项目有几十种类型的对象,它们的属性之间可能有数百种依赖关系。我是这样编写的,但在尝试添加新功能或解决bug时,很难进行推理

是否存在某种设计模式来设置对象属性之间的依赖关系,然后当一个属性更新时,它会更新依赖于它的其他对象上的所有属性(这可能会触发进一步更新依赖于现在新更新的属性的属性)?某种用于指定这些依赖项的声明性语法对于可读性/可维护性来说可能是最好的

另一个问题是,一个属性可能有多个依赖项,必须先更新所有依赖项,然后才能更新该属性本身


我一直在寻找一种酒吧/酒吧类型的解决方案,但我认为这是一个足够复杂的问题,需要寻求帮助。顺便说一句,我正在使用javascript。

这是我提出的黑客解决方案。我创建了一个包装器类,将匿名函数传递给getter/setter/updater。然后调用prop1.dependsOn(prop2)以声明方式设置依赖项。它涉及建立对象属性之间依赖关系的有向无环图,然后在更新属性值时,显式调用以使用拓扑排序解析相关依赖关系。我没有在效率上花太多心思,我打赌有人可以想出一个更健壮/更高性能的解决方案,但我认为现在就可以了。很抱歉代码转储,但我认为这可能对以后试图解决类似问题的人有所帮助。如果有人想让这个语法更清晰,请便

// This is a class that will act as a wrapper for all properties 
// that we want to tie to our dependency graph.
function Property(initialValue, ctx) {
    // Each property will get a unique id.
    this.id = (++Property.id).toString();
    this.value = initialValue;
    this.isUpdated = false;
    this.context = ctx;
    Property.dependsOn[this.id] = [];
    Property.isDependedOnBy[this.id] = [];
    Property.idMapping[this.id] = this;
}
// Static properties on Property function.
Property.id = 0;
Property.dependsOn = {};
Property.isDependedOnBy = {};
Property.idMapping = {};

// Calling this updates all dependencies from the node outward.
Property.resolveDependencies = function (node) {
    node = node.id;
    var visible = [];
    // Using Depth First Search to mark visibility (only want to update dependencies that are visible).
    var depthFirst = function (node) {
        visible.push(node);
        for (var i = 0; i < Property.isDependedOnBy[node].length; i++) {
            depthFirst(Property.isDependedOnBy[node][i]);
        }
    };
    depthFirst(node);
    // Topological sort to make sure updates are done in the correct order.
    var generateOrder = function (inbound) {
        var noIncomingEdges = [];
        for (var key in inbound) {
            if (inbound.hasOwnProperty(key)) {
                if (inbound[key].length === 0) {
                    // Only call update if visible.
                    if (_.indexOf(visible, key) !== -1) {
                        Property.idMapping[key].computeValue();
                    }
                    noIncomingEdges.push(key);
                    delete inbound[key];
                }
            }
        }

        for (var key in inbound) {
            if (inbound.hasOwnProperty(key)) {
                for (var i = 0; i < noIncomingEdges.length; i++) {
                    inbound[key] = _.without(inbound[key], noIncomingEdges[i]);
                }
            }
        }

        // Check if the object has anymore nodes.
        for (var prop in inbound) {
            if (Object.prototype.hasOwnProperty.call(inbound, prop)) {
                generateOrder(inbound);
            }
        }

    };
    generateOrder(_.clone(Property.dependsOn));
};
Property.prototype.get = function () {
    return this.value;
}
Property.prototype.set = function (value) {
    this.value = value;
}
Property.prototype.computeValue = function () {
    // Call code that updates this.value.
};
Property.prototype.dependsOn = function (prop) {
    Property.dependsOn[this.id].push(prop.id);
    Property.isDependedOnBy[prop.id].push(this.id);
}

function PropertyFactory(methodObject) {
    var self = this;
    var PropType = function (initialValue) {
        Property.call(this, initialValue, self);
    }
    PropType.prototype = Object.create(Property.prototype);
    PropType.prototype.constructor = PropType;
    if (methodObject.get !== null) {
        PropType.prototype.get = methodObject.get;
    }
    if (methodObject.set !== null) {
        PropType.prototype.set = methodObject.set;
    }
    if (methodObject.computeValue !== null) {
        PropType.prototype.computeValue = methodObject.computeValue;
    }

    return new PropType(methodObject.initialValue);
}
下面是一个在所有基础设施设置完毕后实际更新属性的示例:

c1.prop.set(3);
Property.resolveDependencies(c1.prop);

我觉得对于那些需要非常复杂的依赖关系的程序来说,这是一个非常强大的模式。Knockout JS也有类似的功能,有computedObservables(它们以类似的方式使用包装器),但根据我所知,您只能将computed属性与同一对象上的其他属性联系起来。上述模式允许您将对象属性任意关联为依赖项。

听起来您需要的是数据绑定。检查一下,你也可以找到几个多边形填充,可能是你的问题的答案。
// This is a class that will act as a wrapper for all properties 
// that we want to tie to our dependency graph.
function Property(initialValue, ctx) {
    // Each property will get a unique id.
    this.id = (++Property.id).toString();
    this.value = initialValue;
    this.isUpdated = false;
    this.context = ctx;
    Property.dependsOn[this.id] = [];
    Property.isDependedOnBy[this.id] = [];
    Property.idMapping[this.id] = this;
}
// Static properties on Property function.
Property.id = 0;
Property.dependsOn = {};
Property.isDependedOnBy = {};
Property.idMapping = {};

// Calling this updates all dependencies from the node outward.
Property.resolveDependencies = function (node) {
    node = node.id;
    var visible = [];
    // Using Depth First Search to mark visibility (only want to update dependencies that are visible).
    var depthFirst = function (node) {
        visible.push(node);
        for (var i = 0; i < Property.isDependedOnBy[node].length; i++) {
            depthFirst(Property.isDependedOnBy[node][i]);
        }
    };
    depthFirst(node);
    // Topological sort to make sure updates are done in the correct order.
    var generateOrder = function (inbound) {
        var noIncomingEdges = [];
        for (var key in inbound) {
            if (inbound.hasOwnProperty(key)) {
                if (inbound[key].length === 0) {
                    // Only call update if visible.
                    if (_.indexOf(visible, key) !== -1) {
                        Property.idMapping[key].computeValue();
                    }
                    noIncomingEdges.push(key);
                    delete inbound[key];
                }
            }
        }

        for (var key in inbound) {
            if (inbound.hasOwnProperty(key)) {
                for (var i = 0; i < noIncomingEdges.length; i++) {
                    inbound[key] = _.without(inbound[key], noIncomingEdges[i]);
                }
            }
        }

        // Check if the object has anymore nodes.
        for (var prop in inbound) {
            if (Object.prototype.hasOwnProperty.call(inbound, prop)) {
                generateOrder(inbound);
            }
        }

    };
    generateOrder(_.clone(Property.dependsOn));
};
Property.prototype.get = function () {
    return this.value;
}
Property.prototype.set = function (value) {
    this.value = value;
}
Property.prototype.computeValue = function () {
    // Call code that updates this.value.
};
Property.prototype.dependsOn = function (prop) {
    Property.dependsOn[this.id].push(prop.id);
    Property.isDependedOnBy[prop.id].push(this.id);
}

function PropertyFactory(methodObject) {
    var self = this;
    var PropType = function (initialValue) {
        Property.call(this, initialValue, self);
    }
    PropType.prototype = Object.create(Property.prototype);
    PropType.prototype.constructor = PropType;
    if (methodObject.get !== null) {
        PropType.prototype.get = methodObject.get;
    }
    if (methodObject.set !== null) {
        PropType.prototype.set = methodObject.set;
    }
    if (methodObject.computeValue !== null) {
        PropType.prototype.computeValue = methodObject.computeValue;
    }

    return new PropType(methodObject.initialValue);
}
function MyClassContainer() {
    this.children = [];
    this.prop = PropertyFactory.call(this, {
        initialValue: 0,
        get: null,
        set: null,
        computeValue: function () {
            var self = this.context;
            var updatedVal = self.children[0].prop.get() + self.children[1].prop.get();
            this.set(updatedVal);
        }
    });
}
MyClassContainer.prototype.addChildren = function (child) {
    if (this.children.length === 0 || this.children.length === 1) {
        // Here is the key line.  This line is setting up the dependency between
        // object properties.
        this.prop.dependsOn(child.prop);
    }
    this.children.push(child);
}

function MyClass() {
    this.prop = PropertyFactory.call(this, {
        initialValue: 5,
        get: null,
        set: null,
        computeValue: null
    });
}

var c = new MyClassContainer();
var c1 = new MyClass();
var c2 = new MyClass();
c.addChildren(c1);
c.addChildren(c2);
c1.prop.set(3);
Property.resolveDependencies(c1.prop);