Twenty years ago, Stefano Z worked with some Very Smart Very Senior Engineers. These Very Smart Senior Engineers liked to find all sorts of "interesting" solutions to common problems.
For example, they had a module in C++ that needed to send data to other systems. To formalize this messaging, they needed a set of Data Transfer Objects (DTOs). Now, there are many libraries that might make generating these kinds of objects easier, but the Very Smart Very Senior Engineers didn't want to use a library. So they started out with just large piles of hand coded:
class Book : public DTOCommonBaseClass
{
private:
string _title;
string _author;
//many more fields
public:
//Constructors, and getters and setters for all fields
//and one big serialization method
};
Then, to use them, you'd have to write the tedious:
Book myBook;
myBook.SetTitle(title);
myBook.SetAuthor(author);
//more properties
No, none of them had a constructor that actually initialized the object, you always had to go property by property.
No one liked this. So the senior dev behind their serialization library decided to take a crack at writing the "Universal Stream" class, a super object that could be all of your DTOs.
Instead of that cumbersome series of sets you had above, you could now do this:
static const string KEY_TITLE = "KEY_TITLE";
static const string KEY_AUTHOR = "KEY_AUTHOR";
static const string KEY_SUBJECT = "KEY_SUBJECT";
static const string KEY_YEAR = "KEY_YEAR";
static const string KEY_YEARFIRST = "KEY_YEARFIRST";
static const string KEY_PAGES = "KEY_PAGES";
UniversalStream stream;
stream.AddItem(KEY_TITLE, blahTitle);
stream.AddItem(KEY_AUTHOR, blahAuthor);
stream.AddItem(KEY_SUBJECT, blahSubject);
stream.AddItem(KEY_YEAR, blahYear);
stream.AddItem(KEY_YEARFIRST, blahYearFirst);
stream.AddItem(KEY_PAGES, blahPages);
So much easier, right?
And then, instead of doing an ugly title = myBook.GetTitle()
, you could write this simple code to get the values back out instead;
StringUniversalItem* pItem = dynamic_cast<StringUniversalItem*>(stream.GetUniversalItem(KEY_TITLE));
if (pItem)
{
string title = pImet.GetAt(0);
}
So much less boilerplate, right? So much simpler.
At this point, you might be thinking to yourself, "did he just reinvent the map?" Well, sort of. This is much like a map, except the values are only allowed to be either string
or int
. But it also allowed you to add multiple values at the same time, like this:
UniversalItem strings;
strings.Add(blahTitle);
strings.Add(blahAuthor);
strings.Add(blahSubject);
static const string KEY_STRINGS = "KEY_STRINGS";
UniversalStream stream;
stream.AddItem(KEY_STRINGS, strings);
// The code to extract data is left as an exercise for the reader,
// but let me give you a little hint:
const size_t TITLE = 0;
const size_t AUTHOR = 1;
const size_t SUBJECT = 2;
So, it allows you to have arrays of string
s and int
s too.
The entire point of a DTO is that you have a simple, clear, and easy to understand arrangement of the fields in the object, so that you can pass them over to other applications without having to share much beyond a simple schema. This offered none of those things, and made nothing easier, and cause many many bugs and much confusion through the entire team.
So of course, it was the standard that everyone used.
Here's one last usage example from Stefano:
string someOptionA, someOptionB, someOptionC; // etc
// Omitted: everything else that had to be processed
// Omitted: get values from somewhere, e.g. a GUI, a public C API, etc.
static string OPTION_A_MARKER = "OPTION_A_MARKER";
static string OPTION_B_MARKER = "OPTION_B_MARKER";
static string OPTION_C_MARKER = "OPTION_C_MARKER";
UniversalItem processingOptionsNames;
UniversalItem processingOptions;
processingOptionsNames.Add(OPTION_A_MARKER);
processingOptions.Add(someOptionA);
processingOptionsNames.Add(OPTION_B_MARKER);
processingOptions.Add(someOptionB);
processingOptionsNames.Add(OPTION_C_MARKER);
processingOptions.Add(someOptionC);
static const string PROCESSING_OPTIONS_NAMES_MARKER = "PROCESSING_OPTIONS_NAMES_MARKER";
static const string PROCESSING_OPTIONS_MARKER = "PROCESSING_OPTIONS_MARKER";
// Omitted: other markers, for everything else that had to be processed
UniversalStream stream;
stream.AddItem(PROCESSING_OPTIONS_NAMES_MARKER, processingOptionsNames);
stream.AddItem(PROCESSING_OPTIONS_MARKER, processingOptions);
// Omitted: add everything else that had to be processed
// Later, how to extract the options:
UniversalItem* pNames = dynamic_cast<UniversalItem*>(stream.GetUniversalItem(PROCESSING_OPTIONS_NAMES_MARKER));
UniversalItem* pValues = dynamic_cast<UniversalItem*>(stream.GetUniversalItem(PROCESSING_OPTIONS_MARKER));
for (size_t i = 0; i < pNames->GetSize(); ++i)
{
string name = (*pNames)[i];
string value = (*pValues)[i];
if (name == OPTION_A_MARKER)
{
// Omitted: process "value" as the Option A
}
else if (name == OPTION_B_MARKER)
{
// Omitted: process "value" as the Option B
}
else if (name == OPTION_C_MARKER)
{
// Omitted: process "value" as the Option C
}
// Omitted: other options
// Omitted: process everything else
}