Let us look at the CObject first, and then try to design our own base class for MQL.
{
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;
};
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.
Last but not the least, every object should have a method to get its string represenation, and such thing is missing here.
{
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.
{
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):
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.
Here is a simple example, which demonstrates the whole idea.
{
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.
{
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.
{
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
}