MQL's OOP notes: Object hierarchies and serialization

21 October 2016, 13:28
Stanislav Korotky
6
704
When it comes to OOP, there should be an hierarchy of classes. And most of hierachies are built from a single base class. This is very powerful feature. It allows you to store objects in various containers, such as vectors, queues, lists, and process them consistently in batch tasks. MQL standard library does also include such a class called CObject. Actually I don't like the library and don't use it, but the idea about classes with a common root is worth a thorough consideration. BTW, many existing languages, such as Java, do also provide a base class, so anyone can investigate them and take some useful hints.

Let us look at the CObject first, and then try to design our own base class for MQL.

class CObject
{
  private:
   CObject          *m_prev;               // previous item of list
   CObject          *m_next;               // next item of list

  public:
                     CObject(void);
                    ~CObject(void);
   CObject          *Prev(void) const;
   void              Prev(CObject *node);
   CObject          *Next(void) const;
   void              Next(CObject *node);
   // methods for working with files
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   // method of identifying the object
   virtual int       Type(void) const;
   // method of comparing the objects
   virtual int       Compare(const CObject *node, const int mode = 0) const;
};
I have no idea why the object contains two pointers for next and previous items. Objects can be out of any list, and these members are of no use. I'd say that for list items there should be a dedicated class - name it Item, for example - derived from the base root. Then the pointers would belong to it. Actually this is a bad idea to have member variables in the root class. It should be as much abstract as possible.

The methods working with files are also inapproriate here. I agree that the object should have methods for serialization and deserializtion - that is saving and restoring it to/from some persistent storage. But why this feature is bound to files and specifically to file handles? What if I like to save object in global variables or on my server via WebRequest? What if someone will pass a handle of a file opened for reading into Save method? And what if the file is in text mode but object wants to write binary data? Such base class can become a cause for many potential errors. 

The method Type has a misleading name, imho. As its comment says, this is a kind of unique identifier of an object, so I'd change the name to something more appropriate. In Java corresponding method is called hashCode.

Last but not the least, every object should have a method to get its string represenation, and such thing is missing here.

After this short criticizm, we can finally develop our base class.  

class Object
{
  private:
  public:
    virtual string toString() const {return classname() + "#" + StringFormat("%d", &this);}
  
    virtual bool save(OutputStream &out) const {return false;}
    virtual bool load(InputStream &in) {return false;}

    virtual long version() const {return 0;}
    virtual string classname() const = 0;    
    virtual long signature() const {return 0;}
    
    virtual int compare(const Object &other, const double customValue = 0) const
    {
      return (int)(signature() - other.signature());
    }
};

You may ask what are the classes OutputStream and InputStream. Here is a draft.

class OutputStream
{
  public:
    virtual bool writeBoolean(const bool b) = 0;
    virtual bool writeByte(const uchar c) = 0;
    virtual bool writeInt(const int i) = 0;
    virtual bool writeLong(const long l) = 0;
    virtual bool writeString(const string &s) = 0;
    virtual bool writeDouble(const double d) = 0;
    virtual bool writeDatetime(const datetime t) = 0;
};

class InputStream
{
  public:
    virtual bool readBoolean() = 0;
    virtual uchar readByte() = 0;
    virtual int readInt() = 0;
    virtual long readLong() = 0;
    virtual string readString() = 0;
    virtual double readDouble() = 0;
    virtual datetime readDatetime() = 0;
};

class TextFileStream: public OutputStream
{
};

class BinaryFileStream: public OutputStream
{
};

class GlobalVariableStream: public OutputStream
{
};

I hope the idea behind them is clear now. They are abstract input/output "interfaces" without dependency to a concrete storage type. The object stored in such external streams can be restored lately using InputStreams. The set of writeXXX functions can be extended with a templatized function for convenience (please note, it's applicable only for textual representation, as it uses string):

    template<typename T>
    bool write(const T t)
    {
      return writeString((string)t);
    }

Now lets us return back to the Object class. Every object has a specification, including - but not limited to - which data does it contain. If you're going to save an object instance for longer persistence (for example, between teminal sessions) you should think about possible changes in specification. If you have a stored object and then extend its class with a new field which should be stored as well, then the old object will lack the new field, and your code should probably perform special actions for proper object initialization. This is why we have the method version - it allows you to return version number of current object specification and compare it with a version of any object being restored from an external storage.

The method classname is used to provide a readable identifier of the class. Most simple and suitable implementation can be just typename(this). Finally we have the method signature, which is an identifier of the object (this is the analogue of Java's hashCode). If 2 objects contains the same data they should return the same signatures. This makes it simple to compare objects. The base implementation is shown in the compare function. It's coded in such a way that compare can be used not only to detect if objects are different but also to order them by some property (the property could be specified by customValue, for example). This is useful for sorting, but requires a proper implementation of the signature method in the sense of "measuring" object "value" consistently.

Here is a simple example, which demonstrates the whole idea.

class Pair : public Object
{
  public:
    double x, y;
    Pair(const double _x, const double _y): x(_x), y(_y) {}
    Pair(const Pair &p2)
    {
      x = p2.x;
      y = p2.y;
    }
    Pair(const Pair *p2)
    {
      if(CheckPointer(p2) != POINTER_INVALID)
      {
        x = p2.x;
        y = p2.y;
      }
      else
      {
        x = y = 0.0;
      }
    }
    Pair(): x(0), y(0) {}
    
    virtual string classname() const
    {
      return typename(this);
    }
    
    virtual bool save(OutputStream &out) const
    {
      out.write(classname() + (string)version());
      out.write(x);
      out.write(y);
      return true;
    }
    
    virtual bool load(InputStream &in)
    {
      string type = in.readString();
      if(type != classname() + (string)version())
      {
        Print("Class mismatch:", type, "<>", classname() + (string)version());
        return false;
      }
      x = in.readDouble();
      y = in.readDouble();
      
      return true;
    }
    
    virtual int compare(const Pair &other, const double customValue = 0) const
    {
      return !(x == other.x && y == other.y);
    }
};

This class comprises 2 fields, which can be serialized and deserialized. At the beginning of every object stream we output the class name and version. When the object is being read from a stream, we check if the class name and version correspond to current values. In a real world case one should somehow upgrade the loaded objects with a lesser version up to the current version.

Another class can contain the Pair object and save/load all its own fields as well as the nested object.

class Example : public Object
{
  private:
    int index;
    double data;
    string text;
    datetime dt;
    Pair pair;

    ...
    
    virtual bool save(OutputStream &out) const
    {
      out.write(classname() + (string)version());
      out.write(index);
      out.write(data);
      out.write(text);
      out.write(dt);
      out.write(&pair); // pair.save(out);
      return true;
    }
    
    virtual bool load(InputStream &in)
    {
      string type = in.readString();
      if(type != classname() + (string)version())
      {
        Print("Class mismatch:", type, "<>", classname() + (string)version());
        return false;
      }
      index = in.readInt();
      data = in.readDouble();
      text = in.readString();
      dt = in.readDatetime();
      pair.load(in);
      
      return true;
    }

The complete source code is attached. Place it in MQL4/Scripts folder. The code provides different stream classes to store objects to the log (as a debug output) or text files. For example, we can save an object to a file and then restore it.

void OnStart()
{
  LogOutputStream log;  // this is just for debug purpose

  Example e1(1, 10.5, "text", TimeLocal());
  Print(e1.toString());
  e1.save(log);
  
  TextFileWriter tfw("file1.txt");
  e1.save(tfw);         // serialize object to the text file
  tfw.close();          // release file handle
  
  TextFileReader tfr("file1.txt");
  Example e2;
  e2.applyPair(-1, -2); // apply some changes, just to make sure load will override them
  e2.load(tfr);         // deserialize the text into another object instance
  Print(e2.toString());
  e2.save(log);
  
  Print("Is equal after restore:", (e1 == e2));      // true, obects are identical
  e2.applyPair(5, 6);
  Print("Is equal after modification:", (e1 == e2)); // false
}


 

Files:
objserial.mq4  9 kb