Blog

Lazy Properties and Property Observers in ColdBox 7

Michael Born July 05, 2023

Spread the word

Michael Born

July 05, 2023

Spread the word


Share your thoughts

ColdBox 7 is a big release for many reasons, but probably the most significant updates center around Wirebox, ColdBox's built-in dependency injection framework.

This article will focus on two smaller but still wildly useful new Wirebox features: Lazy Properties and Property Observers. With these new DI features, developers have access to speed boosts and automatic locking around object construction with lazy properties, better logging and debugging capabilities (among many other uses) with property observers, and much much more.

I personally believe delegation is the most significant new feature in Coldbox 7. But as Jon Clausen has already written an excellent post on ColdBox 7's new Delegates functionality, I will not be covering delegates here.

Lazy Properties

Lazy Properties, contrary to popular belief, are not "conducive to inactivity or indolence", but are wirebox properties that are configured to only inject or "build" the object once called. Since not every request may use the property, the app benefits from a speed gain as it does not need to waste time constructing dependencies that it does not need.

Let's look at an example. Here we have a ShoppingCart object which can store and check out cart items. This cart uses a PaymentProcessor to purchase the items when purchase() is called:

// ShoppingCart.cfc
component{
    property name="paymentProcessor";

    public component function init(){
        variables.paymentProcessor = new models.PaymentProcessor();
        return this;
    }

    public function purchase(){
        paymentProcessor.makePayment( getTotalCartPrice() )
    }

    // ...
}

This is all fine and good, but if the PaymentProcessor builds a payment connection or authentication upon creation, it is a fairly time-consuming object to create. This is egregious when we consider how many times this ShoppingCart will be rendered before the payment is made... if it is made at all.

To avoid the time cost of building the PaymentProcessor, then, we can use lazy properties to only build once the getter is called.

First, we need to add the lazy annotation to the property to inform Wirebox that we will be using a lazy constructor:

 property name="paymentProcessor" lazy;

Next, we need to move creation of the PaymentProcessor instance outside of the init() method and into a buildPaymentProcessor() method:

// painfully construct the payment processor
 private function buildPaymentProcessor(){
     return new models.PaymentProcessor();
 }

Note the method name is buildPaymentProcessor(), which matches the default build{propertyName} method name that Wirebox looks for when calling the builder method. If we wish to use a custom method name, we could set that in the property lazy annotation:

property name="paymentProcessor" lazy="setupPaymentProcessor";

private function setupPaymentProcessor(){
     return new models.PaymentProcessor();
 }

Finally, our direct reference to the property name (i.e. variables.paymentProcessor) needs to be replaced with a getter method call in order for Wirebox to call the property constructor:

 public function purchase(){
     getPaymentProcessor().makePayment( getTotalCartPrice() )
 }

Putting it all together, our minimal ShoppingCart component looks something like this:

// ShoppingCart.cfc
component{
    property name="paymentProcessor" lazy="setupPaymentProcessor";

    public component function init(){
        return this;
    }

    public function purchase(){
        getPaymentProcessor().makePayment( getTotalCartPrice() )
    }

    private function setupPaymentProcessor(){
        return new models.PaymentProcessor();
    }
}

By using ColdBox 7's lazy property syntax, usage of the ShoppingCart object does not need to wait for construction or validation of the PaymentProcessor object. This will make cart rendering or access quicker, while still keeping purchase logic in the cart object.

For more information on lazy properties, I highly recommend the Wirebox Lazy Properties reference documentation.

Property Observers

Property Observers, quite simply, are a means to observe value changes upon a component property.

Let's take our ShoppingCart example, and add a totalPrice property:

// ShoppingCart.cfc
component{
    property name="totalPrice" type="numeric";

    public component function init(){
        variables.totalPrice = 0;
        return this;
    }
}

With this syntax, developers can call cart.getTotalPrice() to see the total cost of all cart items.

But say we wish to log each time the total price changes.

We could do this within the addItem() method. However, the Separation of Concerns principle recommends we avoid co-locating one concern (debugging and logging) with another concern (the business logic of adding items to a cart). Thus, we can use property observers to note and log changes from a separate method, without cluttering our cart addition functionality.

First, we add the observed annotation to the totalPrice property declaration:

property name="totalPrice" observed;

This will instruct Wirebox to call totalPriceObserver() each time setTotalPrice() is called. (This method name follows the {propertyName}Observer syntax, i.e. totalPrice + Observer = totalPriceObserver().) So now we add our observer method:

function totalPriceObserver( newValue, oldValue, property ){
    log.info( "Cart price changed to #newValue#" );
}

We can now log each time the cart price changes! The real power, though, comes from a custom observer used for observing multiple properties simultaneously:

// ShoppingCart.cfc
component{
    property name="totalPrice" type="numeric" observed="cartObserver";
    property name="subtotal" type="numeric" observed="cartObserver";
    property name="user" type="struct" observed="cartObserver";

    public component function init(){
        variables.totalPrice = 0;
        variables.subtotal = 0;
        return this;
    }

    function cartObserver( newValue, oldValue, property ){
        log.info( "Cart #property# changed:", newValue );
    }
    // ...
}

Our cartObserver can now be used to log or track multiple types of cart changes to help drive better business decisions by knowing what the customers are spending on, or which items just sit in the cart, unpurchased. And that's money. 🤑

For more information on property observers, I highly recommend the excellent Wirebox documentation covering property observers.

Add Your Comment

Recent Entries

Ortus June 2024 Newsletter!

Ortus June 2024 Newsletter!

Welcome to the latest edition of the Ortus Newsletter! This month, we're excited to bring you highlights from our sessions at CFCamp and Open South Code, as well as a sneak peek into our upcoming events. Discover the latest developments in BoxLang, our dynamic new JVM language, and catch up on all the insightful presentations by our expert team. Let's dive in!

Maria Jose Herrera
Maria Jose Herrera
June 28, 2024
BoxLang June 2024 Newsletter!

BoxLang June 2024 Newsletter!

We're thrilled to bring you the latest updates and exciting developments from the world of BoxLang. This month, we're diving into the newest beta release, introducing a new podcast series, showcasing innovative integrations, and sharing insights from recent events. Whether you're a seasoned developer or just getting started, there's something here for everyone to explore and enjoy.

Maria Jose Herrera
Maria Jose Herrera
June 28, 2024
BoxLang 1.0.0 Beta 3 Launched

BoxLang 1.0.0 Beta 3 Launched

We are thrilled to announce the release of BoxLang 1.0.0-Beta 3! This latest beta version is packed with exciting new features and essential bug fixes, including robust encryption functionality, enhanced Java interoperability, and more efficient event handling. Key highlights include the introduction of query caching capabilities, seamless coercion of Java Single Abstract Method (SAM) interfaces from BoxLang functions, and support for virtual thread executors. So, let’s dive into the details of what’s new in BoxLang 1.0.0-Beta 3 and how you can start leveraging these updates today!

Luis Majano
Luis Majano
June 28, 2024