As writers of software we strive to craft code with high cohesion and loose coupling. This can be tough to do with the Actor Framework as the default method of sending messages between actors effectively couples them together. Depending on your application this coupling may not matter, but I will show you one way to manage it if it does.
The above image illustrates a small section of the flow of control for an application used to calculate properties of a black hole. There is a high level module used to calculate the tidal force that objects experience while near a black hole's event horizon. This tidal force calculation requires the black hole's mass as an input, which is calculated by a lower level module. The high level tidal force module needs the black hole's mass in order to perform its calculation, and it depends on the low level mass module to supply it.
Quick Note: Based on some feedback I received on this post I posted a simple UML primer
The image above shows the source code dependencies between the different classes that make up the black hole application. Lets take a look at the LabVIEW code that implements these functions.
The Calculate Tidal Forces Actor calls the Send method of the Calculate Mass Message, which invokes its Do method.
The Do method of the Calculate Mass Message calls the Calculate Mass method of the Calculate Mass Actor, which performs the mass calculation.
The Calculate Mass method of the Calculate Mass Actor uses the Send method of the Calculate Force Message to pass the calculated mass back to the Calculate Tidal Forces Actor.
Calling the Send method of the Calculate Force Message invokes its Do method, which calls the Calculate Force method of the Calculate Tidal Forces Actor.
Take a moment and compare the image showing the flow of control of the application to the image showing the source code dependencies. The run time dependencies between modules as dictated by the flow of control are the same as the source code dependencies. If we ever expect to use the the Calculate Mass Actor without also using the Calculate Tidal Forces Actor (which is a very reasonable expectation to have, as the mass is an input to many other black hole property calculations) then we will need to break the source code dependency between the two modules, inverting it against the flow of control.
This inversion of dependencies is known as the Dependency Inversion Principle (DIP), and is one of Bob Martin's five S.O.L.I.D. principles of object-oriented design, which you can read about in the book shown above. The DIP states that:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.
We can invert the Calculate Tidal Force Actors dependency on the Calculate Mass Actor by creating an Abstract Calculate Mass Actor that defines an interface for mass calculation, and having Calculate Mass Actor inherit from it.
Notice the arrow illustrating that the Calculate Mass Actor implements the Abstract Calculate Mass Actor opposes the flow of control. This is the "Inversion" in the Dependency Inversion Principle.
By creating the Abstract Calculate Mass Actor and having the Calculate Mass Actor inherit from it we have satisfied the two parts of the definition of dependency inversion.
A. The high level module, Calculate Tidal Forces Actor, no longer depends on the low level module, Calculate Mass Actor, it now depends on an abstraction.
B. The Abstract Calculate Mass Actor defines an interface for calculating the mass of black hole but does not define the details of that calculation; the details are defined by the Calculate Mass Actor, which implements the abstraction.
The only difference from our codes' point of view is that now the Do method of the Calculate Mass Message depends on the dynamically dispatched Calculate Mass method of the Abstract Calculate Mass Actor instead of the Calculate Mass method of the Calculate Mass Actor.
We are not entirely out of the woods yet though. While the Calculate Tidal Forces Actor no longer depends on the Calculate Mass Actor, the Calculate Mass Actor does still depend on the Calculate Tidal Forces Actor. This dependency exists because the Calculate Mass method of the Calculate Mass Actor still calls the Send method of the Calculate Force Message, which invokes the Do method of the Calculate Force Message, which depends on the Calculate Tidal Forces Actor (This is easier to convey graphically than textually...)
The fact that the Calculate Mass Actor depends on the Calculate Tidal Forces Actor is a problem because we want to be able to use our Calculate Mass Actor with other modules without having each new module cause the Calculate Mass Actor's dependency tree to grow.*
*There is an assumption here that we actually do want to reuse the Calculate Mass Actor (or at least use it in a way that would cause us to benefit from cutting the dependency, like wrapping it up inside of a Packed Project Library), and that cutting this dependency will add value to our application(s). However, this may not always be the case, inverting dependencies for the sake of following DIP is not always necessary.
Needing to decouple the Calculate Mass Actor from its caller completely will be more difficult than decoupling the Calculate Tidal Forces Actor from the low level module that implements the Calculate Mass function. We can't simply create an abstraction for the Calculate Tidal Forces Actor because that would imply that the Calculate Mass Actor had a policy of needing to pass the result of the mass calculation back to a Calculate Tidal Forces interface. We need to be more generic than that, the Calculate Mass Actor's policy should be that it needs to pass the result of the mass calculation back to whoever requested the calculation in the first place. To satisfy this policy we will need to create an abstraction around the Actor Framework Message class.
We have created a message classed named Low Coupled Message and made it a child of the Actor Framework Message class. We can now create abstract children of the Low Coupled Message that define data types for messages, and have accessor methods that allow getting and setting data in the object. These abstract children define the data but do not override the Do method and so do not define how the data is used.
Because the Low Coupled Message does not override the Do method, which the Actor Framework Message class has marked as Must Override, we need to set the flag for "Transfer all Must Override requirements to descendant classes" on the Inheritance page of its class properties. The same needs to be done for the abstract data classes, as they also do not override the Do method.
Notice that it is possible to put a self addressed message into the class data of an abstract data class. This is illustrated in the UML above by showing that Abstract XYZ MSG has an ABC Message in its class data. This approach allows the ABC Class (not pictured) to send the Abstract XYZ MSG to the XYZ class (not pictured) while specifying that the XYZ class send data back via the ABC Message. This is equivalent to me mailing you a letter and putting a self addressed envelope inside the envelope I send you along side the letter, so that all you have to do is put your own letter in the envelope I provided and drop it in the mail.
We can apply the Low Coupled Message concept to our black hole property application by changing the Calculate Force Message and the Calculate Mass Message classes inheritance.
Notice how the Calculate Mass Actor no longer depends on the Calculate Force Message class, it now depends on both the Low Coupled Message and "Abstract Return Mass Message" classes. We have now inverted all source code dependencies against the flow of control.
We can take a tour of the software to see how this UML translates to code.
The Calculate Tidal Forces actor sends an Abstract Calculate Mass Message to the Calculate Mass actor that was injected at run time. Included in the payload of the Abstract Calculate Mass Message is a self addressed Calculate Tidal Force Message that the Calculate Mass actor can use to return the mass once it has been calculated.
The Send method of the Low Coupled Message class is just a wrapper around the Actor Framework Enqueue method.
The Do method of the Calculate Mass Message class uses the accessor methods from the Abstract Calculate Mass Message class to extract the data (including the return message) and pass it into the Compute Mass method of the Calculate Mass class.
The Calculate Mass method performs its calculation and then uses the Write Mass accessor from the Abstract Return Mass Message to pack the data into the reply message and send it using the Send method of the Low Coupling Message class.
The Do method of the Calculate Force Message use the Read Mass accessor from the Abstract Calculate Force Message to extract the mass and then pass it into the Calculate Tidal Force method of the Calculate Tidal Forces actor.
By making these changes to how our actors communicate with each other we are able to decouple our actors from each other. In the example above we used the Abstract Calculate Mass Message class and the Abstract Return Mass Message class to create a generic way for high level modules (like the Calculate Tidal Force actor) to communicate with our low level Calculate Mass actor. This generic communication was achieved by inverting the source code dependencies between the modules, while leaving the run time dependencies the same.
Jon McBee is a Principal Software Engineer at Cambridge NanoTech and is a Certified LabVIEW Architect, Certified LabVIEW Embedded Developer, Certified TestStand Developer, an NI Certified Professional Instructor, and a LabVIEW Champion