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
- Multiple Interface Support Design Pattern is implemented using Façade Design Pattern (*).
- 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)
- Base Class constructor returns a cluster of all public Interface objects (Façade Class instances) for the Base Object. ([1] slide 22)
- Base Class constructor shall never return the by-value Base Object as this would be a different instance of the Base Class.
- 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:
- Implement a by-value class that needs to be exposed to different callers via different interfaces ([3] HT_Configuration_Handler class).
- 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.
- 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:
- A Helper Class ([3] HT_Configuration_Handler) needs implementing multiple Dependency Interfaces ([3] Scanner_Configuration_Interface and Fluidics_Configuration_Interface)
- For each Dependency Interface create a Facade Class (Dependency Interface clone + constructor method) and make it a child of the Dependency Interface Class
- Implement (by hand) all methods of Dependency Interfaces (possibly under a different name) in the Helper Class
- Add boilerplate “call-through” code to all Façade Class methods (Workflow A, Step 2)
- 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
- Allows implementing ‘regular’ Interfaces as well as Dependency Interfaces.
- Implementing Interfaces for Base Class Descendants comes at incremental cost (by extending Base Class Interfaces via Single Inheritance)
- Interfaces are stateless by-value classes – OK to branch the wire and pull throughout application code.
- Interfaces may implement synchronous (DVR) or asynchronous (Queue or User Defined Events) calls.
Design Pattern Cons
- Synchronous Interfaces require de-referencing DVRs on each call (performance hit)
- 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.
- 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.
- 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.
- 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