The problem
Suppose you have an application which provides "core" functionality for other developers. Suppose you want to expose functionality as C++ classes, rather than via APIs. Suppose you want to enable extension of those classes by third-parties. What are the pitfalls? How do you avoid them?
Discussion
Let's say you expose functionality via a class cBase. This class can be used "as is", but is really intended to be used as a base class for other classes (created by third-parties):
class cNew : public cBase {...}
The third-party code is going to create cNew objects (your code doesn't know about them), and when your code interacts with these objects you can only treat them as cBase objects, not cNew objects (because, again, you don't know about cNew).
Therefore you have to make any methods you intend to make overrideable as virtual methods:
class cBase {
public:
virtual ~cBase(); // make destructor virtual
virtual void myMethod(); // a virtual method
Now in your code you deal with cBase pointers, which are really pointing to cNew objects:
cBase *pObj; // pointer to cBase / cNew
pObj->myMethod(); // could call cNew's method
Virtual methods are called indirectly through a vector table in the object; if the pObj pointer was pointing to a cNew object, then the myMethod() pointer would have been initialized to point to cNew's myMethod by the cNew constructor.
This is the intent of virtual functions, and it works great. So...
Back to the problem
Now suppose you've released cBase into the field, and a bunch of third-party developers have created various cNew classes based upon it. You cannot change the interface to cBase in any way! Nope, not even if you're careful to keep all the old methods backward-compatible. The problem is that if you add a new virtual method to cBase, its vector table is going to get bigger. The vector table in objects made by existing third-party code will be incorrect (because it won't have the correct number of vectors for cBase).
It is really unlikely that you'll never want to enhance cBase. Furthermore, it is even more unlikely that you'll be able to distribute an updated cBase header to each third-party developer, and it is astronomically unlikely that they'll update their code and successfully distribute it to each of their users. In other word, backward-compatibility with "old" third-party code is essential. What do you do?
The solution
The solution is simple - you extend cBase by deriving from it. You create a new class for your code's use:
class cBase2 : public cBase {...}
In your code, anywhere you can you should assume you have a cBase object, as in the example above. If you have code which must "know" whether you have a cBase2 object, then you have to ask the object what it is. For this you need a getVersion() method. And you have to build this into cBase:
class cBase {
public:
virtual int getVersion(return 1); // returns cBase version
Note that getVersion() is virtual. Later, when you create a cBase2, you can do it like this:
class cBase2 : public cBase {
public:
virtual int getVersion(return 2); // returns cBase version
virtual void newMethod(); // method only in cBase2
Later there may be a cBase3, and a cBase4, and they will all return different versions.
Now that we have getVersion(), we can write intelligent code to behave appropriately with both "old" and "new" objects:
cBase *pObj; // object pointer
if (pObj->getVersion() == 1) { // if object based on cBase
// do something intelligent with "old" object
}
else {
cBase2 pObj2 = (cBase2*) pObj; // cBase2 pointer
pObj2->newMethod(); // now can call new method
}
This way you will always provide support for those "old" third-party objects that may be out there. And you can enhance the support for the "new" objects. Having created cBase2, what do you do with your third-party developers?
Simple: you distribute the cBase2 header (which includes the cBase header) to all third-party developers. Any developer who wants can take advantage of the new capabilities by deriving from cBase2:
class cNew : public cBase2 {...}
And the old school developers who don't want or need the new functionality can continue to derive from cBase, with no loss of support.
This technique benefits your developers, too; they might well have modules which need to detect "old" modules at customer sites, and can use the getVersion()method to do it.
That's it - please let me know if you have questions or suggestions.
|