Message Factory and Message Interface Design Pattern
Intent
Distributed systems involve several communicating processors. The interfaces
between these processors are often governed by interface documents and standard
header files. The main objective of the Message Interface Design
Pattern is to decouple the software from its external interfaces. This pattern
also allows for a smoother role out of interface by maintaining backward
compatibility.
Also Known As
- Interface Agent
- Message Handler
Motivation
Software developers of distributed system resist changes in interfaces
between modules. This is mainly due to:
- Making interface changes requires scheduling releases with the updates at
the same time. This can be a logistic nightmare when teams working on
different modules are not at the same location.
- Interface handling code and main logic of the module often get
intertwined. Thus making interface changes difficult.
- Many times processors have different byte ordering and byte alignment
conventions, this complicates interface design. (See the article on byte
alignment and ordering)
This pattern addresses these issues as mentioned below:.
- The interfaces are defined to support backward compatibility, this reduces
the effort involved in scheduling two independent software releases at the
same time.
- The code for interface handling is decoupled from regular code, thus
simplifying interface upgrades.
- Interfaces are packed to network conventions when sending the message. The
messages are unpacked to native format when they are received.
Applicability
This pattern can be used in the following cases:
- Modeling message interfaces with another processor within the system when
the messages are packed at byte level.
- Modeling message interfaces with external modules (many times these
messages are packed at bit level)
Structure
- Different messages supported by an interface are implemented as a class
hierarchy. Each message class overrides the Pack and Unpack methods to
define message specific packing and unpacking.
- Processor class contains a Message Factory class. Message Factory creates
objects when the message type and version are specified.
- A ProcessorManager class might be defined when multiple processors with
same message interface need to be supported.
Participants
The key actors of this design pattern:
- Processor: Base class that implements all
the common functionality for all processors. Typically, classes representing
processor interfaces would inherit from this class.
- MessageFactory: Message Factory creates a
message object when the message type is supplied as a parameter.
- Message: Base class interfaces that are
common to all messages.
Collaboration
The following diagram shows the relationship and collaboration between
various classes involved in the Message Factory and Message Interface Pattern.
Consequences
Use of this pattern has the following benefits:
- Message interface handling and rest of the logic are completely decoupled
- Introducing interface changes is easy as modules with new interfaces can
also work with modules with older interface.
- Byte alignment and ordering considerations do not limit message interface
design. There is less chance of bugs in this area.
Implementation
The following scenarios are supported:
Setting Up Message Factory
- All message classes define a static method that creates an instance of the
message by calling new.
- A function pointer to the above function, message type and version are
passed to the Message Factory.
- Message Factory stores the mapping between message type, version and
static message creation method.
Receiving a Message
- Application asks a Processor class to receive all messages from the OS
queue.
- Processor obtains the message type and version from the message
- Message Factory is invoked with the message type and version
- Message Factory invokes the previously registered static method to create
an object of the required type.
- The pointer to the message object is returned to the caller.
- Processor class now invokes Unpack for the message object and specifies
the received message's pointer.
- Unpack method first unpacks the header of the message
- Once the header is unpacked, message specific field unpacking is done.
- After Unpacking is completed, the Processor specific message handler is
invoked.
Sending a Message
- Application asks the Processor class to send a message object.
- The Processor class then invokes the Pack functionality to obtain a packed
message.
- Packed message is passed to the operating system.
Sample Code and Usage
Message
Interface Design Pattern (Framework) |
class Message
{
public:
// Message specific pack method, to be defined according to the message's
// fields. The base class functions pack and unpack the common header
virtual int Pack(RawBuffer *pBuf) const
{
m_bitIndex = 0;
PackBits(pBuf, 10, m_hdr.value);
PackBits(pBuf, 6, m_hdr.version);
PackBits(pBuf, 16, m_hdr.source);
PackBits(pBuf, 16, m_hdr.destination);
return m_bitIndex;
}
// Corresponding unpack method.
virtual int Unpack(const RawBuffer *pBuf) const
{
m_bitIndex = 0;
m_hdr.value = UnpackBits(pBuf, 10);
m_hdr.version = UnpackBits(pBuf, 6 );
m_hdr.source = UnpackBits(pBuf, 16);
m_hdr.destination = UnpackBits(pBuf, 16);
return m_bitIndex;
}
struct Header
{
int type;
int version;
int source;
int destination;
};
protected:
Header m_hdr;
// Bit index is used to keep track of the current packing/unpacking byte index.
int m_bitIndex;
// PackBits packs a specified number of bits at the next bit index. The method increments
// the bit index
void PackBits(RawBuffer *pBuf, int numBits, int value) const;
/ UnpackBits packs a specified number of bits at the next bit index. The method increments
// the bit index
int UnpackBits(const RawBuffer *pBuf, int numBits) const;
};
// Type for message creation function pointers
typedef Message *(*CreateMessageFunction)();
class MessageFactory
{
// Mapping of message type, version and function pointers. Note that different versions
// of the same message can be supported by this arragement. A different class is defined
// for each version
CreateMessageFunction m_messageCreateFunctions[MAX_MESSAGE_TYPES][MAX_MESSAGE_VERSIONS];
public:
// Register Message saves the function pointer for a message object creation function
// for that class
void RegisterMessage(int messageType, int messageVersion, CreateMessageFunction function)
{
m_messageCreateFunctions[messageType][messageVersion] = function;
}
// Create Message invokes the message creation function specific to the message type and version
Message *CreateMessage(int messageType, int messageVersion)
{
CreateMessageFunction function = m_messageCreateFunctions[messageType][messageVersion];
Message *pMsg = (*function)();
return pMsg;
}
}
class Processor
{
public:
void ReceiveMessages()
{
RawBuffer *pBuf;
Message *pMsg;
while((pBuf = receive()))
{
pMsg = m_messageFactory.CreateMessage(pBuf->hdr.type, pBuf->hdr.version);
pMsg->Unpack(pBuf);
HandleMessage(pMsg);
delete pMsg;
}
}
void SendMessage(const Message *pMsg)
{
RawBuffer *pBuf;
pBuf = pMsg->Pack();
send(pMsg->Destination(), pBuf);
}
protected:
MessageFactory m_messageFactory;
void HandleMessage (const Message *pMsg) = 0;
};
|
Message
Interface Design Pattern (Example) |
class ResourceAllocMsg : public Message
{
private:
enum [MAX_RESOURCES = 15; }
int m_resourceType;
int m_resourceCount;
int m_resourceId[MAX_RESOURCES];
public:
// Message creation function that will be passed to the message factory
static Message *CreateMessage()
{
return (new ResourceAllocMsg);
}
// Message specific pack and unpack methods. The
// methods also return the complete bit level
// length of the messages. These methods can be further
// overriden by message classes that inherit from this class
int Pack(RawBuffer *pBuf) const
{
// Call the base class Pack for header packing
Message::Pack(pBuf);
PackBits(pBuf, 5, m_resourceType);
PackBits(pBuf, 4, m_resourceCount);
for (i=0; i < MAX_RESOURCES; i++)
{
PackBits(pBuf, 4, m_resourceId[i]);
}
return m_bitIndex;
}
virtual int Unpack(const RawBuffer *pBuf)
{
// Call the base class Unpack for header unpacking
hdr = Message::Unpack(pBuf);
m_resourceType = UnpackBits(pBuf, 5);
m_resourceCount = UnpackBits(pBuf, 4);
for (i=0; i < MAX_RESOURCES; i++)
{
m_resourceId[i] = UnpackBits(pBuf, 4);
}
return m_bitIndex;
}
};
class CASProcessor : public Processor
{
// Constructor registers all the messages with the message factory. If multiple versions of a message
// are supported, RegisterMessage method will be invoked multiple times.
CASProcessor() : Processor()
{
m_messageFactory.RegisterMessage(RESOURCE_ALLOC_MSG, 1, ResourceAllocationMessage::CreateMessage);
m_messageFactory.RegisterMessage(RESOURCE_ALLOC_MSG, 2, NewResourceAllocationMessage::CreateMessage);
m_messageFactory.RegisterMessage(RESOURCE_FREE_MSG, 1, ResourceFreeMessage::CreateMessage);
. . .
}
// Processor will invoke HandleMessage when it receives a message, this method will pass the
// message to the appropriate message handler class
void HandleMessage(const Message *pMsg)
{
switch(pMsg->GetType())
{
case RESOURCE_ALLOC_MSG:
resourceHandler.HandleResourceAllocMessage(pMsg);
break;
case RESOURCE_FREE_MSG:
resourceHandler.HandleResourceFreeMessage(pMsg);
break;
default:
}
}
};
|
Known Uses
Any type of message interfaces can be implemented using this pattern. A few
examples are:
- Standard telecom protocols like SS7, V5.2, ISUP, GSM, GPRS (Different
versions of a protocol can be supported seamlessly)
- Byte level packing for proprietary protocols. This gives the protocols
independence from byte alignment and ordering for a particular processor.
Related Patterns
|