views:

247

answers:

6

This is a generic C++ design question.

I'm writing an application that uses a client/server model. Right now I'm writing the server-side. Many clients already exist (some written by myself, others by third parties). The problem is that these existing clients all use different protocol versions (there have been 2-3 protocol changes over the years).

Since I'm re-writing the server, I thought it would be a great time to design my code such that I can handle many different protocol versions transparently. In all protocol versions, the very first communication from the client contains the protocol version, so for every client connection, the server knows exactly which protocol it needs to talk.

The naive method to do this is to litter the code with statements like this:

if (clientProtocolVersion == 1)
    // do something here
else if (clientProtocolVersion == 2)
    // do something else here
else if (clientProtocolVersion == 3)
    // do a third thing here...

This solution is pretty poor, for the following reasons:

  1. When I add a new protocol version, I have to find everywhere in the source tree that these if statements are used, and modify them to add the new functionality.
  2. If a new protocol version comes along, and some parts of the protocol version are the same as another version, I need to modify the if statements so they read if (clientProtoVersion == 5 || clientProtoVersion == 6).
  3. I'm sure there are more reasons why it's bad design, but I can't think of them right now.

What I'm looking for is a way to handle different protocols intelligently, using the features of the C++ langauge. I've thought about using template classes, possibly with the template parameter specifying the protocol version, or maybe a class heirarchy, one class for each different protocol version...

I'm sure this is a very common design pattern, so many many people must have had this problem before.

Edit:

Many of you have suggested an inheritance heirarchy, with the oldest protocol version at the top, like this (please excuse my ASCII art):

IProtocol
    ^
    |
CProtoVersion1
    ^
    |
CProtoVersion2
    ^
    |
CProtoVersion3

... This seems like a sensible thing to do, in terms of resuse. However, what happens when you need to extend the protocol and add fundamentally new message types? If I add virtual methods in IProtocol, and implement these new methods in CProtocolVersion4, how are these new methods treated in earlier protocol versions? I guess my options are:

  • Make the default implementation a NO_OP (or possibly log a message somewhere).
  • Throw an exception, although this seems like a bad idea, even as I'm typing it.
  • ... do something else?

Edit2:

Further to the above issues, what happens when a newer protocol message requires more input than an older version? For example:

in protocl version 1, I might have:

ByteArray getFooMessage(string param1, int param2)

And in protocol version 2 I might want:

ByteArray getFooMessage(string param1, int param2, float param3)

The two different protocol versions now have different method signatures, which is fine, except that it forces me to go through all calling code and change all calls with 2 params to 3 params, depending on the protocol version being used, which is what I'm trying to avoid in the first place!

What is the best way of separating protocol version information from the rest of your code, such that the specifics of the current protocol are hidden from you?

A: 

I'd tend towards to using different classes to implement adapters for the different protocols to the same interface.

Depending on the protocol and the differences, you might get some gain using TMP for state machines or protocol details, but generating six sets of whatever code uses the six protocol versions is probably not worth it; runtime polymorphism is sufficient, and for most cases TCP IO is probably slow enough not to want to hard code everything.

Pete Kirkham
+8  A: 

Since you need to dynamically choose which protocol to use, using different classes (rather than a template parameter) for selecting the protocol version seems like the right way to go. Essentially this is Strategy Pattern, though Visitor would also be a possibility if you wanted to get really elaborate.

Since these are all different versions of the same protocol, you could probably have common stuff in the base class, and then the differences in the sub classes. Another approach might be to have the base class be for the oldest version of the protocol and then have each subsequent version have a class that inherits from the previous version. This is a somewhat unusual inheritance tree, but the nice thing about it is that it guarantees that changes made for later versions don't affect older versions. (I'm assuming the classes for older versions of the protocol will stabilize pretty quickly and then rarely ever change.

However you decide to organize the hierarchy, you'd then want to chose the protocol version object as soon as you know the protocol version, and then pass that around to your various things that need to "talk" the protocol.

Laurence Gonsalves
Indeed the Strategy pattern is the most likely approach, you would probably like to combine with Factory which would be responsible to deliver you the right 'Strategy' for the version you are using.
Matthieu M.
A: 

maybe oversimplified, but this sounds like a job for inheritance? A base class IProtocol which defines what a protocol does (and possibly some common methods), and then one implementation for IProtocol for each protocol you have?

stijn
A: 

You need a protocol factory that returns a protocol handler for the appropraite version:

ProtocolHandler&  handler = ProtocolFactory::getProtocolHandler(clientProtocolVersion);

You can then use inheritance to only update the parts of the protocol that have been changed between versions.

Notice that the ProtocolHandler (base class) is basically a stratergy pattern. As such it should not maitain its own state (if required that is passed in via the methods and the stratergy will update the property of the state object).

Becuase the stratergy does not maintain state we can share the ProtcolHandler between any number of threads and as such the ownership does not need to leave the factory object. Thus the factory just need to create one handler object for each protocol version it understands (this can even be done lazily). Becuase the factory object retains ownership you can return a reference of the Protocol Handler.

Martin York
+4  A: 

I have used (and heard of others using) templates to solve this problem too. The idea is that you break your different protocols up into basic atomic operations and then use something like boost::fusion::vector to build protocols out of the individual blocks.

The following is an extremely rough (lots of pieces missing) example:

// These are the kind of atomic operations that we can do:
struct read_string { /* ... */ };
struct write_string { /* ... */ };
struct read_int { /* ... */ };
struct write_int { /* ... */ };

// These are the different protocol versions
typedef vector<read_string, write_int> Protocol1;
typedef vector<read_int, read_string, write_int> Protocol2;
typedef vector<read_int, write_int, read_string, write_int> Protocol3;

// This type *does* the work for a given atomic operation
struct DoAtomicOp {
  void operator ()(read_string & rs) const { ... }
  void operator ()(write_string & ws) const { ... }
  void operator ()(read_int & ri) const { ... }
  void operator ()(write_int & wi) const { ... }
};

template <typename Protocol> void doProtWork ( ... ) {
  Protocl prot;
  for_each (prot, DoAtomicOp (...));
}

Because the protocl version is dynamic you'll need a single top level switch statement to determine which protocl to use.

void doWork (int protocol ...) {
  switch (protocol) {
  case PROTOCOL_1:
    doProtWork<Protocol1> (...);
    break;
  case PROTOCOL_2:
    doProtWork<Protocol2> (...);
    break;
  case PROTOCOL_3:
    doProtWork<Protocol3> (...);
    break;
  };
}

To add a new protocol (that uses existing types) you only need to do define the protocl sequence:

typedef vector<read_int, write_int, write_int, write_int> Protocol4;

And then add a new entry to the switch statement.

Richard Corden
This is a very interesting approach, thank you!
Thomi
A: 

I'm gonna agree with Pete Kirkham, I think it would be pretty unpleasant to maintain potentially 6 different versions of the classes to support the different protocol versions. If you can do it, it seems like it would be better to have the older versions just implement adapters to translate to the latest protocol so you only have one working protocol to maintain. You could still use an inheritance hierarchy like shown above, but the older protocol implementations just do an adaption then call the newest protocol.

Corwin Joy