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

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.
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(); }
virtual int getSeconds() const = 0;
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.
bool isTimeCome() { return lastNotification + getSeconds() < TimeLocal(); } };
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.
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.
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:
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.