Easy on the Case (Part 2)
First part of this article here.
So you won't be able to remove all these case structures. Don't try too hard, you may just end up making your code much more complicated than it should be. Here is one more question I ask to myself when I wonder whether a case structure could be replaced and refactored. Look at the case input selector, and determine its nature. Is it an ever-changing value, or is it a descriptive part of a bigger object/entity? For instance:
- A case structure that triggers a specific logging action because the acquired data is out of range.
- A case structure that plays a different sound depending on the model (or brand) of a toy.
Fundamentally, those are two very different scenarios. The "model" describes what kind of toy it is, it is one of its attributes and can alter its behavior.
Tell, Don't Ask
Because of this, we should not try to externalize this attribute by probing it and take different actions based on the result like we would normally do with a case structure. The toy itself should know what sound it should play. We are turning everything upside down. We philosophically consider the toy as an intelligent artifact, capable of making distinct choices when we want to compare it to other toys.
This principle, Tell Don't Ask, is well known and should be remembered. If you manage to apply it, you will write smarter code! And because people who write languages and frameworks also want you to go down this way, object-oriented programming exist. OOP leverages polymorphism. And polymorphism helps to remove branching like case structures. Remember, we don't want monster octopuses. Or If statements, like these guys.
Good for us, the vast majority of object-oriented principles can be applied in LabVIEW if you decide to write LVOOP code. It's a very different paradigm that adheres to other laws than the classic (exact word is procedural) programming. I'd like to keep this post of decent length so I'll skip the "How do I do LVOOP?" section. Feel free to navigate the LabVIEW examples, the NI website or the LVOOP Fundamentals course if you want too know more.
Now think about the toy. You can find many ways to identify groups of toys based on one characteristic. That's what we're going to do.
We'll write a Toy class. It's generic enough to contain some attributes such as the brand or the kid's age range.
And we'll write a Noisy Toy class (after all, we rarely see a wooden horse play a robotic sound). Any Noisy Toy is a Toy, so it shall inherit its abilities and attributes. Every Noisy Toy can play a sound, so we'll add a Play Sound.vi function to this class. We're going to leave it empty though. What sound are we going to play? We don't know just yet.
Time to make a difference between Karaoke Barbie and the fireman truck. We can create these two classes, and they will both inherit from Noisy Toy. From there, they're allowed to override the default Play Sound method, so let's write a specific Play Sound.vi for each class. The first one will sing Katy Perry and the second one plays the loud emergency siren (that all parents deeply love).
The project and code should look like this.
At run-time, the code decides which flavor of the Play Sound VI to run based on the wired class. First, the Barbie then the fireman truck.
By refactoring the code and taking advantage of polymorphism, we removed a potentially growing case structure (who knows if we won't have to deal with 200 different noisy toys some other day?) and modularized the code.
Some might argue over this solution because it seems more complex: several classes, more VIs, and the impression it takes more time to browse all the code.
In fact, the increased number of files legitimates this impression but this is as far as it gets. The actual complexity is lower for two reasons:
- we bundled the action and the data into one single structure: a class.
- the cyclomatic complexity has globally decreased in the main VI. This is because we have less branching, so the data path is more straight forward.
Another argument is something that I quickly mentioned in the first part of this article, cohesion. Cohesion increases when we separate a task into smaller reusable components. Doing so makes testing easier, and enables you to maintain and extend your code like a piece of cake: you know where to edit your code, and if coupling is low (the dependency with other functions), you won't need much work. We'll focus more on coupling some other time ;)
Convinced? Ready to kill more case structures with polymorphism?