25

I'm working on a project that processes requests, and there are two components to the request: the command and the parameters. The handler for each command is very simple (< 10 lines, often < 5). There are at least 20 commands, and likely will have more than 50.

I've come up with a couple of solutions:

  • one big switch/if-else on commands
  • map of commands to functions
  • map of commands to static classes/singletons

Each command does a little error checking, and the only bit that can be abstracted out is checking for the number of parameters, which is defined for each command.

What would be the best solution to this problem, and why? I'm also open to any design patterns I may have missed.

I've come up with the following pro/con list for each:

switch

  • pros
    • keeps all commands in one function; since they're simple, this makes it a visual lookup table
    • don't need to clutter up source with tons of small functions/classes that will only get used in one place
  • cons
    • very long
    • hard to add commands programmatically (need to chain using default case)

map commands -> function

  • pros
    • small, bite-size chunks
    • can add/remove commands programmatically
  • cons
    • if done in-line, same visually as switch
    • if not done in-line, lots of functions only used in one place

map commands -> static class/singleton

  • pros
    • can use polymorphism to handle simple error checking (only like 3 lines, but still)
    • similar benefits to map -> function solution
  • cons
    • lots of very small classes will clutter project
    • implementation not all in the same place, so it's not as easy to scan implementations

Extra notes:

I'm writing this in Go, but I don't think the solution is language-specific. I'm looking for a more general solution because I may need to do something very similar in other languages.

A command is a string, but I can easily map this to a number if convenient. The function signature is something like:

Reply Command(List<String> params)

Go has top-level functions, and other platforms I'm considering also have top-level functions, hence the difference between the second and third options.

8
  • 8
    map commands to functions and load them at runtime from configuration. use the command pattern. Commented Apr 3, 2013 at 17:03
  • 4
    Don't be too afraid of creating lots of small functions. Usually, a collection of several small functions is more maintainable than one huge function. Commented Apr 3, 2013 at 17:19
  • 9
    What's with this topsy-turvy world, where people answer questions in the comments and ask for more information in the answers?
    – pdr
    Commented Apr 3, 2013 at 17:23
  • 4
    @SteveEvers: If it doesn't need elaboration, it's an answer, no matter how short. If it does, and you don't have time or whatever, then leave it for someone else to answer (always feels like cheating to write an answer that confirms a comment that already has a half-dozen upvotes). Personally, I feel this does need elaboration, the OP really wants to know why the best solution is the best solution.
    – pdr
    Commented Apr 3, 2013 at 18:06
  • 1
    @pdr - Right. My inclination was a map of commands to functions, but I'm a relatively junior programmer in a CS design course. My professor likes lots of classes, so there's at least 2 legitimate solutions. I wanted to know the community's favorite.
    – beatgammit
    Commented Apr 3, 2013 at 19:28

6 Answers 6

15

This is a great fit for a map (2nd or 3rd proposed solution). I've used it dozens of times, and it's simple and effective. I don't really distinguish between these solutions; the important point is that there is a map with function names as the keys.

The major advantage of the map approach, in my opinion, is that the table is data. This means that it can be passed around, augmented, or otherwise modified at runtime; it's also easy to code additional functions which interpret the map in new, exciting ways. This wouldn't be possible with the case/switch solution.

I haven't really experienced the cons that you mention, but I would like to mention an additional drawback: dispatching is easy if only the string name matters, but if you need to take additional information into account in order to decide which function to execute, it's a lot less clean.

Perhaps I've just never run into a hard enough problem, but I see little value in both the command pattern and encoding the dispatch as a class hierarchy, as others have mentioned. The core of the problem is mapping requests to functions; a map is simple, obvious, and easy to test. A class hierarchy requires more code and design, increases the coupling between that code, and may force you to make more decisions upfront that you will later be likely to need to change. I don't think the command pattern applies at all.

4

Your problem lends very nicely to the Command design pattern. So basically you will have a base Command interface and then there would be multiple CommandImpl classes that would implement that interface. The interface essentially needs to just have a a single method doCommand(Args args). You can have the arguments passed in via an instance of Args class. This way you leverage the power of polymorphism instead of clunky if/else statements. Also this design is easily extensible.

4

Whenever I find myself asking whether I should use a switch statement or OO-style polymorphism I refer to the Expression Problem. Basically, if you have different "cases" for your data and wand to support different "actions" (where each action does something different for each case) then its really hard to make a system that naturally lets you add both new cases and new actions in the future.

If you use switch statements (or the Visitor pattern) then its easy to add new actions (because you wite everything in a single function) but hard to add new cases (because you need to go back and edit the old functions)

Conversely, if you use OO-style polymorphism its easy to add new cases (just make a new class) but hard to add methods to an interface (because then you need to go back and edit a bunch of classes)

In your case you only have one method that you need to support (proccess a request) but lots of possible cases (each different command). Since making it easy to add new cases is more important than adding new methods, just make a separate class for each different command.


By the way, from the point of view of how extensible things are, it doesn't make a big difference whether you use classes or functions. If we are comparing against a switch statement what matters is how things get "dispatched" and both classes and functions get dispatched the same way. Therefore, just use whatever is more convenient in your language (and since Go has lexical scoping and closures, the distinction between classes and functions is actually very small).

For example, you can usually use delegation to do the error checking part instead of relying on inheritance (my example is in Javascript because O don't know Go syntax, I hope you don't mind)

function make_command(real_command){
    return function(x){
        if(check_arguments(x)){
            return real_command(x);
        }else{
            //handle error here
        }
    }
 }

 command1 = make_command(function(x){ 
     //do something
 })

 command2 = make_command(function(x){
     //do something else
 })

 command1(17);
 commnad2(42);

Of course, this example assumes that there is a sensible way to have the wrapper function or parent class check arguments for every case. Depending on how things go it might be simpler to put the call to check_arguments inside the commands themselves (since each command might need to call the checking function with different arguments, due to a different number of arguments, different command types, etc)

tl;dr: There is no best way to solve all problems. From a "making things work" perspective, focus on creating your abstractions in ways that enforce important invariants and protect you from mistakes. From a "future-proofing" perspective, keep in mind what parts of the code are more likely to be extended.

1
  • Doesn't the polymorphic OO approach make the requests coupled with how they should be processed? Commented Sep 12, 2023 at 12:04
1

I've never used go, as a c# programmer I would probably go down the following line, hopefully this architecture will fit what you are doing.

I would create a small class/object for each with the main function to execute, each one should know its string representation. This gives you plugability which it sounds like you want as the number of functions will increase. Note that I wouldn't use statics unless you really need to, they don't give much of an advantage.

I would then have a factory which knows how to discover these classes at run time altering them to be loaded from different assembles etc. this means you don't need them all in the same project.

Thus also makes it more modular for testing and should make code nice and small, which is easier to maintain later.

0

How do you select your commands / functions?

If there's some "clever" way of selecting the correct function then that's the way to go - means that you can add new support (perhaps in an external library) without modifying the core logic.

Also, testing individual functions is easier in isolation than an enormous switch statement.

Finally, only being used in one place - you might find that once you get to 50 that different bits of different functions can be reused?

1
  • The commands are unique strings. I can map these to integers if needed.
    – beatgammit
    Commented Apr 3, 2013 at 16:52
0

I have no idea how Go works, but an architecture I have used in ActionScript is to have a Doubly Linked List that acts as a Chain of Responsibility. Each link has a determineResponsibility function (which I implemented as a callback, but you could write each link separately if that works better for what you're trying to do). If a link determined it had responsibility, it would call meetResponsibility (again, a callback), and that would terminate the chain. If it didn't have responsibility, it would pass the request to the next link in the chain.

Different use cases could be easily added and removed simply by adding a new link between the links of (or at the end of) the existing chain.

This is similar to, but subtly different from, your idea of a map of functions. The nice thing is that you just pass in the request and it does its thing without your having to do anything else. The down side is that it doesn't work well if you need to return a value.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.