Design Pattern: Variant
Design Pattern: Variant
Copyright © Ernest S. Pazera Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included here. The IssueI have a class that handles messages, based in part on the "Chain of Responsibility" design pattern. A very watered down possible implementation for my message handler class would look something like this: class IMessageHandler { private: IMessageHandler* m_pmhParent; protected: virtual bool OnMessage(int iMessageID,void* pvMessageParms){return(false);} public: CMessageHandler(IMessageHandler* pmhParent):m_pmhParent(pmhParent){} ~CMessageHandler(){SetParent(0);} void SetParent(IMessageHandler* pmhParent){m_pmhParent=pmhParent;} IMessageHandler* GetParent(){return(m_pmhParent);} bool HasParent(){return(GetParent()!=0);} bool HandleMessage(int iMessageID,void* pvMessageParms) { if(OnMessage(iMessageID,pvMessageParms)) return(true); else if(HasParent()) return(GetParent()->HandleMessage(iMessageID,pvMessageParms)); else return(false); } }; As you can see, IMessageHandler has a single member, called m_pmhParent. This stores a pointer to the message handler's parent. If m_pmhParent is 0(i.e. a null pointer), then the message handler is said to have no parent, and would be at the root of the tree. There is, of course, no limitation to the number of message handlers that can call another message handler its parent. In fact, at least in this implementation, there is no limitation restricting a message handler from being its own parent (but this could be simply fixed in the SetParent function, so for now ignore it). SetParent and GetParent are obviously a way of managing the assignments of parents to message handlers. The HasParent function is a shorthand to keep us from having to write if(GetParent()!=0) all the time. The constructor sets up a message handler with an initial value for its parent, and the destructor eliminates that value, resetting the parent pointer to 0 (not that it really matters in this implementation). The interesting functions are OnMessage and HandleMessage. Since OnMessage is virtual (and protected so that it cannot be called directly), we can assume that it is meant to be overridden in derived classes. This implementation simply returns false, as IMessageHandler knows nothing about how to handle any sort of message whatsoever. In another implementation, this function should perhaps be made pure virtual, as IMessageHandler itself should not be instantiated as it does nothing useful. HandleMessage is where the "glue" between message handlers lies. It first tries to handle the message with its own OnMessage function. Failing that, it checks for a parent. If a parent exists, it passes the message on down to the parent, who repeats the process until the message is handled or the top of the chain is reached and the message is still unhandled, in which case false is returned. Thus far, this solution is the best that I have found for basing my applications on. My application object is a subclass of IMessageHandler, as are various types of event handlers (this concept is often represented by a window), and I have also used it for finite state machines and its component states, custom user interfaces, and so on. Typically I go with a richer set of operations, like child management, but on some platforms, IMessageHandler has looked almost exactly like the implementation I have here. So, what exactly is the problem? My main issue is the parameter list for HandleMessage and OnMessage. Both take an int (which identifies the type of message being sent), and a void pointer, which is the most generic way I could come up with to send a variable amount of data that I could cope with. I could have gone the " " route with these functions, but I try my best to stay away from " ", as it is even more difficult to manage than a void pointer parameter, at least in my opinion. Even with the int and void*, there are huge problems that crop up: Problem #1: the int parameter, iMessageID, is the parameter used to differentiate types of messages. How, then, shall we ensure that our applications do not duplicate message ids, since these must be unique? Often, when defining derived classes of IMessageHandler, we will be assigning values to message ids that will wind up hidden in the implementation. If, for example, we create a custom UI and create a CButton class to represent a command button, we will naturally want a message id reserved for clicking a button. As a developer, I don't really care what the values are for a particular message id, and I really shouldn't have to care. I simply want to use some sort of identifier to send and check for messages. Problem #2: a void* parameter is a poor way to store a variable number of parameters, especially in a strongly typed language like C++. It approaches being as bad as using " ". Many types of messages will have no parameters at all, which makes the parameter a waste. A quit message sent to an application needs no additional data. If we were simply concerned about overrunning the data buffer, we could add a size_t parameter that specifies how large the data being sent is, but that just compounds the useless parameter issue. Solution?The requirements here can be broken down into the following: Requirement #1: Messages should be represented by a fixed number of parameters, and as few as possible, without ever having wasted parameters.
To meet requirement #1, we shall limit the number of parameters to just one, so it is becoming obvious that we shall be creating a message class of some sort, which we shall pass either by pointer or reference, but we won't lock ourselves down to one or the other yet. We will need a base class for all other types of messages, which I will call MGeneric (M standing for Message), and so HandleMessage and OnMessage shall either be passed a MGeneric* or an MGeneric&. While taking care of requirement #1, we have fulfilled requirement #2 (since we shall have a sole parameter of type MGeneric* or MGeneric&, we naturally shall not have a parameter of type void* nor a " ") and requirement #5 (through inheritance, MGeneric can be subclassed to represent any amount of data we wish). However, this leads to the question of how exactly we will access the extended data from derived classes. Naturally, this will have to occur through some sort of casting. Typically, I make use of dynamic_cast for this, and because of this choice, I favor MGeneric* over MGeneric& as the parameter type (since a failed dynamic_cast of a pointer returns the a null pointer, whereas with a reference, a bad_cast exception is thrown-so pointers are easier). So, here's a simple implementation for MGeneric: typedef unsigned long messageid_t; class MGeneric { private: messageid_t m_messageid; protected: void SetMessageID(messageid_t messageid){m_messageid=messageid;} public: MGeneric(messageid_t messageid):m_messageid(messageid){} ~MGeneric(){} messageid_t GetMessageID(){return(m_messageid);} }; Within MGeneric is the minimal amount of functionality needed to represent a message, namely the identifier (a messageid_t value, which is just an alias for unsigned long, a type sufficiently large enough for most purposes, but your mileage may vary). The SetMessageID member function is protected, so that only friends and derived classes may access it (for encapsulation purposes). GetMessageID is public, and will typically be the first value checked in an IMessageHandler::OnMessage function, usually in a chain of if/else if blocks. Now we need to modify IMessageHandler::HandleMessage and IMessageHandler::OnMessage like so: bool IMessageHandler::HandleMessage(MGeneric* pMessage,bool bDelete=true) { bool bResult=false; if(pMessage!=0) { if(OnMessage(pMessage)) bResult=true; else if(HasParent()) bResult=GetParent()->HandleMessage(pMessage,false); if(bDelete) delete pMessage; } return(bResult); } This function is now made a little more complicated by the fact that pMessage is an MGeneric*, so we have to cleanly handle null pointer values, and since the pointer is likely going to be dynamically allocated within a function call, we have to do some garbage collection (hence the bDelete parameter). Yes, I know I made a big to-do about useless parameters, but this one isn't useless, I guarantee you. For example, consider the following. Would you rather write code like this: pHandler->HandleMessage(new MGeneric(0)); -OR- MGeneric message(0); pHandler->HandleMessage(&message); Sending a message, any message, is logically a single action, and single actions should take place on a single statement, which the first option gives you, and the second does not. There is nothing really wrong with either approach, I just prefer the former, which means I need a bDelete parameter. The OnMessage function remains nearly the same(still virtual, still protected): bool IMessageHandler::OnMessage(MGeneric* pMessage) { return(false); } The only change was the parameter list. It now has but one parameter, an MGeneric*. So we've got three of the five requirements out of the way, we still need a way of assigning unique messageid_t values, and a way to keep them hidden from the user. I do it with a class that makes use of the factory patter and singleton pattern. I call the class FMessageID: class FMessageID { private: static messageid_t s_messageid_next=0; static bool s_check_overflow=false; public: static messageid_t Next() { if(s_check_overflow) { if(s_messageid_next==0) { //overflow! } } else { s_check_overflow=true; } return(s_messageid_next++); } }; Each time the user calls FMessageID::Next(), he gets a new number, starting with 0. If, for whatever reason, it gets back around to 0 again, then an overflow occurs (typically accompanied by some sort of exception being thrown). This is taken care of my the s_check_overflow variable, which skips the overflow check only once when 0 is first being assigned. As for how we start to attach these messageid_t values to classes, we just add them as static members to the classes that they pertain to. For example, if we created a subclass of IMessageHandler called IApplication, and wanted to associate a messageid_t with a quit message, this is how we would go about it (ignoring all of the OTHER stuff that would go into an application class): class IApplication: public IMessageHandler { private: static messageid_t s_MSGID_Quit=FMessageID::Next(); public: static messageid_t MSGID_Quit(){return(s_MSGID_Quit);} }; We make the actual variable private, so that it cannot be interfered with accidentally, and then access the value from the static member function MSGID_Quit, making it a read-only property very effectively. As for what the actual value of IApplication::s_MSGID_Quit, I have no idea, and more importantly, I don't care. It could be 0, it could be 500, and it doesn't really matter. As long as it is uniquely identified by a call to IApplication::MSGID_Quit(), I'm happy. Other UsesTruth be told, the solution I have presented here was not originally implemented for messages. It was developed as a solution for exception classes. The solution looks rather a lot like MGeneric and FMessageID(so I'll spare you the code, and just fill in what is different), with the corresponding classes being EGeneric and FErrorID and errorid_t is used instead of messageid_t. In addition, EGeneric contains an additional member of type std::string that contains a textual description of the exception. For most exceptions, a simple error id and a string is all that is needed, but of course inheritance can extend them. Additional uses I have found for this pattern are useful in AI. Imagine a generic action class called AGeneric, with actionid_t values (assigned, of course, by the FActionID class) such as AXNID_North, AXNID_South and so on. Also imagine a generic goal class, GGeneric, with goalid_t values assigned by the FGoalID class, representing both types of goals (like GOALID_Kill), priorities of goals, and extended information about the goal (like who to kill). Goals can then be placed into priority queues, and used to decide on a sequence of actions. ConclusionIn my lexicon of design patterns, the one that comes closest to doing what this one does is Command, but I sort of think of it as being called "Variant". I have found it to be a useful pattern, and I imagine others would as well, and so I'm sharing it. Discuss this article in the forums
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|