Software Implementation to Reduce the Impact of Interface Revision

Gerry Tyra

Embedded Software Engineer, Senior Staff

Lockheed Martin Aeronautics ADP

Copyright IEEE 2014

Abstract

Complex software systems are subject to revision. Unfortunately, even simple changes to the interface tend to cause ripples of change through the entire system. These changes increase costs for engineering and test, and have the potential to adversely impact the delivery of the updated system.

This paper presents a C++ software methodology that allows multiple revisions of an interface to co-exist within a project. By this means, only those components directly affected by the revision need to be updated, while the remainder of the project can be left unchanged. Since only the newest form of the interface is actually active, message structures do not need to carry redundant data. Updates are carried out by replacing the dynamic library containing the interface. As a result, neither recompiling nor relinking is required.

Introduction

Any software project of significant size and complexity will evolve over time. Requirements change, latent errors are identified, and improvements in efficiency and maintainability are sought. With these changes, the data structures that are used to transfer data from one component of the system to another must also change. Given conventional software development practices, changing interfaces creates a major problem for program managers.

Even seemingly simple changes have a tendency to cause other changes that ripple through a project. Before work on a new release can be started, management must estimate the extent of the change and allocate resources to accomplish the task. Those resources may not only be internal to the organization, some of the work may be proprietary to vendors.

Any error in estimating the scope of the change can easily result in budget overruns and missed delivery dates.

The approach described in this paper allows the new release to be produced with changes only to those components requiring the new interface. All other components continue to use the previous interface definition. Furthermore, the unmodified components do not have to be recompiled; the existing binaries in the field can continue to be used. One possible exception to this is if the compiler used for the program is updated. But such an update has program wide implications.

As will be touched on later, the inclusion of metadata in the templates can be used to facilitate the updating of older code versions and the maintenance of the interface itself.

Problem Space

While any dynamic or long lived complex system is subject to the problems addressed here, the author's experience with avionic systems, airborne and ground stations, was the seed for this line of inquiry. Therefore, avionics systems will be used as the target environment for the remainder of this paper.

The simplest examples are a group of software components residing in a single memory space computer system. The navigation platform provides position, attitude, velocity and acceleration data to the other sub-systems. If the navigation platform is replaced, updated or upgraded, assume that the interface will change (see fig. 1). The changes may be subtle, but any change will force the other system components to be recompiled and link to the new interface.

Figure 1 – Basic Avionics Interface, Navigation data goes everywhere

However, given the methodology presented in this paper, the system revision is shown in fig. 2 below. In this view, the navigation platform is updated, and it is assumed that sub-system #1 requires data contained in the revised interface. As such, it must be updated. However, sub-systems #2 and #3 require no modification as they do not require the modified data, so they continue to use the older interface.

Figure 2 – Revision Tolerant Interface

The next example is a track file. This would normally consist of an array of record structures, with each record used to describe one object of interest, either in the air or on the ground. As shown in fig. 3, the record serves as a central repository of all data currently available on the object of interest. Data is input by on-board sensors and shared with off-board assets. The data is reviewed and evaluated by a sensor fusion function. The data is also provided to the pilot and any on-board mission planning capability. The exact structure of the record is subject to revision for a number of reasons, such as changes in the sensor suite, new off-board data becoming available, or new algorithms for the sensor fusion system.

Figure 3 – Track Record Data and Accessing Functions

The third example is a communications system. One of the classic issues of any digital communications system is the conflict of requirements: Timely change to allow for evolving operational requirements vs. stability to assure interoperability. Ease of encoding and decoding the data stream vs. the use of high value, low bandwidth communications links. In the end, it all comes down to ones and zeros. It should be remembered that this problem exists over the full range of system interfaces, from within a single platform to an entire theater of operations. Figure 4 presents the actors for transporting a single message type.

Figure 4 makes several points. More than one system may be contributing to and/or consuming data from a given message (e.g. the J2.2 position message transmitted by every aircraft on a given Link 16 net). The preferred native format of a given system may not match the defined structure of the message, in which case a translation function must be provided between the system and the message's data storage.

Using the common data structure also reduces the number of translators and serializers required. If every system had to directly pass its data directly to every other system, the number of translators and serializers would be of Order N2 (ON2). However, as drawn, the number of translators is ON and there is only one serializer per message (O0).

The serializer consists of a combination of planned word/bit packing, compression and mapping .

Figure 4 – Communications System Elements

Applying revision controls to this type of implementation is consistent with any other pub/sub communications system, but with the addition of a revision number. Once a data link is established, all messages would be passed at the highest common revision level see Figure 5.

Figure 5 – Pub/Sub Interface Negotiated to the Highest Common Revision

Methodology

For the following discussion, a rudimentary understanding of the C++ programming language is useful, though not essential. All of the test code is provided in a zip file of the Visual Studio 2010 project is available from the author.

Objective

The revision tree that was used as a target example is shown in figure 5. While such a revision tree should not be allowed for its sheer complexity, reality can push us to create implementations that are less elegant than we would like. Therefore, this tree was selected to validate that the solution would be able to handle the problem space.

In the following discussion, we will start with the simplest cases (Msg15_v2 to Msg15_v3) and work up to the most complex case, the splitting of Msg17_v2 into Msg19_v1 and Msg20_v2.

Revision Management

Most of this discussion is directed towards the issues of handling increasing complexity as new revisions are added. However, under normal circumstances, the older revisions will be deprecated, then declared obsolete and removed.

As a quick example, removing Msg15_v2 is accomplished by actually removing the software associated with that class from the .h and the .cpp files, and removing the call to the Msg15_v2 constructor in Msg15_v3.

In the event that a legacy system cannot be updated to a newer revision, the unused layers of revision can still be deleted. Again looking at Msg15_v2, let's assume that the rest of messages 15, 16, 17 and 18 are no longer called and the messages are considered obsolete.

Figure 6 – Revision Tree for Test case

In such a case, see Figure 7, Msg15_v2 can be linked directly to Msg20_v1, and Msg20_v1 will call the constructor for Msg15_v2. The entire revision tree can thus be collapsed with minimum effort.

Figure 7 – Collapsed Revision Tree

Basics

One of the themes of C++ is to build objects that hide their inner workings. As a simple example:

class Position

{

private:

float lat;

float lng;

float alt;

public:

float getLat();

void setLat( float value );

float getLng();

void setLng( float value );

float getAlt();

void setAlt( float value );

} pos;

In this example latitude, longitude and altitude are stored as single precision floating point numbers. The actual storage locations are not accessible to any functions outside the Position class. Rather, the values are manipulated through the publicly available set and get functions. To get the altitude, the expression would be:

float myAlt = pos.getAlt();

C++ also allows the use of templates, which provide a type independent mechanism to define objects. So, let us define a simple template:

template< class T, class P > class ElementCur : public ElBase<T,P>

{

private:

T data;

public:

T& get() {return data;};

void set(T& t) {data = t;};

void setValue(T t) {data = t;};

};

class T can be any previously defined type, simple or complex. This template provides the protection of the actual data and the get and set functions. Redefining the original class using this template becomes:

class Position

{

public:

ElementCur<float> lat;

ElementCur<float> lng;

ElementCur<float> alt;

} pos;

It should be noted that this approach creates very strong typing. Accessing the altitude is now:

float myAlt = pos.alt.get();

Additional metadata can be added to the template, which will be briefly touched on during the discussion of the implementation. This allows information, such as data type (lat vs. lng), units (degrees vs. PiRads), error margins, etc., to be associated with the data. This can facilitate error checking and older revision software can automatically take advantage of increased resolution provided by a newer interface.

Implementation

The initial test case is fairly simple, take two classes with nearly the same structure, and map one into the other. This provides a detailed look at the level of maintenance work required to change from one version to a newer one.

In the beginning, is Message 15, version 2. It contains a simple set of position data, latitude and longitude in floating point degrees and altitude in integer feet:

class DllExport Msg15_v2 : public MsgBase

{

public:

template< class T > class ElType

{

public:

typedef ElementCur< T, Msg15_v2 > type;

};

Msg15_v2( MBmap& );

ElType< long >::type Alt;

ElType< float >::type Lat;

ElType< float >::type Lng;

};

Msg15_v2 is derived from a base class MsgBase. MsgBase serves as the base for all message types. It provides the functionality, among other things, to create new message instances of the correct type. The macro DllExport is used to declare that Msg15_v2 is accessible from outside the library where this will reside.

The constructor, Msg15_v2(Mbmap&), is passed a reference to a map of pointers. The pointers are to any newer revisions. This is needed later as the structure becomes more complex.

The embedded template ElType is used to mask a level of complexity. The use of the typedef statement is needed for older C++ compilers (through Visual Studio 2012). With C++ revision 2011 (aka C++11), a template typedef with a “using” statement would be the preferred approach.

The various elements are defined using the ElType statements.

Metadata

A template declaration can take any type as an argument, as well as any integer values which are known at compile time. This provides a means to include predefined metadata. Any values that are not known until run time, or are not integer types, must be provided through the constructor or an initialization function. As a quick example, assume that the enumerated types eLatitude, eDegrees, and eFloat exist. In this case, the declaration could be:

ElType< float, eLatitude, eDegrees, eFloat > Lat;

And to provide run time by an additional member function of the template, an identifying string in this case:

Lat.initMetaData( “ownship” );

This additional data would be stored in a properties structure within the template, which could be accessed by an external application capable of dynamically formatting data, such as an XML parser. The parser would be able to verify that it was accessing a value of latitude, that the units were in degrees, the type was a float and the vehicle that it referred to. The operational value of this capability is a property of the overall system design. However, as it does not affect the subject at hand, it will not be addressed any further.

Operationally, the metadata can be used to inform an application that the resolution implied by the current implementation was not met by the application providing the data. By way of an example, if the source application passed the data in through a revision that has only one byte of resolution; the fact that the data is stored in a double precision floating point value does not impart any more fidelity to the data.

During development, metadata can also facilitate debugging by providing better context data as the programmer steps deeper into the template calling stack.

Adding a Revision

Now that the basic construct has been shown, a new revision is added, Msg15_v3. While versions 2 and 3 have identical data content, the order of the data elements has changed and would require a recompile. This provides a simple first step in understanding the more complex implementations. First, design the new revision:

class DllExport Msg15_v3 : public MsgBase

{

public:

template< class T > class ElType

{

public:

typedef ElementCur< T, Msg15_v3 > type;

};

Msg15_v3( MBmap& );

ElType< float >::type Lat;

ElType< float >::type Lng;

ElType< long >::type Alt;

private:

void init();

};

Aside from changing Msg15_v2 to Msg15_v3, and moving Alt, everything in the declaration is the same. The first significant change is in the constructor:

Msg15_v3::Msg15_v3( MBmap& ptr ) : MsgBase( 15, 3, ptr )

{

peer[15] = this;

older[15] = new Msg15_v2(peer);

init();

};

For bookkeeping purposes, the constructor takes a map object as an argument, and passes it to the MsgBase class, along with it message type (15) and revision (3) number. The constructor then records that it is its first and only peer (a message type 15) at this level in the hierarchy for this message type. Finally, it creates a new instance of the previous version of the message. In so doing, it informs the older version of all the peers above it (this will become significant later).

peer and older are map objects that represent the relationship between other message type instances and this specific instance. In this case, there is only one peer instance, the one being created (represented by the pointer this) and the message type is “15”. There is only one older revision, which was created by the new operator. This new instance was also of type “15”.

The call to the init function provides the mapping from the older message version to the current message version:

void Msg15_v3::init()

{

Msg15_v2* ptr2 = static_cast<Msg15_v2*>(older[15]);

ptr2->Lat.init( this, &Msg15_v3::Lat );

ptr2->Lng.init( this, &Msg15_v3::Lng );

ptr2->Alt.init( this, &Msg15_v3::Alt );

};

In this case, ptr2 is the instance of the older version of the message. Each of the elements has its own init function, which are called in turn. The arguments, the this pointer and the member pointer, provide the needed address data for the older message version to access the correct element in the newer version.

The revised Msg15_v2 is now

class DllExport Msg15_v2 : public MsgBase

{

public:

template< class T > class ElType

{

public:

typedef ElementPrev< T, Msg15_v3 > type;

};

Msg15_v2( MBmap& );

ElType< long >::type Alt;

ElType< float >::type Lat;

ElType< float >::type Lng;

};

The changes are replacing ElementCur with ElementPrev and changing the mapping from Msg15_v2 to Msg15_v3. The ElementPrev template contains the logic required to link this version of the message to the newer version.

It should be noted that as a matter of style, the init() function could logically be a member of the older message class, rather than being added to the newer class definition. The implementation as presented has the relative advantage of keeping the init function, and any translation functions, private within the message class.

Figure 8 presents the calling sequence graphically. The newest revision contains the data. Any older revision can only ask the next newer revision to provide the data. As the revision stack grows, without older revisions being removed as obsolete, the request to get or set data has to traverse all of the active layers. The number of calls is mitigated by the “thinness” of each layer and the potential to collapse the stack in the future.

Figure 8 – Calling Sequence from an Application to Access Data

Template Details

Now that we have looked at the higher level abstraction of linking the message versions, let's look at the ElementPrev template that drives the functionality:

template< class T, class P > class ElementPrev : public ElBase<T,P>

{

private:

typedef typename P::ElType<T>::type P::* mPtr;

typedef T& (P::*getFunc)();

typedef void (P::*setFunc)( T t );

T data;

P* parent;

mPtr member_ptr;

getFunc member_getter;

setFunc member_setter;

public:

void init( P* pIn, mPtr ptrIn, getFunc gfIn = NULL, setFunc sfIn = NULL )

{

parent = pIn;

member_ptr = ptrIn;

member_getter = gfIn;

member_setter = sfIn;

};

T& get()

{

if (member_getter)

return (parent->*member_getter)();

else

return (parent->*member_ptr).get();

};

void set(T& t)

{

if (member_setter)

(parent->*member_setter)( t );

else

(parent->*member_ptr).set(t);

};

void setValue(T t)

{

if (member_setter)

(parent->*member_setter)( t);

else

(parent->*member_ptr).setValue(t);

};

};

The main differences between the ElementCur and the ElementPrev templates are in how the get and set functions work and the addition of the init function. The init function accepts a pointer to the newer/parent version, as well as a member pointer to the element corresponding to the element that this template instance represents. Two additional function pointers, both defaulting to NULL, are provided to handle more complex cases, as will be shown below.

The get and set functions of this template test if there were specialized get/set functions provided. If so, the function is used to access the data from the newer version. If not, the newer version's normal set and get functions are used to access the data.

Message Base Class

The binding mechanism that allows for controlled access to a specific message version is contained in the MsgBase class:

class MsgBase;

typedef std::map<long, MsgBase*> MBmap;

typedef MBmap::iterator MBitr;

class DllExport MsgBase

{

protected:

long msgNum; // message number/ID for the derived class

long ver; // the version of the associated derived class

MBmap older; // pointer to the next earlier version, if any

MBmap newer; // pointer to the next newer version, if any

MBmap peer; // pointer to peer messages created from one or more previous messages, if any

MBmap roots; // pointer to the highest revision of any message with a different ID that preceded this message, if any

MsgBase( long msgNumIn, long rev, MBmap& nmap );

void setRoot( MsgBase* msg, long msgNum );

void setOlder( MBmap& ptr ) {older = ptr;};

void setPeer( MBmap& ptr ) {peer = ptr;};

public:

static MsgBase* getInstance( int msgNum );

MsgBase* getVersion( long msgNum, long rev );

};

To understand the finer points of most of the functions, the reader is directed to examine the source code. But of immediate interest are the functions getInstance and getVersion. In the immediate example, only message 15 is addressed. In the source code, a more complex example is provided.

When a given message is updated, the getInstance function must be updated to access the correct constructor. This example creates a new instance of the message revision stack, but different implementation could get an instance from an available pool. In the more advanced case presented in the source code, when a message of type 15 is requested, the message actually created is type 17 and later type 20.

MsgBase* MsgBase::getInstance(int msgNum)

{

static MBmap tempMap;

static bool firstTime = true;

MsgBase* msg = NULL;

if (firstTime)

{

tempMap.clear();

firstTime = false;

}

switch (msgNum)

{

case 15:

msg = new Msg15_v3( tempMap );

break;

default:

break;

}

return msg;

}

The pointer returned by this function is of type MsgBase*. But it can be cast back to Msg15_v3, in which case it will act normally for the application requesting the message instance. However, this cannot be assumed to be the case forever. When the next revision is made, the returned MsgBase* will not be a polymorphic type Msg15_v3.

In order to get a pointer to an instance of the message of the desired revision, the getVersion function is used (see the source code for the actual implementation). An application using the message type 15, version 3 interface would use the following code:

MsgBase* msgBase = MsgBase::getInstance( 15 );

Msg15_v3* msgV3 = static_cast<Msg15_v3*>(msgBase->getVersion(15, 3));

While an application needing a message type 15, version 2 instance would have:

Msg15_v2* msgV2 = static_cast<Msg15_v2*>(msgBase->getVersion(15, 2));

If run time typing is available, a dynamic cast could be used in place of the static cast. To report an error, getInstance and/or getVersion can return NULL, a null object, or throw an exception.

Changing Types

Now that we have walked slowly through a trivial case, we move quickly on to more complex examples. The first structural change we make is to change Alt from integer feet to floating point meters in Msg15_v4. First, change the type declaration of Alt:

ElType< float >::type Alt;

Connectivity to the earlier version is accomplished by adding two extra functions in Msg15_v4:

long& Msg15_v4::getAlt()

{

float temp = 3.28084f * (this->Msg15_v4::Alt).get();

tempAlt = static_cast<long>(temp);

return tempAlt;

};

void Msg15_v4::setAlt( long val )

{

float temp = static_cast<float>(val) * 0.3048f;

(this->Msg15_v4::Alt).set(temp);

};

And then referencing these functions in the Msg15_v4 init function.

void Msg15_v4::init()

{

Msg15_v3* ptr3 = static_cast<Msg15_v3*>(older[15]);

ptr3->Lat.init( this, &Msg15_v4::Lat );

ptr3->Lng.init( this, &Msg15_v4::Lng );

ptr3->Alt.init( this, NULL, &Msg15_v4::getAlt, &Msg15_v4::setAlt );

};

Therefore, pointers of each of the three versions of message type 15 are able to access the same data, though the data may be represented differently:

long altFeet = msg15_v2Ptr->Alt.get();

long altFeet = msg15_v3Ptr->Alt.get();

float altMeters = msg15_v4Ptr->Alt.get();

Consolidating Elements

The next increase in complexity comes from taking the three data elements and wrapping them in a structure called Position:

struct DllExport Position

{

template< class T > class ElType

{

public:

typedef ElementCur< T, Position > type;

};

ElType< float >::type Lat;

ElType< float >::type Lng;

ElType< float >::type Alt;

};

The Msg15_v5 class has only a single data element, but must provide older versions with a means for getting and setting the three wrapped elements.

class DllExport Msg15_v5 : public MsgBase

{

public:

template< class T > class ElType

{

public:

typedef ElementPrev< T, Msg17_v1 > type;

};

Msg15_v5( MBmap& );

ElType< Position >::type Pos;

private:

// setters and getters from v4 to v5

float& getLat(){return (this->Msg15_v5::Pos).get().Lat.get();};

void setLat( float val ){(this->Msg15_v5::Pos).get().Lat.set( val );};

float& getLng(){return (this->Msg15_v5::Pos).get().Lng.get();};

void setLng( float val ){(this->Msg15_v5::Pos).get().Lng.set( val );};

float& getAlt(){return (this->Msg15_v5::Pos).get().Alt.get();};

void setAlt( float val ){(this->Msg15_v5::Pos).get().Alt.set( val );};

void init();

};

And the Msg15_v5 init function logs these functions for use:

void Msg15_v5::init()

{

Msg15_v4* ptr4 = static_cast<Msg15_v4*>(older[15]);

ptr4->Lat.init( this, NULL, &Msg15_v5::getLat, &Msg15_v5::setLat );

ptr4->Lng.init( this, NULL, &Msg15_v5::getLng, &Msg15_v5::setLng );

ptr4->Alt.init( this, NULL, &Msg15_v5::getAlt, &Msg15_v5::setAlt );

};

Because of the Position wrapper, accessing altitude becomes:

float altMeters = msg15_v5Ptr->Pos.get().Alt.get();

Translating, msg15_v5Ptr->Pos refers to the structure of type Position contained in the message instance pointed to by msg15_v5Ptr. The .get() returns a reference to that structure instance, making the data contained in the structure accessible. .Alt.get() specifies that we are interested in the Alt element of the structure and returns a reference to the data.

Changing Underlying Types

Leaving Message 15 for a moment, the next issue is a major change in data type and structure. To demonstrate this, Msg16_v1 contains a set of Euler angles, which would normally be used to represent the attitude of a vehicle. In version 2, the representation has been changed to quaternions. The set and get functions in Msg16_v2 are:

EulerAngles& Msg16_v2::getAtt()

{

tempAtt.setFromQuaternion((this->Msg16_v2::Attitude).get());

return tempAtt;

};

void Msg16_v2::setAtt( EulerAngles att )

{

Quaternions q;

q.setFromEA( att );

Attitude.set( q );

};

The structures containing the Euler angles and quaternions include the required translator functions. The init function for Msg16_v2 becomes:

void Msg16_v2::init()

{

Msg16_v1* ptr = static_cast<Msg16_v1*>(older[16]);

ptr->Attitude.init( this, NULL, &Msg16_v2::getAtt, &Msg16_v2::setAtt );

};

Combining Messages

At some point, it may become advantageous to combine messages, either for ease of computation or maintenance. For the purposes of the test environment, two different cases were tried. In the first case, messages 15 and 16 were combined to create a 6 degree of freedom (position and attitude) representation of an object. With the combination, a new message number 17 was created, while messages 15 and 16 became deprecated. In the second case, message 18, which represents the velocity of a point, was merged with Msg17_v1 to become Msg17_v2.

With this new possibility of a break in the message numbering sequence, the utility of a map to store and forward the pointers to various revision instances becomes clear. We no longer have a linear string, but have moved on to a one to many mapping. Keeping with the philosophy that a revision should only minimally intrude on the older version, the mechanism used to maintain the relationship among the versions should be as self-contained and simple as possible. Allowing it to either be searched directly, by index, or iterated through.

For each message type in the hierarchy, the roots map has an entry with that type's number as the key. It is paired with a pointer to the instance of the newest version of that type. A request for an instance pointer will always be directed to the newest version instance in the current hierarchy. At this stage, that would be to Msg17_v2. A request for the pointer to the instance of Msg15_v3 must be walked down through the tree until the correct instance is found. The roots map allows Msg17_v2 to send the request immediately to Msg15_v5, without having to search the Msg16 and Msg18 trees.

Similarly, the newer and older maps provide the index and pointer relationships to the various message instances while maneuvering up and down the message tree.

Several mechanisms were considered, but the map template appears to be near optimum. While indexed access is not that common, it does provide access times of OlogN. The more common case of iterating through the list has an access time of ON. In most cases, there is only one entry in the map, so access time is actually O0. The use of a vector template is potentially faster, but only at the cost of significant wasted memory.

The end result of the combinations is:

class DllExport Msg17_v2 : public MsgBase

{

public:

template< class T > class ElType

{

public:

typedef ElementPrev< T, Msg20_v1 > type;

};

Msg17_v2( MBmap& ptr );

ElType< Position >::type Pos;

ElType< Velocity >::type Vel;

ElType< Quaternions >::type Attitude;

private:

void init();

};

The constructor and init for Msg17_v2 is:

Msg17_v2::Msg17_v2( MBmap& ptr ) : MsgBase( 17, 2, ptr )

{

peer[17] = this;

older[17] = new Msg17_v1(peer);

older[18] = new Msg18_v1(peer);

init();

};

void Msg17_v2::init()

{

Msg17_v1* ptr17 = static_cast<Msg17_v1*>(older[17]);

Msg18_v1* ptr18 = static_cast<Msg18_v1*>(older[18]);

ptr17->Pos.init( this, &Msg17_v2::Pos );

ptr18->Vel.init( this, &Msg17_v2::Vel );

ptr17->Attitude.init( this, &Msg17_v2::Attitude );

};

And the constructor and init for Msg17_v1 is:

Msg17_v1::Msg17_v1( MBmap& ptr ) : MsgBase( 17, 1, ptr )

{

peer[17] = this;

older[15] = new Msg15_v5(peer);

older[16] = new Msg16_v2(peer);

init();

};

void Msg17_v1::init()

{

Msg15_v5* ptr15 = static_cast<Msg15_v5*>(older[15]);

Msg16_v2* ptr16 = static_cast<Msg16_v2*>(older[16]);

ptr15->Pos.init( this, &Msg17_v1::Pos );

ptr16->Attitude.init( this, &Msg17_v1::Attitude );

};

From these snippets, it should be obvious that combining messages as part of an update/revision cycle is not significantly more difficult that making a basic revision.

Splitting Messages

Just as there can be reasons to combine messages, the point can be reached where breaking up larger messages into smaller messages makes sense. This also provides a mechanism to segregate and deprecate data elements that no longer have a useful purpose.

For this test case, message Msg17_v2 is broken into Msg19_v1, angular measure, and Msg20_v1, linear measure. The caveat for splitting messages is that until all of the older messages are obsolete and deleted, or the hierarchy has been pruned to a simple linear chain, the split messages must still be created as a set. Any application with access to Msg17_v2 must be able to access linear position and velocity, which are located in Msg20_v1, as well as angular position, which is located in Msg19_v1.

As a result of this linkage, the constructor of Msg19_v1 must also create an instance of Msg20_v1, and the other way around. So, a mechanism must be provided to avoid recursion. To accomplish this, the constructors are:

Msg19_v1::Msg19_v1( MBmap& ptr, bool recursive ) : MsgBase( 19, 1, ptr )

{

peer[19] = this;

if (recursive)

{

// instantiate any peers

peer[20] = new Msg20_v1( ptr, false );

// share the list of peers

peer[20]->setPeer( peer );

// create any older message(s)

older[17] = new Msg17_v2(peer);

// tell the peers about the older messages

peer[20]->setOlder( older );

}

};

Msg20_v1::Msg20_v1( MBmap& ptr, bool recursive ) : MsgBase( 20, 1, ptr )

{

peer[20] = this;

if (recursive)

{

// instantiate any peers

peer[19] = new Msg19_v1( ptr, false );

// share the list of peers

peer[19]->setPeer( peer );

// create any older message(s)

older[17] = new Msg17_v2(peer);

// tell the peers about the older messages

peer[19]->setOlder( older );

}

// initialize the older revision

init();

};

Access from older message to the data in Msg19_v1 is through Msg20_v1. This is not a terribly eloquent mechanism, but the alternative is worse. The ability to access two or more independent parent types required an equal number of access template definitions. This, in turn, created new types, which required additional new access templates in the even older messages. This ripple effect violates the requirement to minimize rework in older message definitions when a revision is made.

So, the access function is:

Quaternions& Msg20_v1::getAttitude()

{

return static_cast<Msg19_v1*>(peer[19])->Attitude.get();

};

void Msg20_v1::setAttitude( Quaternions val )

{

// get a reference to the structure then set the sub-element

static_cast<Msg19_v1*>(peer[19])->Attitude.setValue( val );

};

And the Msg20_v1 init function is:

void Msg20_v1::init()

{

Msg17_v2* ptr17 = static_cast<Msg17_v2*>(older[17]);

ptr17->Pos.init( this, &Msg20_v1::Pos );

ptr17->Vel.init( this, &Msg20_v1::Vel );

ptr17->Attitude.init( this, NULL, &Msg20_v1::getAttitude, &Msg20_v1::setAttitude );

};

Therefore, an access of angular data from Msg17_v2, or lower, makes the call into Msg17_v2 and is passed into Msg20_v1, which will then access the data in Msg19_v1.

Building Libraries

Once the interface is written, it can be compiled into a dynamic library. This would be a .dll in the Windows environment, or a .dso in a Unix/Linux environment. As has been demonstrated in the test case, in a Visual Studio 2010 environment, newer libraries can be built and dropped into an existing application without impacting the application. Two .dll projects are provided, one containing only messages 15 and 16, and the other containing all of the messages. The user can swap in either .dll into the simpler test case, and it will continue to perform the same.

Conclusions

In a complex system, change is inevitable. Unfortunately, the cost of implementing a “simple” change that ripples through a body of code can be staggering. The resulting reticence to change hinders a program's ability to field changes in a timely manner. Under the classic waterfall methodology of development this is a painful fact. But these delays can eliminate the advantages of an agile development environment.

Yet this does not have to be the case. With minimal development effort (and only a slight increase in the calling complexity), the schedule, cost and risk of changing the interface in a system can be more closely managed.