Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to design this program with easy modification in mind?

Tags:

c++

This question is about how to design a program so it will be easy to make certain modifications.

I have a class which holds some (non-trivial) data and has several member functions that change this data.

Sometimes I need to compute some property of this data. But it is slow to recompute it from scratch on every change. It is much faster to compute a small update to these properties instead.

I have several such properties which I need to be able to easily add or remove to/from my class (or turn on/off) to carry out some numerical experiments. The class is only modified by myself and is used for numerical simulations (scientific code).

Concrete example

Let's say I have a class that holds a number x. But I also need 2^x (a "property" of x). The basic class is:

class C {
    double x;

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

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

But now I need to keep track of 2^x and to keep updating this value every time x changes. So I end up with

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 marks changes I needed to make to the class C. That's a lot of changes, which is error prone. It becomes even more complicated with several properties, which my even depend on each other.

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
    }
};

Question: What is a good design for such a program? I'm sure it's possible to do better than the above. The goals are: 1) Make it easy to add/remove such properties or turn on/off their computation 2) Performance is critical. inc and dec are called in tight inner loops and do relatively little. They cannot be made virtual for performance reasons.

In reality x is a more complex data structure. Think e.g. adding/removing edges to/from a graph and keeping track of its degree sequence during the process.


Update

@tobi303 asked that I show how this class would be used. It's in a manner similar to this:

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();
    }
}

Or in words:

  • make a (random) change
  • get property values after the change
  • depending on the new property values, decide whether to accept or undo the change.

It's actually a Monte-Carlo simulation similar to a Metropolis-Hasting algorithm.

A concrete example could be where the "data" in class C (state) is the spin state of an Ising model (for those familiar with it) and properties are the total energy and total magnetization of the system. These are much faster to update after a single spin flip than to recompute from scratch. In practice I don't have an Ising model, I have something a bit more complicated. I have several properties, some fast to compute and some slow (actually I have some auxiliary data structures that help compute properties). I need to experiment with combinations of different properties, so I often change what I include in the code. Sometimes I implement new properties. When I don't need an already implemented property, I need to be able to turn off its computation for performance reasons (some are really slow to compute).

like image 355
Szabolcs Avatar asked May 24 '16 16:05

Szabolcs


People also ask

What are the 5 steps of the design thinking process?

The short form of the design thinking process can be articulated in five steps or phases: empathize, define, ideate, prototype and test.

What is design thinking program?

Design Thinking and Innovation will teach you how to leverage fundamental design thinking principles and innovative problem-solving tools to address business challenges and build products, strategies, teams, and environments for optimal use and performance.

What is design thinking mind set?

Design thinking is a deeply human process that taps into abilities we all have but get over-looked by more conventional problem-solving practices. Design thinking requires an experimental, collaborative, and optimistic mindset. We define mindset as the ideas and attitudes with which a person approaches a situation.


2 Answers

Just be lazy and don't calculate the properties when you need to. It will remove plenty of code and unnecessary computation.

When you do need your property, compute it if it's not already in cache. So you need a boolean for each property to tell if the cache is up-to-date, and you need to invalidate the booleans each time x itself is updated.

Basically:

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();
    }
};

Edit: an even lazier solution is to instead of storing a boolean in cache, store an integer correponding to the last update and maintain a counter in C. Each time the cache is invalidated, the counter in C is increased. When getting propX, if the counter doesn't match propX.lastUpdate then update `propX.

That way, invalidating cache is just one operation and doesn't have to update all the properties' cache.

like image 160
coyotte508 Avatar answered Oct 08 '22 09:10

coyotte508


Here is an approach that may work for you:

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...);
        });
    }
};

When you add a new property, you only need to add one call in for_each_property(). The use of variadics avoids the need to provide new overloads for different parameters as long as you stick to the same formula.

This doesn't eliminate the duplication in the constructor, unless you are willing to switch to doing default initialization of the properties and then call set(0.0).

like image 38
Vaughn Cato Avatar answered Oct 08 '22 11:10

Vaughn Cato