Blog

Delegates - The Lightning of (Coldbox 7) Empowerment

Jon Clausen June 27, 2023

Spread the word

Jon Clausen

June 27, 2023

Spread the word


Share your thoughts

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.

Recent Entries

ColdBox 7.2.0 Released

ColdBox 7.2.0 Released

ColdBox, a widely used development platform for ColdFusion (CFML), has unveiled version 7.2. Packed with compelling new features, bug fixes, and enhancements, this release is designed to empower developers by boosting productivity, refining scheduled task capabilities, and enhancing the overall reliability and efficiency of application development. This article will delve into the key highlights of ColdBox 7.2 and elucidate how these advancements can positively impact developers in their daily coding endeavors.

Luis Majano
Luis Majano
November 20, 2023
Into the Box 2023 Series on CFCast

Into the Box 2023 Series on CFCast

Excitement is in the air as we unleash the highly anticipated ITB 2023 series exclusively for our valued CFCast subscribers – and the best part? It's FREE for CFCast members! Now is the perfect time if you haven't joined the CFCast community yet. Plus, we've got an incredible End-of-Year deal that's too good to miss

Maria Jose Herrera
Maria Jose Herrera
November 20, 2023
Ortus Deals are Finally Here!

Ortus Deals are Finally Here!

The much-anticipated Ortus End-of-the-Year Sale has arrived, and it's time to elevate your development experience! Whether you're a seasoned developer, a tech enthusiast, or someone on the lookout for top-notch projects, Ortus has something special in store for you. Brace yourself for incredible discounts across a wide array of products and services, including Ortus annual events, books, cutting-edge services, and more.

Maria Jose Herrera
Maria Jose Herrera
November 15, 2023