tags:

views:

87

answers:

4

Whether as members, whether perhaps static, separate namespaces, via friend-s, via overloads even, or any other C++ language feature...

When facing the problem of supporting multiple/varying formats, maybe protocols or any other kind of targets for your types, what was the most flexible and maintainable approach?

Were there any conventions or clear cut winners?

A brief note why a particular approach helped would be great.

Thanks.

[ ProtoBufs like suggestions should not cut it for an upvote, no matter how flexible that particular impl might be :) ]

+2  A: 

If you need many output formats for many classes, I would try to make it a n + m problem instead of an n * m problem. The first way I come to think of is to have the classes reductible to some kind of dictionary, and then have a method to serlalize those dictionarys to each output formats.

Rasmus Kaj
+1 for scalability viewpoint..
rama-jka toti
+1  A: 

I used OpenH323 (famous enough for VoIP developers) library for long enough term to build number of application related to VoIP starting from low density answering machine and up to 32xE1 border controller. Of course it had major rework so I knew almost anything about this library that days.

Inside this library was tool (ASNparser) which converted ASN.1 definitions into container classes. Also there was framework which allowed serialization / de-serialization of these containers using higher layer abstractions. Note they are auto-generated. They supported several encoding protocols (BER,PER,XER) for ASN.1 with very complex ASN sntax and good-enough performance.

What was nice?

  • Auto-generated container classes which were suitable enough for clear logic implementation.
  • I managed to rework whole container layer under ASN objects hierarchy without almost any modification for upper layers.
  • It was relatively easy to do refactoring (performance) for debug features of that ASN classes (I understand, authors didn't intended to expect 20xE1 calls signalling to be logged online).

What was not suitable?

  • Non-STL library with lazy copy under this. Refactored by speed but I'd like to have STL compatibility there (at least that time).

You can find Wiki page of all the project here. You should focus only on PTlib component, ASN parser sources, ASN classes hierarchy / encoding / decoding policies hierarchy.

By the way,look around "Bridge" design pattern, it might be useful.

Feel free to comment questions if something seen to be strange / not enough / not that you requested actuall.

Roman Nikitchenko
thanks.. and all appreciated, +1. I always do an upvote phase first for the perspective (and there are many) or effort, and more questions perhaps later..
rama-jka toti
+1  A: 

Assuming you have full access to the classes that must be serialized. You need to add some form of reflection to the classes (probably including an abstract factory). There are two ways to do this: 1) a common base class or 2) a "traits" struct. Then you can write your encoders/decoders in relation to the base class/traits struct.

Alternatively, you could require that the class provide a function to export itself to a container of boost::any and provide a constructor that takes a boost::any container as its only parameter. It should be simple to write a serialization function to many different formats if your source data is stored in a map of boost::any objects.

That's two ways I might approach this. It would depend highly on the similarity of the classes to be serialized and on the diversity of target formats which of the above methods I would choose.

jmucchiello
+1 for boost as i almost consider it part of language.. and of course for similarity/diversity angle.
rama-jka toti
+1  A: 

Reading through the already posted responses, I can only agree with a middle-tier approach.

Basically, in your original problem you have 2 distinct hierarchies:

  • n classes
  • m protocols

The naive use of a Visitor pattern (as much as I like it) will only lead to n*m methods... which is really gross and a gateway towards maintenance nightmare. I suppose you already noted it otherwise you would not ask!

The "obvious" target approach is to go for a n+m solution, where the 2 hierarchies are clearly separated. This of course introduces a middle-tier.

The idea is thus ObjectA -> MiddleTier -> Protocol1.

Basically, that's what Protocol Buffers does, though their problematic is different (from one language to another via a protocol).

It may be quite difficult to work out the middle-tier:

  • Performance issues: a "translation" phase add some overhead, and here you go from 1 to 2, this can be mitigated though, but you will have to work on it.
  • Compatibility issues: some protocols do not support recursion for example (xml or json do, edifact does not), so you may have to settle for a least-common approach or to work out ways of emulating such behaviors.

Personally, I would go for "reimplementing" the JSON language (which is extremely simple) into a C++ hierarchy:

  • int
  • strings
  • lists
  • dictionaries

Applying the Composite pattern to combine them.


Of course, that is the first step only. Now you have a framework, but you don't have your messages.

You should be able to specify a message in term of primitives (and really think about versionning right now, it's too late once you need another version). Note that the two approaches are valid:

  • In-code specification: your message is composed of primitives / other messages
  • Using a code generation script: this seems overkill there, but... for the sake of completion I thought I would mention it as I don't know how many messages you really need :)


On to the implementation:

Herb Sutter and Andrei Alexandrescu said in their C++ Coding Standards

Prefer non-member non-friend methods

This applies really well to the MiddleTier -> Protocol step > creates a Protocol1 class and then you can have:

Protocol1 myProtocol;
myProtocol << myMiddleTierMessage;

The use of operator<< for this kind of operation is well-known and very common. Furthermore, it gives you a very flexible approach: not all messages are required to implement all protocols.

The drawback is that it won't work for a dynamic choice of the output protocol. In this case, you might want to use a more flexible approach. After having tried various solutions, I settled for using a Strategy pattern with compile-time registration.

The idea is that I use a Singleton which holds a number of Functor objects. Each object is registered (in this case) for a particular Message - Protocol combination. This works pretty well in this situation.


Finally, for the BOM -> MiddleTier step, I would say that a particular instance of a Message should know how to build itself and should require the necessary objects as part of its constructor.

That of course only works if your messages are quite simple and may only be built from few combination of objects. If not, you might want a relatively empty constructor and various setters, but the first approach is usually sufficient.


Putting it all together.

// 1 - Your BOM
class Foo {};
class Bar {};

// 2 - Message class: GetUp
class GetUp
{
  typedef enum {} State;
  State m_state;
};

// 3 - Protocl class: SuperProt
class SuperProt: public Protocol
{
};

// 4 - GetUp to SuperProt serializer
class GetUp2SuperProt: public Serializer
{
};

// 5 - Let's use it
Foo foo;
Bar bar;

SuperProt sp;
GetUp getUp = GetUp(foo,bar);

MyMessage2ProtBase.serialize(sp, getUp); // use GetUp2SuperProt inside
Matthieu M.
I'll accept this for completeness and effort covering plenty of angles.. cheers. however, I cannot see the coding standards preference not introduce friend, and while I can't nail it yet, I'm somewhat reluctant to go for it.. could be wrong though but have to see first.
rama-jka toti
http://www.gotw.ca/publications/c++cs.htm > Item 44. The idea is to promote a better encapsulation since member and friend functions are susceptible to changes of the inner workings of a class while nonmember nonfriend functions are not (they only depend on the public methods).
Matthieu M.