LabVIEW Development Best Practices Documents

cancel
Showing results for 
Search instead for 
Did you mean: 

Design Pattern: Multiple Interface Support in G

The Multiple Interface Support in G Design Pattern had been first presented on the NIWeek 2016 Advanced Users Track (AKA “Room 15”):

[1] TS9518 How Applying Agile SW Design Principles Changed My Designs and Code

 

In a follow-up discussion I’ve been asked for a more detailed explanation and sample code – leading to this document and bonus material (attached) :

[2] HT_Configuration_Handler_Details.pptx

[3] ArT_HT_Configuration_Handler_Example_LV2015.zip

 

Intent

 

To provide a way for calling the same Object through different Interfaces.

 

Motivation

 

G classes support Single Inheritance only. No Interfaces (Java, C#). No Mixins (C++, Groovy, Python, Ruby, etc.). No Traits (Scala). To achieve high levels of code reuse contemporary software design techniques (such as SOLID Design Principles) recommend separating callers from concrete class implementations through some sort of well-defined and stable “interfaces”. In particular, the Interface Segregation Principle (ISP) requires that “Clients should not be forced to depend on methods that they do not use”. This LabVIEW-specific Design Pattern allows creating multiple “interfaces” (suitable for different caller types) to the same by-value base class.

 

Implementation

 

  1. Multiple Interface Support Design Pattern is implemented using Façade Design Pattern (*).
  2. Each Interface is a Façade Class, exposing a desired subset of Base Class methods and wrapping a DVR (or a Queue Refnum) to the same Base Class object ([1] slide 21)
  3. Base Class constructor returns a cluster of all public Interface objects (Façade Class instances) for the Base Object. ([1] slide 22)
  4. Base Class constructor shall never return the by-value Base Object as this would be a different instance of the Base Class.
  5. Base Object can be manipulated only through its Interface Objects.

 

(*) Facade Pattern is used to provide a simple and specific Interface to one or more classes that have a complex and general Interface. Facade Pattern looks much like Adapter and Decorator Patterns, but the intent (use cases) are quite different.

 

To implement ‘regular’ Interfaces follow Workflow A:

  1. Implement a by-value class that needs to be exposed to different callers via different interfaces ([3] HT_Configuration_Handler class).
  2. Create Facade Classes implementing such interfaces ([3] HT_Assembler_Configuration class). Each Façade class may ‘wrap’ a different subset of Base Class methods and keep a DVR to the same Base Class instance in its private data.
  3. Make Base Class constructor return a cluster of all public Interface objects for the Base Object. ([2] slide 1).

 

Single Responsibility Principle requires classes to delegate implementing non-core functionality to helper classes. Dependency Inversion Principle requires decoupling classes from their helper classes through Dependency Interfaces.

 

Follow Workflow B to implement Dependency Interfaces for a given Helper Class:

  1. A Helper Class ([3] HT_Configuration_Handler) needs implementing multiple Dependency Interfaces ([3] Scanner_Configuration_Interface and Fluidics_Configuration_Interface)
  2. For each Dependency Interface create a Facade Class (Dependency Interface clone + constructor method) and make it a child of the Dependency Interface Class
  3. Implement (by hand) all methods of Dependency Interfaces (possibly under a different name) in the Helper Class
  4. Add boilerplate “call-through” code to all Façade Class methods (Workflow A, Step 2)
  5. Make Helper Class constructor return a cluster of all public Interface objects (including Dependency Interface objects) for the Helper Object. ([2] slide 1; Workflow A, Step 3)

 

Design Pattern Pros

 

  1. Allows implementing ‘regular’ Interfaces as well as Dependency Interfaces.
  2. Implementing Interfaces for Base Class Descendants comes at incremental cost (by extending Base Class Interfaces via Single Inheritance)
  3. Interfaces are stateless by-value classes  – OK to branch the wire and pull throughout application code.
  4. Interfaces may implement synchronous (DVR) or asynchronous (Queue or User Defined Events) calls.

 

Design Pattern Cons

 

  1. Synchronous Interfaces require de-referencing DVRs on each call (performance hit)
  2. Requires substantial amount of boilerplate code – a royal pain when done by-hand. It would be very helpful having a scripting tool for generating Interface Classes (both, Workflow A and Workflow B)

 

Editorial Comments

 

[Dmitry Sagatelyan] Provided sample code ([3]) shows how to implement both, ‘regular’ Base Class Interfaces and Dependency Interfaces. It also shows how to design ([2]) and implement an application-specific Configuration Handler class.

 

  1. Constructor Injection may be used to pass Configuration Clusters into class constructors (Init) and take it out of Class Destructors (Done). With this design a class does not need Load_Config and Save_Config methods. Assembler class loads the entire Configuration supercluster on startup and passes appropriate configuration subclusters to subsystem constructors ([1] slides 13, 44). Simple and lightweight.
  2. It gets more complicated when subsystems need to get/update their configuration clusters while application is running. Dependency Injection cannot be used in such case. Subsystems have to make Get/Update_Config calls as they deem necessary. This is when Service Locators ([1] slide 47) save the day.
  3. A Configuration Handler is required when two or more subsystems need getting/updating their configuration data at runtime ([1] slide 47). Reusable subsystem code (Scanner and Fluidics classes) should be kept decoupled from each other as well as from application-specific Configuration Handler class. This may be done by introducing Dependency Interfaces ([2] slide 1) per Dependency Inversion Principle. In a nutshell ([2] slide 2):
    • Configuration Handler implements Scanner and Fluidics Dependency Interfaces ([3] Scanner_Configuration_Interface and Fluidics_Configuration_Interface classes)
    • Configuration Handler constructor creates Dependency Interface objects and passes them to caller (HT Scanner Assembler object)
    • HT Scanner Assembler object injects Dependency Interface objects into reusable subsystems (Scanner and Fluidics objects) instead of injecting their configuration clusters (as done with Constructor Injection) - enabling reusable subsystems to call Get/Update_Config methods via Dependency Interface objects at runtime.

Enjoy,

 

Dmitry Sagatelyan

CLA, LabVIEW Champion

Arktur Technologies LLC

 

Contributors