MQL's OOP notes: Multiple timers with publisher/subscriber design pattern and abstract class

23 September 2016, 12:34
Stanislav Korotky
0
82
Every expert and indicator in MetaTrader can have a timer. From time to time a novice MQL developer asks a question: what should I do if I need many timers? The answer is simple: use a single timer with lesser period to manage and dispatch all other timer events with larger periods. Thanks to OOP this can be done easily using a well-known design pattern "publisher/subscriber".

The example will demonstrate the whole idea based on EventSetTimer function which supports intervals starting from 1 second. You can modify it for more fine-grained timers using EventSetMillisecondTimer

Let's start from a class which defines a ground-based astraction of a timer. Usually it is called interface and comprises only pure virtual methods, that is virtual methods without implementation (see below). But in this case we will use an abstract class with a bit of fundamental timer-related data and logic. Nevertheless it's still an abstract class, that is a class which can not be instantiated (only objects of derived classes with concrete implementations of pure virtual methods can be created).

Surely the class should have a method to signal the timer event and a member variable storing a time of previous notification.

class TimerNotification
{
  private:
    datetime lastNotification;
    
  public:
    TimerNotification(): lastNotification(0)
    {
    }
  
    virtual void notify()
    {
      lastNotification = TimeLocal();
    }
In the notify method we save the time when it's called last time. Next method should return a number of seconds in timer period.
    
    virtual int getSeconds() const = 0;
This is a pure virtual method. It requires an implementation in a descendant class in order to create an object.

Timer period is the main and the only property of a timer, but we do not define a variable for it in the abstract class - insteed we allow derived classes to provide this information via the virtual method, and manage the period on their own, including probably a floating period with changes in time.

Finally we need a method to detect when the timer event should fire. Here is where we use the method getSeconds returning data from descendants.  
    
    bool isTimeCome()
    {
      return lastNotification + getSeconds() < TimeLocal();
    }
};
All looks fine except for the nuance that if the method notify will not be called in descendants, the variable lastNotification will remain 0 and the checkup in isTimeCome will work incorrectly. We need somehow check if descendants call the base implementation of notify from their overriden versions. For that purpose we add a new variable and more code. Here is the complete code.

class TimerNotification
{
  private:
    datetime lastNotification;
    bool checkedNotifyCall;
    
  public:
    TimerNotification(): lastNotification(0), checkedNotifyCall(false)
    {
    }
  
    virtual void notify()
    {
      lastNotification = TimeLocal();
    }
    
    virtual int getSeconds() const = 0;
    
    bool isTimeCome()
    {
      if(lastNotification == 0)
      {
        if(!checkedNotifyCall)
        {
          checkedNotifyCall = true;
          return true;
        }
        else
        {
          Print("ERROR: call to TimerNotification::notify() is missing in concrete timer class");
        }
      }
        
      return lastNotification + getSeconds() < TimeLocal();
    }
};

If descendant "forgets" to invoke the base notify from its own notify, the error will be shown on logs.

Now, having this abstract class we can implement a worker class of a timer derived from it.

class Timer: public TimerNotification
{
  private:
    int seconds;
    
  public:
    Timer(const int s)
    {
      seconds = s;
    }
    
    virtual int getSeconds() const
    {
      return seconds;
    }
    
    virtual void notify()
    {
      Print(__FUNCSIG__, seconds);
      TimerNotification::notify();
    }
};

The constructor accepts a number of seconds as the timer period and then returns this number from getSeconds. Do not forget to call notify method of the base class.

For this timer to work we need a kind of a timer manager, which would call every second isTimeCome method and if it returns true, then fire the timer via notify. Actually it should do so for all existing timers. Here is where the "publisher/subscriber" design pattern comes into action.

class MainTimer
{
  private:
    TimerNotification *subscribers[];

This array will store pointers to all timers which registered themselves in the manager. 

  public:
    MainTimer()
    {
      EventSetTimer(1);

We use 1 second period here as the least possible period for timers with second granularity. If you plan to work with milliseconds consider the value carefully, because too small value may increase CPU overheads.  

    }
    
    ~MainTimer()
    {
      EventKillTimer();
    }

Finally - the method to add a timer object in the internal array. This is a subscription.

    void bind(TimerNotification &tn)
    {
      int i, n = ArraySize(subscribers);
      for(i = 0; i < n; ++i)
      {
        if(subscribers[i] == &tn) return; // already added
        if(subscribers[i] == NULL) break; // empty slot
      }
      
      if(i == n) // no empty slots, extend the array
      {
        ArrayResize(subscribers, n + 1);
      }
      else // use empty slot (if any)
      {
        n = i;
      }
      subscribers[n] = &tn;
    }

The next method does the contrary - unsubscribes (disables) a timer.

    void unbind(TimerNotification &tn)
    {
      int n = ArraySize(subscribers);
      for(int i = 0; i < n; ++i)
      {
        if(subscribers[i] == &tn)
        {
          subscribers[i] = NULL; // mark the slot as empty
          return;
        }
      }
    }

And this is the method which should be called from OnTimer event handler in global context. It publishes information for existing subscribers. The timer manager class is the publisher.

    void onTimer()
    {
      int n = ArraySize(subscribers);
      for(int i = 0; i < n; ++i)
      {
        if(CheckPointer(subscribers[i]) != POINTER_INVALID)
        {
          if(subscribers[i].isTimeCome())
          {
            subscribers[i].notify();
          }
        }
      }
    }
};

Now we need to go back to our Timer class and insert some lines in order to make subscription/unsubscription. The final variant of code is the following:

class Timer: public TimerNotification
{
  private:
    int seconds;
    MainTimer *owner;
    
  public:
    Timer(MainTimer &main, const int s)
    {
      seconds = s;
      owner = &main;
      owner.bind(this); // subscribe itself
    }
    
    ~Timer()
    {
      owner.unbind(this); // unsubscribe itself
    }
    
    virtual int getSeconds() const
    {
      return seconds;
    }
    
    virtual void notify()
    {
      Print(__FUNCSIG__, seconds);
      TimerNotification::notify();
    }
};

To test the classes write a simple example with 2 timers.

const int seconds = 2;

MainTimer mt();
Timer t1(mt, seconds);
Timer t2(mt, seconds * 2);

void OnTimer()
{
  mt.onTimer();
}

The output looks like this:

0 01:40:56.272 : void Timer::notify()2
0 01:40:58.301 : void Timer::notify()4
0 01:40:59.315 : void Timer::notify()2
0 01:41:02.357 : void Timer::notify()2
0 01:41:03.414 : void Timer::notify()4
0 01:41:05.399 : void Timer::notify()2
0 01:41:08.441 : void Timer::notify()2
0 01:41:08.441 : void Timer::notify()4

The timer with 2 seconds period is fired 2 times faster than the timer with 4 seconds period. Notifications do not come exactly with 1 second accuracy, but this is normal for standard EventSetTimer. You can make sure that every next notification comes not ealier than the specified number of seconds for every timer.

Bingo.

 

Table of contents

 

Files:
timer.mq4 4 kb
Share it with friends: