Our mission for this stream was to facilitate multi-package development when there are triggers in play. For as long as I can remember, the trigger rule has always been: one trigger per object. This makes multiple packages difficult because the triggers and their associated logic often cross-depend on one another. Finding a way to decouple triggers and trigger logic from one another is a bit of a sticky wicket.
The diagram above shows what we’re building. The orange rectangle represents a package containing everything related to actual .trigger files, along with the trigger framework and a custom trigger handler class:
CustomMDTTriggerHandler.cls. With this trigger package installed, any other package we create can include both the custom trigger handlers we want executed, as well as custom metadata records registering those trigger handlers.
To meet our goals of maintaining the single trigger per object while decoupling triggers to enable packaging, we had to find the trigger logic we wanted to run at runtime, regardless of what package it may be in. To accomplish this, we used a combination of three technologies: a trigger framework, custom metadata, and meta-programming.
To save time on the stream, we started with a trigger framework by Kevin O’Hara that both Simon and I have used in the past. You can read more about it here: Trigger Framework. The key to this framework is the virtual class it provides. Because it’s virtual, extending classes can override when necessary and inherit when no override is present. The provided virtual class defines a default
.run() method. This allows your actual triggers to have nothing more than a single line of logic:
Custom metadata types are a Lightning Platform feature. Not only do custom metadata types allow you to create new types of data, you can package and deploy that data as well. In a multi-package org, this means you could easily create one package defining the custom metadata type, and other packages as necessary could include deployable records. Our solution uses this technology to define a custom metadata type describing trigger logic. We then created records of that type to describe specific classes we wanted executed and in what order. Having a custom metadata record means our trigger can query and get a list of classes to execute. However, the query is, at best, able to return strings of class names to us. Converting those class names into actual instantiated objects that we can manipulate is where meta-programming comes in.
Apex has a fairly robust Type class. One of its charms is the ability to get a Type object for a given string. With this, we can convert the string ‘SomeType’ into a Type object. It’s important to note that this is possible, not only with sObjects like Account and Custom_Object__C but also Apex class types. Once we have a Type object, we can call the
newInstance() method to return a new object of that type. Using these two Type methods together allows us to construct a new object dynamically, based on the string representation of the class’s name we pulled from our custom metadata records.
Here’s what that Apex looks like, fully formed.
TriggerHandler handler = (TriggerHandler)Type.forName(trygger.Class_Name__c).newInstance();
Note: we’re having to cast the resulting object to
TriggerHandler. The trigger framework we started with requires our specific trigger handlers to extend the provided
TriggerHandler class. Because our individual trigger handler classes all extend this class, we can safely cast any object we dynamically create to that Type. While this may seem like a limitation, it gives us access to all of the methods defined in
TriggerHandler including things like beforeInsert(), afterUpdate() etc.
These three bits, when combined, allow us to define a generic
CustomMDTTriggerHandler class that all triggers can call. That generic TriggerMDTHandler class is then responsible for:
- Determining the sObject DML has been performed on.
- Querying for custom metadata related to the sObject for individual trigger handlers to execute.
- Meta-programming those class names into actual objects that are executed.
Using this pattern we can ship one package (trigger package) with the trigger framework, CustomMDTTriggerHandler, custom metadata type, as well as a single trigger per object — without depending on any other code. Other packages (domain packages) in the org can ship specific trigger handler classes and custom metadata type records. These domain packages will maintain a dependency on the trigger package, but this is most likely a dependency that won’t cause you heartache (i.e. updating one would likely require no change in other packages).
We’ll be doing more Live Coding streams here in the near future. Keep a watch on this page to register for our next one! In the meantime, if you’ve got an idea for something you’d like to see us live code, reach out to me at @codefriar on Twitter with your suggestions and requests. Like what you’ve seen here? Check out the code base! We’ve also set up a trailmix with more information on custom metadata, triggers, and types (oh my!)