Skip to main content
GameDev.net gamedev.net
🔒 Locked

Working around fragile base class syndrome

Started by antareus Nov 15, 2004 at 9:35 PM 9 replies 1.6k views
Original Post
antareus
antareus
I'm working on future-proofing my architecture. Currently, interested components can sign up for notification of events through a templated system that keys event handlers to a string. (I am considering GUIDs for the key, but that isn't the point). The data that is passed around within the event is usually either a simple type (int/bool/char*) or an abstract base class. The problem the abstract base class is brittle. When I go to refactor the base class, all the client code must be recompiled to reflect the modified vtable, or it will spew access violations left and right. This isn't suitable for a system that is designed to be extensible. It should be more failsafe than relying on the virtual function table matching up exactly. I considered only loading components that match the API 'version' so these sorts of inconsistencies could be detected, but that seems half-assed. Another solution is fairly creative: * Rewrite abstract base classes to be concrete * Base class implementation is little more than a call to GetProcAddress() with the 'polymorphic' function explicitly named (e.g. AccountConnect(void* acct, const char* server, unsigned short port)). Since all components are loaded from DLLs, the 'contract' between components changes from a brittle vtable to a string lookup managed by the OS. The behavior is still polymorphic -- but a little more code has to be written. Additionally, there is the issue of adding method calls and existing components not implementing them. This gives me pause. I thought it was an interesting take on the problem, and I'm sure its been implemented elsewhere, but I'd like to know what you think.
--God has paid us the intolerable compliment of loving us, in the deepest, most tragic, most inexorable sense.- C.S. Lewis
C-Junkie
C-Junkie
What kind of changes are you making to your ABC?
antareus
antareus
Well, I'd like the flexibility of adding additional methods to it without praying that adding them at the end of the base class definition won't shatter existing code. It shouldn't, but there isn't much of a guarantee there.

So I implement support for the AIM protocol first. Being a good little OOP boy I make the following ABC:
class Contact{virtual ~Contact() = 0;virtual const wchar_t* GetUserID() = 0;virtual const wchar_t* GetProfile() = 0;// blah blah blah};

Now, I move on to support ICQ, noticing that, hey, it supports profiles that are *much* more complex than a simple string.

However, Joe Random has written a component that expects GetProfile() in the Contact ABC to return a pointer to a wide character string - now I'm in a bind. I could go the foobar2k way and say "all your 0.7.1 plugins won't load they gotta be recompiled," but I get pretty angry when I upgrade software and find out all of the existing stuff doesn't work for it because the authors haven't upgraded it yet. (Firefox is a great example, I still don't have all the extensions updated).

With the name lookup method, at least I can provide a default value (0 maybe) if the 'polymorphic' method isn't found. Sure beats crashing or saying "you're outta luck, wait til the components are updated."

Like I said, it isn't foolproof, and the API version check is still probably essential if I make changes (like reordering parameter lists -- that's never safe!).
--God has paid us the intolerable compliment of loving us, in the deepest, most tragic, most inexorable sense.- C.S. Lewis
SiCrane
SiCrane
One option is to simply derive new interface classes from the existing base classes, and pass those around. Derived classes are more or less guaranteed not to change the vtable layout in the original base class. Just be careful not to accidently introduce multiple inheritance in the interface classes, or that could go out the window.

In your example, you could extend the Contact class with a Contact2 class, and add a GetProfile2() method that returns a pointer to a Profile class (or whatever).

This is similar in philosophy to some of the COM interface extensions. Though without the full blown complexities like QueryInterface() and all.
Phillip Martin
Phillip Martin
Your best bet to future proof your design is to never ever change your interface classes. It sounds like it wont do you any good, but it really is the only way to go.

Once you have released a version of the software, that set of interfaces is forever frozen to ensure binary compatability. If at any time you need more functionality, you simply provide more interfaces for plugins to provide implementations for.

In your example, you have a Contact interface, but you wanted to add information to it. Doing so will break binary compatability as you rightly pointed out. In the next release of your software, you allow plugins to hand out implementations of a Contact2 or DetailedContact or WhateverContact interfaces.

An excellent API to look at for examples is the DirectX API. The whole thing maintains backwards compatability of interfaces. Whenever new functionality or modifications are required, a new interface is made.

Edit: I just noticed that SiCrane said pretty much the same thing. Sorry for the duplication.
Teknofreek
Teknofreek
It sounds like, in your specific case, that perhaps the easiest solution would be to simply add an extra level of indirection in your base classes. If you replace all the simple types it uses(eg. returning wchar_t*) with a new set of base classes(eg. IUserID, IProfile, etc) then you should be able to create your main ABC's in a way that you will rarely, if ever, need to refactor them.

I don't know if this will solve your problem entirely, but I don't think it could hurt :)

-John
John
d000hg
d000hg
Quote:
Original post by Phillip Martin
Your best bet to future proof your design is to never ever change your interface classes. It sounds like it wont do you any good, but it really is the only way to go.

Once you have released a version of the software, that set of interfaces is forever frozen to ensure binary compatability. If at any time you need more functionality, you simply provide more interfaces for plugins to provide implementations for.

In your example, you have a Contact interface, but you wanted to add information to it. Doing so will break binary compatability as you rightly pointed out. In the next release of your software, you allow plugins to hand out implementations of a Contact2 or DetailedContact or WhateverContact interfaces.

An excellent API to look at for examples is the DirectX API. The whole thing maintains backwards compatability of interfaces. Whenever new functionality or modifications are required, a new interface is made.

Edit: I just noticed that SiCrane said pretty much the same thing. Sorry for the duplication.
I agree. Once somebody other than the development team gets hold of the software, you have to accept you're stuck with those interfaces. As long as you don't vastly need to extend it you should be fine (otherwise you'll get ObjectInterface142!)
antareus
antareus
Quote:
As long as you don't vastly need to extend it you should be fine (otherwise you'll get ObjectInterface142!)

Yeah, nothing like AOL-inspired class names. *shudder*

I like the one additional layer of indirection idea. Good call.

The QI idea is interesting. I am not sure if I am a huge fan of it, however. I meant to mention that this sort of 'DLL polymorphism' is also a step towards language independence, since most languages can call C functions. Obviously I don't value language independence a lot or else I would be using COM -- but moving towards a C/C++/anything that can mingle with C approach isn't a terrible hassle.

One stylistic question:
Some methods in my interface do not immediately return results, but rather raise events when they have completed. What is a good way to distinguish them?

Code explanation:
class Account{public: // This returns immediately with a result. virtual const wchar_t* GetUserID() = 0; // This checks with the server first and raises // AccountUpdateEvent when it is done. At a quick // glance it may appear that the operation occurs // immediately, which is erroneous. virtual bool SetVisibility(bool visible) = 0;};


I was thinking something like BeginSetVisibility() or the like, but it seems a bit kludgy.
--God has paid us the intolerable compliment of loving us, in the deepest, most tragic, most inexorable sense.- C.S. Lewis
SiCrane
SiCrane
I usually stick an "Async" in the function name, or name it something like "Set****Mode" depending on the context. The Async name I use when the function is intended to raise a finite number of events, and Set****Mode when it is intended to change the long term behavior of the container.

So in the case of your sample function, based on the comments, I would probably call it SetVisibilityAsync().

I wouldn't worry about it being kludgy; delayed operations like this should have big neon signs pointing them out.
antareus
antareus
Suffixing "Async" looks to be the way to go.

And now, less theory, more code. Here's a little class I wrote that is meant to be privately inherited from. I have a later version that is suitable for aggregation (private inheritance is kind of dumb, but whatever.) Hopefully this is useful for someone or sparks some ideas.

Source
Beta-quality code. Build your 'base class' atop this and make the real calls using the templated CallDLLFunction member function, passing in arguments if necessary.

To do:
* Error handling policy
* Caching mechanism for function names (that is, if GetProcAddress() doesn't cache name lookups)
* Contains() member function to get a yes/no answer on whether a particular function is implemented.
* Change class name? Current one is kind of silly.

[Edited by - antareus on November 17, 2004 9:09:17 AM]
--God has paid us the intolerable compliment of loving us, in the deepest, most tragic, most inexorable sense.- C.S. Lewis

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.