Saturday, 14 November 2020

OfficeFloor: going beyond Dependency Injection

This was a submission to DZone's Computer Science Fare:  https://dzone.com/articles/officefloor-going-beyond-dependency-injection

 
So why another Dependency Injection (DI) framework?  Because Dependency Injection is only part of the Inversion of (Coupling) Control problem.  OfficeFloor provides the complete injection solution.


Dependency Injection Problem

Dependency Injection on it's own can actually promote lower cohesion and higher coupling.  Why, because Dependency Injection provides undisciplined short cuts to get references to objects.  I need a repository to retrieve some data, I just inject it.  I need some logic to work out some result, I just inject it.  Overtime with on going improvements to systems, everything starts to reference everything else.  Changing the interface of one thing becomes difficult, as so many things just pull it in (higher coupling).  Furthermore, because it is so easy to "just inject" dependencies, convenience starts polluting single purpose logic of classes (lower cohesion).

Now, in disciplined development, we use principles such as SOLID to attempt to reduce this problem.  However, we are continually fighting Lehman's laws of two general concerns:

  1. systems need to continually add new functionality to stay relevant
  2. new functionality increases complexity reducing ability to change the system

Too often deadlines, budgets, even just bad days, we just keep enhancing classes without paying much attention to the second problem.  This all builds up and eventually we either:

  • require an expensive rewrite of the system (typically not feasible by the business)
  • live with degraded ability to add new features (potentially becoming the death of the system as it can no longer stay competitive)

Furthermore, Dependency Injection just looks at the static structural concerns of how objects reference each other.  We have not added into the mix threading or how we can compose methods together (effectively gaining benefits of functional composition).  We find this problem so systematic in DI only systems, that it has actually pulled the OO Matrix veil over our eyes.


Going beyond Dependency Injection

This is where OfficeFloor is taking things beyond Dependency Injection.

OfficeFloor injects dependencies not only into objects but also as arguments into  methods.  This means we do not have to structurally link all objects together for the method to traverse these structures.  The method just adds a new parameter for a new dependency.  The result is we avoid high coupling because objects no longer require structural references to all other objects.

However, this only addresses the low coupling, as we also want higher cohesion.  Well, OfficeFloor not only injects objects into methods, it also injects other methods into methods.  This effectively provides function composition, a concept OfficeFloor has termed Continuation Injection (or easier to remember Function Injection).  The result is higher cohesion of methods.  Methods can focus on single purposes and be composed together to achieve more complex functionality.

Note that due to OfficeFloor's containerising of the execution of methods, OfficeFloor can also containerise Monads to be composed in with methods.  This allows blending Object Orientation with Functional Programming in the same application.  We are still exploring this aspect, but the composibility provided by OfficeFloor keeps methods focused on single purposes resulting in significantly higher cohesion.

Going even further

Now we could have stopped there, as we are gaining significant benefits over Dependency Injection.  However, this only lets you build more maintainable systems.  We also want to build highly performant systems too.

Now multi-threading is hard.  Frameworks usually opt for a single model of threading (thread-per-request / asynchronous single threaded).  Attempting to blend threading models requires senior developer skill sets.  Furthermore, these blended solutions can run into tricky problems as further incremental functionality is added by other less skilled developers.

OfficeFloor, allows assigning execution of the individual methods to particular thread pools (what it terms Thread Injection).  This allows blending various threading models (even thread affinity patterns) to achieve the right mix of performance and ease of development.  OfficeFloor even allows for different execution profiles based on different hardware.  This provides highly performant applications.
 

Elephant in the room

Regardless of all these injection patterns, the real elephant in the room for applications is growing complexity.  To really address the second general Lehman issue of complexity, we need to be able to see the problem.
 
Lines of code one after another requires us to mentally visualise the complexity in the code.  As we practice small incremental changes, complexity becomes hidden in the scroll blindness of the vast lines of code.  Yes, we modularise and use principles.  However, deadlines and budgets focus us on that small new feature and not the growing complexity elephant in your system.

Furthermore, this level of complexity becomes hard to explain to non-technical individuals who generally hold the purse strings or set priorities.  As code is already too complex, I find we start drawing diagrams to communicate the problem.  We then use these diagrams to justify "refactoring" efforts to simplify the code, with promises that we will be able to add new features faster after this.  What's even more "fun" is trying to provide estimates on the effort of the refactoring.  When opening the Pandora's box of tackling the complexity, I find in DI only systems, the uncovered herd of complexity elephants just trample over those estimates.

However, by the time you find the need to draw diagrams to start to communicate the complexity problem, it is all too late. Complexity has already become a problem.  The slow creep of new functionality has added complexity to the point technical debt may become interest only payments.  Basically, complexity is showing the signs the system is creaking to it's demise.

This late discovery of complexity goes against modern software practices.   We want early feedback on this complexity risk so that it can be addressed before it becomes a problem.

OfficeFloor's configuration of wiring the methods together is graphical.  This graphical configuration visually demonstrates the complexity of the application.  The more spaghetti the diagram looks, the more need to spend effort simplifying the system.  As this is visual from the start of the project and always up to date (as is the actual configuration of the system), complexity is easily seen.  This means complexity can be addressed before it starts becoming a problem.

So our motivation behind OfficeFloor is to provide the necessary improvements over Dependency Injection to ease the building of systems, ease the maintaining of systems, and avoiding the complexity increasing so that developers keep their hair when trying to wrangle in that new bit of functionality.

Inversion of Coupling Control

So you might ask, why aren't Dependency Injection frameworks adopting Function Injection / Thread Injection for a complete Inversion of Coupling Control solution?

Because Function Injection and Thread Injection are significantly harder to implement than just injecting objects into each other.

To achieve method injection, we needed to containerise the execution of each method.  The container:
  1.  Ensures all dependencies are available for the method
  2.  Delegates the execution of the method to the appropriate thread pool
  3.  Handles all exceptions coming from the method
Now, this is only the start of the problem.  Once we containerised the method, we needed to type the method.  To compose the methods together, we needed to know what dependencies on other methods and objects the method requires.  The result is a typing system for methods that allows them to be visually represented in the graphical configuration.  This then lets the wiring of the methods be graphically undertaken.

The model driven design issues of using graphical configuration also needs to be avoided.  This is achieved by the graphical configuration being modularised similar to class source files.   They are:
  • modularised into separate files
  • each modular file is then typed, so can be included like typed methods in other modules 
  • within each module there is a flat structure (this avoids hierarchies that cause source code merge issues - merge is just like flat lines of code)
  • the flat structure is sorted on save, so editing graphical configurations results in the same stored XML (avoiding duplications due to source code merging different line orderings) 
  • contain no generated identifiers causing merge differences between developers
Finally the backing XML to the graphical configuration is easily readable for those rare occasions of having to resolve merge conflicts.

Performance requirements dictated that we could not keep swapping threads for each method executed.  Hence, if the next method was to be executed by the same thread pool, the current thread would continue on to execute that method.  This meant that thread context switches only occurred when required and not for every method execution.  This allows OfficeFloor to act as both asynchronous single threaded (no context switch) and thread-per-request (when using thread pools for method execution).  However, as threading can be tailored to specific methods, there are various custom threading models available to improve performance of your application (even including thread affinity).

Building this flexibility in threading allows the containerised methods to be executed by any thread. Hence, memory synchronisation issues became important.  To avoid significant locking and memory synchronising between threads, the internals of OfficeFloor is written mostly via immutable structures.  This allows significantly reduced locking when executing the methods within the containers.

Furthermore, as methods are not statically linked like objects, there is need for fast look up of dependencies.  OfficeFloor provides a compiler that transforms the modular configuration into a graph to represent the entire application.  This graph is checked for type correctness so that invalid application configurations is detected at compile time.  Once deemed valid, the graph is compiled down into the execution engine that uses array index lookup of all dependencies for performance (avoiding hash map look ups by strings).

Show me the working code

The following provides working code demonstrations of OfficeFloor:

 

Conclusion

So next time you look to build an application, consider OfficeFloor.  With the enhancements of Function Injection and Thread Injection over Dependency Injection, OfficeFloor encourages significantly lower coupling and higher cohesion in your applications.  This avoids complexity from creeping up and can keep you adding new features long after a DI only system become frozen from complexity. See the tutorials to get going with OfficeFloor.


No comments:

Post a comment