In my previous post, I introduced the concept of object-oriented inheritance. I also suggested that, as a regular G developer, you already know enough to start writing your own object-oriented code.
Maintaining this same (I hope empowering) perspective, let’s dive into the next foundational principle of object-oriented programming: encapsulation. Look for a post covering the final principle (polymorphism) shortly.
Pillar Two: Encapsulation – Classes protect their data
When writing any LabVIEW API (object-oriented or not), we spend time thinking about who or what should access the API’s varied functions. We might also create a palette to expose specific VIs for easy access and lock some VI block diagrams. These API design choices share an important concern: not every building block of the API should be directly accessible to the end user. There are always bits and pieces of code that, while integral to the function of the API, are never intended to light up a computer screen. We want to build an API that is easy to understand and difficult to break.
If you understand these basic design concepts, you already understand the main purpose of encapsulation: letting the developer define how the software gets used. Well-designed object-oriented software makes the use—and reuse—of classes both safer and more intuitive. Encapsulation helps us achieve these noble goals by preventing undesired modification of data and unexpected calling of methods. Not only do good classes take care of their own business, they also define which business details they share with the outside world.
- Access Scope – Rule defining what software has permission to access data or VIs/methods
- Friend – A user-defined relationship between classes or libraries (not inheritance-based)
LabVIEW access scopes:
- Private – Only the owning class or library can call or access (most restricted)
- Community – Only classes or libraries defined as Friends can call or access
- Protected – Only descendants can call or access (applies to classes only)
- Public – Anything can call or access (unrestricted)
You Already Had Encapsulation…Sort of.
With regular LabVIEW libraries, we already have the ability to set access scope (Right Click folder or VI>>Access Scope). Consider this example of non-object-oriented code:
Note that the Controller.lvlib library has a folder named Private VIs containing one member VI. (The red key icon overlay provides a visual cue that any files within this folder can only be called by other VIs also belonging to Controller.lvlib.) Calling private VIs or type definitions outside their specified access scope produces a broken run arrow at edit-time. Whether or not we choose to program with objects, scoping is a useful tool for maintaining clean software and communicating our design intent to other developers.
Now, With Classes!
Below, I present a simple but real-world example of object-oriented encapsulation. The example class abstracts message transport, meaning that the calling code need not fret the details of how messages get transported; every child implementation of the MessageTransport class handle its own business. The examples below focus mostly on the QueueMessageTransport child class:
Remember, from the inheritance post, that an object is just a special type of cluster. Elaborating on that idea, these statements are true of LabVIEW classes, without exception:
- Every LabVIEW class library contains one object (private data cluster)
- The object name always matches the class name (it changes automatically if you rename the class library)
- All of a LabVIEW object’s data are privately scoped (hence the label “Cluster of class private data”)
While a class library’s object is always entirely private, we have freedom to assign different access scopes to any methods or type definitions inside a class. Thus, we completely control how software outside of our class uses and manipulates it.
The QueueMessageTransport object contains the private data required for queued messaging:
Just as with a cluster type definition, double-clicking the private data object in a LabVIEW project opens the data cluster for editing. In this case, the single class datum is a LabVIEW queue reference.
When we want to use the queue reference inside of a class method (to read from the queue, for instance), we simply unbundle the reference from the class data wire using unbundle by name.
…But when we try to unbundle the class data outside of a QueueMessageTransport method, LabVIEW shows a broken wire:
Encapsulation keeps our classes in charge of their own data, but there is a workaround—creating an accessor VI. To create an accessor, right click on a class and select New>>VI for Data Member Access.
A class can then allow other methods or VIs to call an accessor by adjusting the accessor’s scope as described above in the non-object-oriented Controller.lvlib example.
An accessor simply bundles (write accessor) or unbundles (read accessor) data onto or off of the class wire. This diagram shows the functionality of a basic read accessor:
Caveat: Just because you can…
A public accessor for the QueueMessageTransport class lets any VI effectively unbundle the class’s queue reference:
This accessor effectively makes the QueueMessageTransport class nothing more than a hard-to-use cluster. If we really do not care about protecting the transport details, we might have saved ourselves the trouble of creating the whole MessageTransport hierarchy and coded this instead:
A public queue reference accessor allows any VI or method (anywhere) to call primitives from the LabVIEW queue palette on the previously private queue, potentially altering the behavior of the class. Is this a good thing? What is the purpose of the class if any VI can access or destroy the queue reference?
There is a reason accessors are considered leaks in abstractions: they leak details and data to the outside world, eroding the benefits of abstraction. If a caller of QueueMessageTransport unbundles the queue reference, that caller no longer depends on the MessageTransport class, it specifically depends on the queue-based child implementation (and this is not good).
Many accessors are road signs pointing you toward a better design. (I tend to think of accessors as vectors of disease transmission between classes.) Sometimes accessors are necessary, but if you have an accessor for most data in your private data cluster, there is probably a better way.
A good class hierarchy works without exposing unnecessary details of the specific sub-class being used. In the MessageTransport example, the details of transporting messages are abstracted from the calling VI. The Read method is public; its details are private and the act of reading is simple:
While the examples above focus on the two extremes of the access scope spectrum (public and private), remember that there are two additional scopes available. The protected access scope lets us share class details only with descendants and the community access scope lets us share class details between friend classes, even when the friends have no inheritance relationship.
Encapsulation empowers developers to design better software, preventing unintended behavior and encouraging proper use. You might want public or protected class data (if you recall, those don’t exist) and you might need accessors (on occasion, you really will need them) …but as you grow your object-oriented skillset, I predict that both your desire for and use of these things decreases precipitously. Now, for my final suggestions: don’t worry about design nuances initially, get your hands dirty, and refactor as you learn. Stay tuned for the thrilling conclusion to this series: object-oriented polymorphism!