WireBox Delegators
Object Composition Elevated!
Last updated
Object Composition Elevated!
Last updated
WireBox supports the concept of object delegation in a simple, expressive DSL.
Object delegation, also known as delegation design pattern, is a technique in object-oriented programming where one object delegates certain responsibilities to another object. Instead of inheriting behavior from a superclass (class inheritance), an object obtains behavior by delegating tasks to another object.
WireBox provides a set of rules for method lookup and dispatching, allowing you to provide delegation easily in your CFML applications.
This pattern is also known as "proxy chains". Several other design patterns use delegation - the State, Strategy and Visitor Patterns depend on it.
Let's discover some benefits of Delegators:
Code Reusability: Delegation promotes code reuse by allowing you to compose and reuse existing objects, enhancing modular design. You can create specialized delegate objects and reuse them in various contexts, avoiding the limitations of single inheritance.
Separation of Concerns: Delegation helps separate concerns and maintain a clear separation of responsibilities between objects. Each object focuses on its core functionality, and the delegated object handles specific tasks or services.
Flexibility and Extensibility: Delegation allows you to change behavior at runtime by switching delegate objects. You can introduce or modify new behavior without altering the main object's structure or functionality.
Reduced Coupling: Delegation reduces tight coupling between objects, leading to a more maintainable and loosely coupled codebase. Changes to the delegate object's implementation don't affect the main object, and vice versa.
Better Composition: Delegation supports a more flexible and fine-grained approach to combining behavior rather than class inheritance. You can compose objects with various delegate objects to achieve different combinations of features.
Single Responsibility Principle (SRP): Delegation aligns with the SRP, as each object focuses on a single responsibility. This promotes cleaner, more maintainable, and testable code.
Encapsulation and Information Hiding: Delegation allows encapsulation of internal behavior within the delegate object. The main object can provide a simplified interface, hiding complex implementation details.
Dynamic Behavior: Delegation facilitates dynamic behavior changes during runtime, making it suitable for scenarios where behavior needs to change based on different conditions or contexts.
Easier Testing: Delegation can lead to more focused and isolated unit tests. You can test delegate objects independently and mock them when testing the main object.
Avoiding Fragile Base Class Problem: Delegation helps avoid issues related to the fragile base class problem, which can occur when modifying superclass behavior affects subclasses.
It's important to note that while delegation offers these benefits, it might introduce some additional complexity to the codebase, especially when managing interactions between objects. Careful design and consideration are necessary to leverage delegation effectively.
In object-oriented programming, there are three ways for classes to work together:
Inheritance (IS A relationship)
Composition (HAS A relationships)
Runtime Mixins or Traits
With inheritance, you create families of objects or hierarchies where a parent component shares functions, properties, and instance data with any component that extends it (derived class). It’s a great way to provide reuse but also one of the most widely abused approaches to reuse. It’s easy and powerful but with much power comes great responsibility. For example, you create a base Animal
class and then derived classes like: Cat
, Dog
, Bird
, etc. You can say: A Cat
IS An Animal
, A Dog
IS An Animal
. You wouldn’t say, a Cat
has an Animal
.
With composition a component will either create or have dependencies injected into them (via WireBox), which it can then use these objects to delegate work to them. This follows the has a
relationship, like A Car has Wheels, a Computer has Memory, etc. The major premise of WireBox is to assist with composition.
Mixins allow you to do runtime injections of user-defined functions (UDFs) and helpers from reusable objects. However, this can lead to method explosion on injected classes, collisions, and not a lot of great organization as you are just packing a class with tons of functions to reuse behavior. Composition is the preferred approach and the less decoupled approach. Delegation is a step further. So let’s explore it with a simple example.
A Computer
is made from many parts, and each part does one thing really well. If you want to render something on the screen, the computer tells the graphics card and lets the card do its job. It tells it what to do but not HOW to do it. That’s composition. You wouldn’t want a computer to extend from a graphics card, or memory or a disk, right? You use them in orchestration. But what if you wanted to give the public access to the memory or graphics card? Or maybe log all calls to the memory module? You would have to build some bridge right, a proxy, a way to access those methods and delegate. So let’s explore how to do this manually:
Memory
Computer
As you can see, we pass along the requests to the Memory
object, and we satisfy our requirements. We can decorate these proxy methods if we want to as well to add behavior. However, imagine if you had many methods or many compositions and needed to do this for all those methods. As you can see, it can get very very tedious writing simple delegation methods. Here is where WireBox can assist.
💡 WireBox Delegates are similar to object delegates in Kotlin, Groovy and Ruby, and Traits in PHP.
You can annotate an injection with the delegate
annotation and WireBox will inspect the delegate object for all its public functions and create those functions at runtime for you in the target. This way, you don’t have to write all those delegation functions. Let’s look at our Computer
again:f
As you can see, the computer will magically have those memory
delegation methods of read() and write()
and will forward correctly to the memory
module. This is great if you are using a full injection approach; not only do you get delegation but also a reference to the memory module via variables.memory
.
Delegates
AnnotationHowever, we also have a shorthand annotation that you can use if you really don’t care about the injection but just about the delegations to happen. This is done by annotating your component with a delegates
annotation.
This annotation can be one or more delegates, and you can use either a WireBox ID or a full classpath:
IMPORTANT Please note that if you define multiple delegates and they have the same method names, the first defined delegate in the list will win unless you define specific prefixes or suffixes to distinguish the injections.
If you need to prefix your delegate methods, then you can use the delegatePrefix
annotation on your property injections. If you don’t give it a value, we will use the property's name as the prefix, or you can give it a value and be very explicit. Every method injected from the delegate will have that prefix.
This is great as you can be more expressive with the way those methods are delegated to.
But what about the simple delegate shortcuts approach? Does it support this? Yes, following the following pattern:
This will allow you to add specific prefixes to distinguish the injections.
The Memory
object’s methods will be prefixed with ram: ramRead(), ramWrite()
You can also leave the prefix EMPTY, and we will use the object's name as the prefix.
Since the >Memory
is defined, then we will use Memory
as the prefix.
If you need to suffix your delegate methods, then you can use the delegateSuffix
Annotation. If you don’t give it a value, we will use the property's name as the suffix or you can give it a value and be very explicit. Every method injected from the delegate will have that suffix.
But what about the simple delegate shortcuts approach? Does it support suffixes? Yes, following the following pattern:
This will allow you to add specific suffixes to distinguish the injections.
The Memory
object’s methods will be suffixed with ram: readRam(), writeRam()
You can also leave the suffix EMPTY and we will use the name of the object as the suffix.
Since the >Memory
is defined, then we will use Memory
as the suffix.
You can declare multiple delegates with no problem at all. All discovered public methods would be injected and delegated. However, if there is a case where each delegate has the same method name WireBox will throw a DelegateMethodDuplicateException
. To avoid this side-effect, you will have to use suffixes or prefixes to remove the ambiguity.
To avoid conflicts, we recommend you use the suffixes and prefixes so the delegate methods are more expressive:
If you want to delegate to only a few methods and not all public methods of an object, then you can use the delegate
annotation and pass in a list of those methods you can to include in the delegation.
The simple shorthand approach can also leverage targeted methods by adding the following pattern to the definition:
You basically add the name of the methods by using a =method
pattern.
Every delegate, once it’s used on a target, will get a $parent
injection available to them. This will allow them to interact with the parent that called for the delegation.
Important: Please note that if your Delegate is a singleton, this can cause issues as it can be potentially injected into many parents. Therefore, we suggest that if you create Delegates that use the
$parent
approach, they remain as transients.
You can also explicitly set the delegates
shorthand expression for a component via the binder’s delegates()
method.
You can also use the property
binder method as well to explicitly define the delegate annotations of any injection property:
Now that we have seen what delegators are, WireBox offers core delegators to your application via the @coreDelegates
namespace
Async - This delegate is useful to interact with the AsyncManager and is the most used functionality for asynchronous programming.
DateTime - Leverage the date time helper
Env - Talk to environment variables
Flow - Several fluent flow methods
JsonUtil - JSON utilities
StringUtil - String utilities
Population - Population utilities
So let's say you have a service that needs to populate objects and work with the system environment: