One of the the most significant new features in Coldbox v7 and Wirebox v7, delegates, is arguably a new language feature rather than a framework feature. Despite the implementation not being native with either Lucee or Adobe engines, the functionality provided using delegates is both long overdue and likely to change the way you program - and, especially, the way you approach inheritance and code reusability.
Runtime Mixins - A.K.A Helpers
Coldbox has long had a concept of application-level "helpers", which may be provided by convention ( e.g. .cfm
files placed in the includes/helpers
directory and declared in your framework config ) by modules upon registration. Helpers, if you are not familiar with them, are .cfm
files which are included as mixins during the framework lifecycle and become first class methods available to handlers, interceptors, views, etc., once they have been declared and registered.
Generally Coldbox helpers are collections of UDF functions which the application or module developer wishes to include as first-class functions in everything they do. The downside to helpers is that, because they must be brought in via cfinclude
, they are subject to variable and name collisions within components. If you have a method declared in a component that collides with a helper in a module or in your custom helpers, you'll often receive an error that "routines may not be declared more than once". This adds to your application's technical debt, because upgrading Coldbox, itself, and modules which use helpers must be approached with more caution - to avoid breaking changes and collisions - but also due to refactoring in how those helpers are applied within your components. It also presents challenges in documenting your logic layer's API ( e.g. via docbox
) because those included helpers are, effectively, a mystery until runtime.
For example, I've seen several assistance requests in Slack and in the Ortus Community forums dealing with this particular runtime error using cbauth
:
component{
property name="auth" inject="AuthenticationService@cbauth";
function doSomething(){
// this code will blows up
if( variables.auth.check() ){
... do more somethings here ...
}
}
}
The attempt to use variables.auth
will blow up in this component because the cbauth
module declares an auth()
helper method - which retrieves the authentication service. Since Wirebox injects the property before the helpers, the property auth
is replaced by the method when the component is created. As such, the attempt to use variables.auth
without an invocation of it as a method now throws an error.
Helpers can be powerful, but the potential for collision and type-mismatch increases as you add more to your application, or more modules which use them.
The Ubiquitous Abuse of Inheritance
Another way to provide first-class component UDFs at runtime has been via inheritance. The inheritance pattern often utilizes a "Base
" or "Abstract
" component which grows in the form of helper UDF methods as functionality is needed in the child components. While this can be a viable way of handling methods that are specific to common variables within the child components, it also lends itself to "junking up" the base object with functions which may apply to only one or two of the child components - but have been placed in the inheritance tree to allow for reuse of the method.
In time, "base" components can grow to thousands of lines of code, as your application grows. In many cases, only a fraction of the LOC in the component are used by every child object.
Take for example this simple use of a "BaseObject", which is taken directly from Contentbox:
component {
public BaseEntity function init(){
return this;
}
/**
* Pass in a helper path and load it into this object as a mixin
*
* @helper The path to the helper to load.
*/
function includeMixin( required helper ){
// Init the mixin location and caches reference
var defaultCache = variables.cachebox.getCache( "default" );
var mixinLocationKey = hash( variables.coldbox.getAppHash() & arguments.helper );
var targetLocation = defaultCache.getOrSet(
// Key
"contentObjectHelper-#mixinLocationKey#",
// Producer
function(){
var appMapping = variables.coldbox.getSetting( "AppMapping" );
var UDFFullPath = expandPath( helper );
var UDFRelativePath = expandPath( "/" & appMapping & "/" & helper );
// Relative Checks First
if ( fileExists( UDFRelativePath ) ) {
targetLocation = "/" & appMapping & "/" & helper;
}
// checks if no .cfc or .cfm where sent
else if ( fileExists( UDFRelativePath & ".cfc" ) ) {
targetLocation = "/" & appMapping & "/" & helper & ".cfc";
} else if ( fileExists( UDFRelativePath & ".cfm" ) ) {
targetLocation = "/" & appMapping & "/" & helper & ".cfm";
} else if ( fileExists( UDFFullPath ) ) {
targetLocation = "#helper#";
} else if ( fileExists( UDFFullPath & ".cfc" ) ) {
targetLocation = "#helper#.cfc";
} else if ( fileExists( UDFFullPath & ".cfm" ) ) {
targetLocation = "#helper#.cfm";
} else {
throw(
message = "Error loading content helper: #helper#",
detail = "Please make sure you verify the file location.",
type = "ContentHelperNotFoundException"
);
}
return targetLocation;
},
// Timeout: 1 week
10080
);
// Include the helper
include targetLocation;
return this;
}
/**
* convert one or a list of permissions to an array, if it's an array we don't touch it
*
* @items One, a list or an array
*
* @return An array
*/
private array function arrayWrap( required items ){
return isArray( arguments.items ) ? arguments.items : listToArray( arguments.items );
}
}
In this case, at some point during development, it made sense to have these first class methods ( representing over 70 LOC ) in the base object.
A quick search shows that only two of the 17 components that extend the base object use the arrayWrap
method. The includeMixin
method effectively duplicates the Wirebox functionality for helpers and is only used in the DI completion of the components to allow module-specific helpers, but is only used in the BaseContent
object - which is extended by 3 other components, making it used by only 3 of 17 components. A quick scan of the BaseEntityMethods
component linked above shows over 150 lines of code dedicated to methods which are not widely used in the models which extend it. A quick scan through a number of client and internal applications on my development machine demonstrates this same kind abuse of inheritance, repeatedly - at times with thousands of lines of code becoming questionable, in hindsight.
This over-use and abuse of inheritance can unnecessarily bloat the memory footprint of components at compilation time and significantly increase the weight of metadata inspection during Direct Injection.
Delegating to Delegates
Now that we've identified some problems which traditional approaches to UDFs in CFML, let's shift our paradigm. Coldbox used the term Delegate
( borrowed from Groovy and Kotlin ) but, if you have worked with PHP, you may be familiar with Traits
. While the afformentioned languages approach the concept of "delegation" differently, the concept is the same - promote code reuse without the abuse of inheritance or the need for pass-through functions to other components.
Let's take our two functions from our "Base" object above and refactor them to use Delegation:
models/delegates/Arrays.cfc
component singleton{
/**
* convert one or a list of permissions to an array, if it's an array we don't touch it
*
* @items One, a list or an array
*
* @return An array
*/
array function arrayWrap( required items ){
return isArray( arguments.items ) ? arguments.items : listToArray( arguments.items );
}
}
models/delegates/Mixins.cfc
component singleton{
/**
* Pass in a helper path and load it into this object as a mixin
*
* @helper The path to the helper to load.
*/
function includeMixin( required helper ){
// Init the mixin location and caches reference
[... all of the code in the above method ...]
// Include the helper
include targetLocation;
return this;
}
}
Now that we have created delegates - one in which we can wrap any methods which might be used to create or manipulate arrays and one which deals with custom mixins, we can use them at runtime.
My "BaseContent" object still needs the mixin method post-DI, so I can bring in the delegate like so:
models/BaseContent.cfc
component delegates="Mixins"{
public BaseContent function init(){
return this;
}
/**
* Prepare the instance for usage
*/
function onDIComplete(){
// Load up content helpers
variables.wirebox
.getInstance( dsl: "coldbox:moduleSettings: contentbox" )
.contentHelpers
.each( function( thisHelper ){
includeMixin( arguments.thisHelper );
} );
}
}
My two components that use the arrayWrap
method can simply bring that method in as a first class UDF by including the delegate:
component extends="BaseEntity" delegates="Arrays"{
...
}
Let's say my Arrays
delegate grows and I only need that one arrayWrap
method. I can ensure only the one method in the component is delegated via the same annotation but using an =
operator after the name of the delegate component. Once I do so, only the arrayWrap
is available as a UDF to the component and its children:
component extends="BaseEntity" delegates="Arrays=arrayWrap"{
...
}
In addition to cleaning up our code base, my delegate components now become available as delegates to other components which are outside of the inheritance tree of my original BaseEntity
component. In many cases, using delegates, you may find that the use of inheritance becomes completely unnecessary, offering you significant compile-time and DI performance benefits.
In Summary
Delegates are game-changers for anyone using Wirebox, in the way you approach code reusability and inheritance. In addition to performance and readability improvements, they also provide flexibility for application-level changes to method behaviors and signatures via versioning ( e.g. no more need to refactor every component that extends BaseModel
if I want to change the behavior or method signature of one function ).
With new features like Delegates and Property Observers, Coldbox v7 and Wirebox v7 have moved beyond improvements to the "framework" in to the realm of improving the way we use the CFML language, itself!
Author's note: In case you noticed the title of this post is, indeed, paraphrased from the book "Zapp! The Lightning of Empowerment" by William Byham and Jeff Cox. If your job is one involving leadership, I highly recommend it.
Add Your Comment
(2)
Jun 27, 2023 23:34:45 UTC
by Will Belden
While this is certainly a quicker meta-inclusion, and allows for a direct usage, I assume, of the function (like the arrayWrap () example), I'm wondering about something like an arrayService. I often use a getServiceFactory().getUtilService().whatever(). Then, I only use it on demand which could be even less than *always* having it available in the object. Just wondering what the advantage of the delegates are beyond "shorter code lines".
Jul 05, 2023 10:06:30 UTC
by Luis Majano
Delegates are a fancier way of doing object composition, Will. It enhances the injection by not only injecting the object you want but also the methods you target as first-class methods in the target object. This allows you to construct nicer method constructs that respect composition and not break the inheritance abuse. It also promotes the building of smaller and more focused objects instead of heavy objects.