Function Call Of Injected Function
def secondary(data):
# Do stuff.
def main(injected_func):
...
injected_func(data)
...
def orchestrator():
...
main(injected_func=secondary)
Simplicity:
No longer simple. There are now three modules required. However, each module is itself very simple.
The coupling consists of two elements: the Orchestrator passing Function B into Module A, and the call on Function B. Worryingly, the two elements of coupling are necessarily in different parts of the code.
The function call has been anonymised: When reading the main() function alone, it is impossible to know what real function the injected function actually is. (In fact, An IDE would probably be unable to 'jump to source' of injected_func.) We may say that the connection is indirect or abstract.
In order to fully understand the coupling, the reader needs to follow a function as it is passed around the code. The complexity of indirect connection is a mental load on the reader.
Replaceability:
The defining attribute of this form of coupling is that the main() function does not need to be touched to change functionality.
However, the orchestrator function will need to be changed, to pass in the new function.
# def secondary(data):
# # Do stuff.
def alternative(data):
# Do stuff better.
def main(injected_func):
...
injected_func(data)
...
def orchestrator():
...
# main(injected_func=secondary)
main(injected_func=alternative)
- Write new replacement function.
- One-line change in orchestrator function, to pass in alternative function.
main()function is not changed.
Method Call On Injected Object
class SomeStupidClass:
def secondary(self, data):
# Do stuff.
def main(injected_object):
...
injected_object.secondary(data)
...
def orchestrator():
...
stupid_object = SomeStupidClass()
main(injected_object=stupid_object)
Simplicity:
Relatively complex. There are now three modules required.
The coupling consists of three elements: the Orchestrator instantiating Object B, the Orchestrator passing Object B into Module A, and the call on Method C of Object B.
Disturbingly, the method call is necessarily in a different part of the code from the other elements of the coupling.
Note that while the object is anonymous, the method call is on the actual named method on the class.
This coupling pattern can manifest in a range of different ways, from perfectly readable to horrendously obfuscated, depending on how the type (class) of the injected object is controlled. Let's explore three of those ways.
Injected Object Has Single Fixed Class
First, consider the case where the injected object is always an instance of a single class. The object may vary in its particulars, but there is only one bit of code to look at. This should not pose any great problems for readability.
Injected Object Of Unknown Class But With Well-Defined Interface
Next, consider the case where the class of the injected object is unknown, but the method constitutes a well-defined interface, in the following sense: While the behaviour of the method necessarily changes depending on the class, the behaviour is always sufficiently well-constrained that the reader does not really need to know about the detailed differences between the methods of the different classes. In other words, the method call can always be treated as a call to a 'black box', whose inner workings do not matter to the reader trying to understand the code of Module A.
The method polymorphism used in this case should not pose a great problem for readability, as long as the software design guarantees that the reader can always treat the method call as a call to a closed 'black box'. In practice, it may be difficult to always achieve this. If the reader does end up needing to know the details inside a method, then this interface could be considered a 'leaky abstraction', at least from the reader's point of view, because the information hidden inside the abstraction 'leaks out'.
Injected Object Of Unknown Class
Finally, consider the case where the class of the injected object is unknown, and the reader simply must read the details inside the method in order comprehend the code.
The method call on the object is an indirect connection: When reading the main() function alone, it is impossible to know the real class of the injected object. (In fact, An IDE will probably not be able to 'go to source' of injected_object.)
In order to fully understand the coupling, the reader needs to follow an object as it is passed around the code. The complexity of indirect connection is a mental load on the reader.
Replaceability:
Could in principle depend on whether the new functionality needs a whole new class or just a new method on the existing class. However, the whole point of the flexibility of the orchestrator pattern is to easily inject a different object, so that's what we'll consider here.
# class SomeStupidClass:
#
# def secondary(self, data):
# # Do stuff.
class LessStupidClass:
def secondary(self, data):
# Do stuff better.
def main(injected_object):
...
injected_object.secondary(data)
...
def orchestrator():
...
# stupid_object = SomeStupidClass()
stupid_object = LessStupidClass()
main(injected_object=stupid_object)
- Write new replacement class with different method of same name.
- One-line change in orchestrator function, to instantiate new class.
main()function is not changed.
🙠