configure this

The latest release of the presage predictive text entry library sports, among other things, a new and improved configuration layer which extends previous capabilities, adds new features and fills in a number of gaps and issues in the previous releases.

Prior to the refactoring work that went into 0.8.1, presage configuration mechanism was limited by the following issues:

  • configuration could only be read from a single profile file
  • runtime changes to configuration variables could not be persisted (written to) the profile XML
  • profile and configuration handling was tightly coupled to the XML profile file
  • changing some configuration variables at runtime had no effect on behaviour

These issues resulted in annoying limitations, such as the inability to modify configuration variables at runtime and saving those changes reflected in the runtime or saved to file.

To fix this, I refactored the entire configuration mechanism by first decoupling the classes reading the XML profiles and the actual runtime configuration classes. The overall design resulted in a ProfileManager class which creates Profile classes. Each Profile class is tied to an actual XML profile file (currently only XML profiles are supported, but a specific Profile class could abstract away from the actual representation and read file in other formats). Each Profile class retrieves configuration variables from the file store and call onto a Configuration class, which is a collection class containing Variable classes.

The beauty of the system is that it is built around the observer pattern. The Variable class implements the Observable interface.

class Observable {
public:
    virtual ~Observable ();
  
    virtual void attach (Observer* observer);
    virtual void detach (Observer* observer);
    virtual void notify ();

    virtual std::string  get_name () const = 0;
    virtual std::string get_value () const = 0;
   
protected:
    Observable ();

private:
    std::list  observers;

};

Each presage component that is interested in a specific configuration variable inherits from the Observer class and registers its interest on startup by attaching itself to the Observable object.

class Observer {
 public:
  virtual ~Observer () { };
  virtual void update (const Observable* changed_observable) = 0;

 protected:
  Observer () { };

};

When the Variable object changes, each Observable item notifies their Observers. Upon receiving a notification, each observer class handles the update by delegating to a templatized Dispatcher class, which maintains a mapping between Variable instances and pointers to class member methods. Therefore, each Dispatcher instantiation knows what class member method to invoke to handle received notifications.

template 
class Dispatcher {
public:
    typedef void (class_t::* mbr_func_ptr_t) (const std::string& value);
    typedef std::map dispatch_map_t;

    Dispatcher(class_t* obj)
    {
	object = obj;
    }

    ~Dispatcher()
    {
	for (std::list::iterator it = observables.begin();
	     it != observables.end();
	     it++) {
	    (*it)->detach (object);
	}

    }

    void map (Observable* var, const mbr_func_ptr_t& ptr)
    {
	var->attach (object);
	observables.push_back (var);
	dispatch_map[var->get_name ()] = ptr;
	dispatch (var);
    }

    void dispatch (const Observable* var) 
    {
	mbr_func_ptr_t handler_ptr = dispatch_map[var->get_name ()];
	if (handler_ptr) {
	    ((object)->*(handler_ptr)) (var->get_value ());
	} else {
	    std::cerr << "[Dispatcher] Unable to handle notification from observable: "
		      << var->get_name () << " - " << var->get_value() << std::endl;
	}
    }

private:
    class_t* object;
    dispatch_map_t dispatch_map;
    std::list observables;

};

For example, Selector component is interested in the SUGGESTIONS and REPEAT_SUGGESTIONS configuration variables. To accomplish that, Selector class has a Dispatcher object, instantiated as:

    Dispatcher dispatcher;

In its constructor, Selector attaches to each Variable he wants to receive notifications from Variable objects and how to handle them with a call to Dispatcher::map() method for each variable:

    dispatcher.map (config->find (SUGGESTIONS), & Selector::set_suggestions);
    dispatcher.map (config->find (REPEAT_SUGGESTIONS), & Selector::set_repeat_suggestions);

Finally, Selector implements the pure virtual method inherited from Observer, which simply delegates to Dispatcher to handle each notification:

void Selector::update (const Observable* variable)
{
    dispatcher.dispatch (variable);
}

In summary, the combination of the Observer pattern and a clever use of pointer to member methods in a Dispatcher class provides an elegant and powerful solution to the configuration problems outlined above.