如何设计这个程序时要考虑到简单的修改?

时间:2016-05-24 16:20:12

标签: c++

这个问题是关于如何设计程序,以便进行某些修改。

我有一个类,它包含一些(非平凡的)数据,并且有几个成员函数可以更改这些数据。

有时我需要计算这些数据的某些属性。但是,每次改变都要从头开始重新计算它。相反,计算这些属性的小更新要快得多。

我有几个这样的属性,我需要能够轻松地添加到我的班级或从班级中删除(或打开/关闭)以进行一些数值实验。该类仅由我自己修改,用于数值模拟(科学代码)。

具体示例

我们说我有一个包含数字x的班级。但我还需要2^x("属性" x)。基本课程是:

class C {
    double x;

public:
    C() : x(0.0) 
    { }

    void inc() { x += 1; } 
    void dec() { x -= 1; } 
    void set(double x_) { x = x_; } 
};

但现在我需要跟踪2^x,并在每次x更改时不断更新此值。所以我最终得到了

class expC {
    double expx;

public:        
    expC(const double &x) {
        recompute(x);
    }

    void inc() { expx *= 2; } // fast incremental change
    void dec() { expx /= 2; } // fast incremental change
    void recompute(const double &x) {
        expx = std::pow(2, x); // slow recomputation from scratch
    }
};


class C {
    double x;

    expC prop1; // XX

public:
    C() : x(0.0), prop1(x) // XX 
    { }

    void inc() { 
        x += 1;
        prop1.inc(); // XX 
    }
    void dec() { 
        x -= 1; 
        prop1.dec(); // XX
    }
    void set(double x_) { 
        x = x_;
        prop1.recompute(x); // XX
    }
};

XX标记我需要对班级C进行的更改。这是很多变化,容易出错。有几个属性变得更加复杂,我甚至相互依赖。

class C {
    double x;

    expC  prop1; // XX
    someC prop2; // XX

public:
    C() : x(0.0), prop1(x), prop2(x, prop1) // XX 
    { }

    void inc() { 
        x += 1;
        prop1.inc(); // XX 
        prop2.inc(); // XX 
    }
    void dec() { 
        x -= 1; 
        prop1.dec(); // XX
        prop2.dec(); // XX
    }
    void set(double x_) { 
        x = x_;
        prop1.recompute(x); // XX
        prop2.recompute(x, prop1); // XX
    }
};

问题:这样的程序有什么好的设计?我确信它可以比上面做的更好。目标是:1)轻松添加/删除此类属性或打开/关闭其计算2)性能至关重要。 incdec在紧密的内循环中被调用并且相对较少。出于性能原因,它们不能虚拟化。

实际上x是一个更复杂的数据结构。想想例如在graph中添加/删除边缘,并在此过程中跟踪其度数序列。

更新

@ tobi303要求我展示如何使用这个类。它的方式类似于:

void simulate(C &c) {
    for (/* lots of iterations */) {
        c.inc();
        double p1 = c.prop1.value();
        double p2 = c.prop2.value();
        if (condition(p1,p2))
            c.dec();
    }
}

或者说:

  • 进行(随机)更改
  • 在更改后获取属性值
  • 取决于新属性值,决定是接受还是撤消更改。

它实际上是类似于Metropolis-Hasting algorithm的蒙特卡罗模拟。

一个具体的例子可能是"数据"在类C(状态)中是Ising model的自旋状态(对于那些熟悉它的人),属性是系统的总能量和总磁化强度。单次旋转翻转后更新速度要快于从头开始重新计算。在实践中,我没有Ising模型,我有一些更复杂的东西。我有几个属性,一些快速计算,一些慢(实际上我有一些辅助数据结构,有助于计算属性)。我需要尝试不同属性的组合,所以我经常更改我在代码中包含的内容。有时我会实现新属性。当我不需要已经实现的属性时,我需要能够出于性能原因关闭计算(有些计算速度很慢)。

2 个答案:

答案 0 :(得分:2)

只需be lazy,无需在需要时计算属性。它将删除大量代码和不必要的计算。

当您需要您的属性时,如果它尚未在缓存中,请进行计算。因此,每个属性都需要一个布尔值来判断缓存是否是最新的,并且每次x本身更新时都需要使布尔值无效。

基本上:

class C {
    double x;

    template <typename Value> struct cachedProp {
        bool cache = false;
        Value value;
    }

    cachedProp<expC> prop1;
    cachedProp<someC> prop2;
    //...

    void invalidateCache() {
         prop1.cache = false;
         prop2.cache = false;
         //...
    }
public:
    expC getProperty1() {
        if (!prop1.cache) {
            recalculateProp1();
            prop1.cache = true;
        }
        return prop1.value;
    }

    void inc() {
        x += 1;
        invalidateCache();
    }
};

编辑:甚至更懒惰的解决方案是在cache中存储一个布尔值,而不是在上一次更新中存储一个整数,并在C中维护一个计数器。每次缓存失效时,C中的计数器都会增加。获取propX后,如果计数器与propX.lastUpdate不匹配,则更新`propX。

这样,使缓存无效只是一个操作,并且不必更新所有属性&#39;高速缓存中。

答案 1 :(得分:1)

这是一种可能适合您的方法:

class C {
    double x;

    expC prop1;
    someC prop2;
    .
    .
    .

    template <typename F>
    void for_each_property(const F &f)
    {
        f(prop1,x);
        f(prop2,x,prop1);
        .
        .
        .
    }

public:
    C() : x(0.0), prop1(x), prop2(x, prop1)
    { }

    void inc()
    {
        x += 1;

        for_each_property([](auto &prop,auto&& ...) {
            prop.inc();
        });
    }

    void dec()
    {
        x -= 1;

        for_each_property([](auto &prop,auto&& ...) {
            prop.dec();
        });
    }

    void set(double x_)
    {
        x = x_;

        for_each_property([](auto &prop,auto&& ... args) {
            prop.recompute(args...);
        });
    }
};

添加新媒体资源时,只需在for_each_property()中添加一个电话即可。变量的使用避免了为不同参数提供新的重载的需要,只要你坚持使用相同的公式。

这不会消除构造函数中的重复,除非您愿意切换到属性的默认初始化,然后调用set(0.0)