If you attended my presentation at NI Developer Day back in March, you probably recognize the content of this three-part blog series. The premise of my presentation was simple and rather obvious given its title “You Already Know How to Use LabVIEW Classes.” In the end, object-oriented LabVIEW is simply a programming style that encourages software modularity and reuse. The doing is easy because it requires mostly bits and pieces with which we are already familiar. Understanding the reasons for doing lets us assemble these familiar bits into a better object-oriented application.
If you are here to learn, I strongly urge you to read—or brush up on—the previous two pillars of object-oriented programming before diving in (For your convenience: inheritance and encapsulation). Welcome to the third and final installment!
Pillar Three: Polymorphism – Class methods can adapt to the input object type
Many of the LabVIEW primitives and device drivers we wire on a daily basis use polymorphic behaviors to improve developer experience. Have you noticed a function that behaves differently on the block diagram, depending on the wiring of its inputs? Have you configured a DAQmx reference by choosing the task type from a drop-down list? If so, you are already familiar with polymorphism.
The polymorphic behavior of LabVIEW classes is very much like the polymorphism we already know and love—with one neat twist: class methods can adapt to the input type while software is executing. This behavior is more than just cool, because it enables us to write clean software that maximizes both code reuse and future flexibility. When we pair polymorphism with the other two pillars of object-oriented programming, new and exciting possibilities emerge.
- Edit-time – Before pressing the run arrow
- Run-time – After pressing the run arrow
- Polymorphic – Adapting to (or adaptable to) data type
- Dynamic Dispatch – Dynamic adaptation to an object type (i.e. run-time polymorphism)
- Override – Calling a descendant’s method instead of the ancestor’s method (i.e. to do something else)
- Extend – Calling a descendant’s method in addition to the ancestor’s method (i.e. to do something extra)
- Abstract – Implementing partial or no functionality (intentionally incomplete)
- Concrete – Implementing complete functionality
If You Use LabVIEW, You Use Polymorphism
It’s hard, if not outright impossible, to write G code without using polymorphism. If you doubt my assertion, consider these common examples:
The add node adds two scalar values:
…But the same add node can also add a scalar value to every element of an array. (Note that the Measurements input and the Scaled Measurements output are both arrays, and that there are no broken wires.)
When we want to index elements from an array, we call the ubiquitous Index Array function, which handles arrays of different dimensions and also lets us expand the function to return multiple elements at once.
You might also frequently use VIs with a selector box hovering below. This box is called a polymorphic VI selector and lets you select the specific instance of the VI you actually want to call. Note that Read Key, below, returns a Double value, courtesy of its polymorphic selection. (Using the Automatic option lets us set the data type by simply wiring a valid data type to the default value input.)
These examples all demonstrate edit-time polymorphism. The developer calls one node (or VI) and changes its behavior, either by wiring a specific data type or by modifying the function on the block diagram. You might have even made your own polymorphic VIs in LabVIEW. It’s really not hard and can provide a more polished developer experience for users of your code.
Known in LabVIEW as dynamic dispatch (DD), run-time polymorphism allows classes to call specific method implementations on the fly, while a VI is running. DD allows more dynamic execution and more scalable designs.
You have two choices when creating a new class method in LabVIEW: VI from Dynamic Dispatch Template and VI from Static Dispatch Template. While it might seem daunting at first, this is simply a choice between a polymorphic method (DD) and a non-polymorphic method (static). Fear not—your choice is not permanent.
As shown below, the DD method has crosshatching around its object input and output terminals and the static method does not. A DD method simply allows child classes to extend it or to override it. If you are not sure which behavior you want out of the gate, I recommend choosing the static version and then changing it to DD later, if needed.
Changing a method to either DD or to static is quite simple—just right-click on the method’s connector pane terminal and then select your desired terminal behavior. (Choose Required if you want static dispatch behavior.)
Dynamic Dispatch in Action
Consider the example of message transport abstraction from the previous encapsulation post—there is one abstract MessageTransport class with four concrete children:
With polymorphism in the mix, we can now appreciate the full value of the hierarchy. Consider this code snippet of a VI that responds to a requestor using the message transport class:
At edit time, the pink MessageTransport wire is the abstract type. From this snippet, we do not know specifically which message transport type will be on the wire when the VI is running—we only know that it will be one of the several valid types. Double-clicking on the Send method above launches an edit-time dialog showing the full list of DD possibilities:
Since none of the items in the list above are grayed-out, we know that all of the message transport children implement their own version of the Send method. LabVIEW will call—or dispatch—the proper Send at run time, depending on the object type on the pink wire.
Thanks to polymorphism, we could wire any of these object types below to the Send method with no problems. We could also, as demonstrated above, rely on the abstract MessageTransport class and let LabVIEW dynamically dispatch the proper version at run time. (Note: it remains the developer’s responsibility to inject “the proper version” onto the wire somewhere before Send gets executed—alas, DD does not magically read your intentions.)
To appreciate the readability and modularity provided by object-oriented polymorphism, consider the non-object-oriented implementation of the message transport abstraction shown below.
This code is functionally equivalent to the class message transport example, but inferior for several reasons. Note that the calling code implements the business rules for every type of message reply inside five different cases of a case structure (flattened for your viewing pleasure). Also take note of the mega-cluster containing references and data for every type of currently supported messaging. It is wishful thinking to call the case structure example “modular”. Adding a new message transport type requires editing both the cluster and case structure. On the other hand, adding a new transport type to the class hierarchy requires no touching whatsoever of the existing code. Plus, isn’t it more readable when the caller depends only on abstract Send, letting each class handle its own business details?
Obligations and Expectations
It turns out that, together, inheritance and polymorphism provide a powerful way of designing reusable and extensible software. Thanks to a few key checkboxes in the Class Properties dialog, we can transfer additional rules to a child or descendant class:
These two choices allow the developer to specify contractual obligations for descendants on a method-by-method basis. Descendants are then allowed to reuse ancestor functionality as appropriate, while also ensuring they fill any intentional gaps in functionality. Failure to fulfill the obligations established in the dialog above generates an edit-time error (a broken run arrow and a description) that clearly communicates the unfulfilled obligation to the developer.
In some cases, a class can be fully abstract and earn the special designation of interface. You can read more about LabVIEW interface classes in my earlier post: So You Want a LabVIEW Interface?.
No Duct Tape Required
With the three pillars of object-oriented programming under your belt, it’s time to get your hands dirty. While each concept is interesting on its own, the real power comes from using them together. Polymorphism, along with the other two principles, gives us modularity we cannot otherwise achieve.
LabVIEW developers already leverage a uniquely powerful dataflow language to solve varied and difficult problems and we can solve these problems even more effectively by adopting an object-oriented mindset. When we treat LabVIEW like the real programming language that it is—using the right techniques, not just the old and comfortable—we all win.
My object-oriented learning was largely fueled by my colleagues, the internet, and, quite importantly, by my willingness to try something different. I hope you enjoyed this three-part blog series and found the content useful. The learning curve of object-oriented programming isn’t as steep as it might seem…and if my experience is any indication, you will enjoy the journey. Happy wiring!