views:

100

answers:

3

I am tasked to maintain and update a library which allows a computer to send commands at a hardware device and then receive its response. Currently the code is setup in such a way that every single possible command the device can receive is sent via its own function. Code repetition is everywhere; a DRY advocate's worst nightmare.

Obviously there is much opportunity for improvement. The problem is each command has a different payload. Currently the data that is to be the payload is passed to each command function in the form of arguments. It's difficult to consolidate functionality without pushing the complexity to a level that calls the library.

When a response is received from the device its data is put into an object of a class solely responsible for holding this data, they do nothing else. There are hundreds of classes which do this. These objects are then used to access the returned data by the app layer.

My objectives:

Throughly reduce code repetition

Maintain similiar level of complexity at application layer

Make it easier to add new commands

My idea:

Have one function to send a command and one to receive (the receiving function is automatically called when a response from the device is detected). Have a struct holding all command/response data which will be passed to sending function and returned by receiving function. Since each command has a corresponding enum value, have a switch statement which sets up any command specific data for sending.

Is my idea the best way to do it? Is there a design pattern I could use here? I've looked and looked but nothing seems to fit my needs.

Thanks in advance! (Please let me know if clarification is necessary)

+2  A: 

This reminds me of the REST vs. SOA debate, albeit on a smaller physical scale.

If I understand you correctly, right now you have calls like

device->DoThing();
device->DoOtherThing();

and then sometimes I get a callback like

callback->DoneThing(ThingResult&);
callback->DoneOtherTHing(OtherThingResult&)

I suggest that the user is the key component here. Do the current library users like the interface at the level it is designed? Is the interface consistent, even if it is large?

You seem to want to propose

device->Do(ThingAndOtherThingParameters&)
callback->Done(ThingAndOtherThingResult&)

so to have a single entry point with more complex data.

The downside from a library user perspective may that now I have to use a manual switch() or other type statement to tell what really happened. While the dispatching to the appropriate result callback used to be done for me, now you have made it a burden upon the library user.

Unless this bought me as a user some level of flexibility, that I as as user wanted I would consider this a step backwards.

For your part as an implementor, one suggestion would be to go to the generic form internally, and then offer both interfaces externally. Perhaps the old specific interface could even be auto-generated somehow.

Good Luck.

sdg
Thank you for your input. Your interpretation of my proposition is correct. However, the user does not have to handle the callback. The user would pass a pointer to a struct in with the "Do" method, which would then be populated by method called when a response is received from the device. My point of view is of someone who needs to add new commands to the library and finds it difficult and frustrating with the current architecture. I think you're correct, though, that I need to consider the user's perspective first. The individual command functions are more explicit.
ChadHydro
+1  A: 

Your first objective should be to produce a library that decouples higher software layers from the hardware. Users of your library shouldn't care that you have a hardware device that can execute a number of functions with a different payload. They should only care what the device does in a higher level. In this sense, it is in my opinion a good thing that every command is mapped to each one function.

My plan will be:

  • Identify the objects the higher data layers need to get the job done. Model the objects in C++ classes from their perspective, not from the perspective of the hardware
  • Define the interface of the library using the above objects
  • Start the implementation of the library. Perhaps an intermediate layer that maps software objects to hardware objects is necessary
  • There are many things you can do to reduce code repetition. You can use polymorphism. Define a class with the base functionality and extend it. You can also use utility classes, that implement functions needed for many commands.
kgiannakakis
Thanks for your input. Well, and I guess I should have been more clear about this, many of the use cases for this library include testing the device in certain circumstances so users need to see if the device properly responds to the commands. In which case they do need to think of it as a HW device executing commands. I can definitely see the advantage of having each command mapped to a separate function. And I like your idea of an intermediate layer. I could potentially keep the interfaces that are currently available and map the data to a new, better organized command payload setup.
ChadHydro
+1  A: 

Well, your question implies that there is a balance between the library's complexity and the client's. When those are the only two choices, one almost always goes with making the client's life easier. However, those are rarely really the only two choices.

Now in the text you talk about a command processing architecture where each command has a different set of data associated with it. In the olden days, this would typically be implemented with a big honking case statement in a loop, where each case called a different routine with different parameters and perhaps some setup code. Grisly. McCabe complexity analysers hate this.

These days what you can do with an OO language is use dynamic dispatch. Create a base abstract "command" class with a standard "handle()" method, and have each different command inherit from it to add their own members (to represent the different "arguments" to the different commands). Then you create a big honking array of these at startup, usually indexed by the command ID. For languages like C++ or Ada it has to be an array of pointers to "command" objects, for the dynamic dispatch to work. Then you can just call the appropriate command object for the command ID you read from the client. The big honking case statement is now handled implicitly by the dynamic dispatch.

Where you can get the big savings in this scenario is in subclassing. Do you have several commands that use the exact same parameters? Make a subclass for them, and then derive all of those commands from that subclass. Do you have several commands that have to perform the same operation on one of the parameters? Make a subclass for them with that one method implemented for that operation, and then derive all those commands from that subclass.

T.E.D.
Thanks for your input. I definitely agree that, whatever I end up doing, I don't want to increase client complexity. What you describe as the method used in the olden days is close to what I'm proposing. The case statement would be used to setup the payload sent to the device.Your device dispatch idea is something I've been considering. I don't necessarily like the idea of a big honking array (the setup could be messy), but it looks like I'm going to end up with a big honking SOMETHING regardless of my approach.
ChadHydro
You can actually get some code sharing in the old-timey `case` dispatching method...but it involves using gotos. That isn't something most folks want to (or even should) do.
T.E.D.
Haha, no I'd rather not be haunted by the ghost of Dijkstra :). I've been thinking a lot about your suggestion. My current thinking is to, as you said, have a class per command which inherits from a base class. Each class would have a SendCommand and HandleResponse method. The class would also encapsulate all data necessary to send the command and any data received in response to the command. The library already uses a functor map to decide which response function to call when a response is received so it wouldn't be too much of a change.
ChadHydro
which is exactly what you said...
ChadHydro
Definitely beats `goto`s. :-)
T.E.D.