Like many InRule users, my background is in software development. As a result, I often find myself taking whatever means I can to bypass low-code tools:
- When I need to make changes to a WordPress website, I inevitably become annoyed at the rich text editor and end up making changes directly to the HTML
- When building WPF controls, I quickly become disillusioned by the layout designer and end up writing the XAML manually
- I didn’t like the Dynamics interface for entering time, so I wrote my own
And so, when I first opened up irAuthor, my immediate reaction was to try and do everything using User Defined Functions (UDFs). This is the reaction that most software developers have when they first encounter irAuthor, and has been the cause of innumerable major rule application refactors when customers find themselves with an unmaintainable rule application.
“UDFs are like cats. They look all adorable and friendly, then you pet them the wrong way, and they scratch you and relieve themselves in your shoes, and you wonder… why did I bring this into my life again?”
-Dan Gardiner (clearly a dog person)
Allow me to explain.
The Origin of the UDF
As with any low-code tool that encapsulates complex logic and syntax behind easy-to-use interfaces, there’s no way for us to implement a UI that covers every possible scenario – that’s why nearly every tool out there includes some failsafe that allows you to execute code.
Within InRule, that fallback option comes in the form of UDFs, which allow you to write code that can be executed directly during rule execution. While nearly all functionality can be implemented using nothing other than Rules, Vocab, and other logic elements – you always have a Plan B (or, more accurately, Plan E) to get you out of a bind.
Unfortunately, developers (myself included) tend to look at the most code-like aspect of a tool and use that as their proverbial hammer for every roughly-nail-shaped-problem that comes along, even if it’s actually a bolt (or a screw, or a cocktail stirrer, or a chicken wing…).
So you’re saying that I shouldn’t use them for everything?
One of our senior engineers recently told me that, with a few exceptions, any time they see UDFs being used, it’s either an indicator that there is functionality missing from the product, or the Rule Application needs to be re-architected. InRule is designed as a decision engine that can be maintained by less technical folks; using UDFs where you could be using native functionality largely defeats the purpose of using a low-code tool like InRule.
In addition, there are a variety of other downsides that come with using UDFs:
- Maintainability: When your logic is written in rules, it’s simple for any rule author to see what’s happening. When logic is delegated to UDFs, only your technical resources will be able to have the context of what’s happening.
- Debugging: UDFs are “Late-Bound,” evaluated code – not compiled code. As a result, if something goes wrong (whether you have a typo, invalid logic, or accidentally wrote a limerick instead of code), you won’t know until the UDF gets executed at runtime (hopefully not in production). In addition, since a UDF can be called from anywhere in the Rule App, it’s possible that a UDF could run successfully in one location, and fail from another (for example, if it references a field by name that only exists in one of the contexts).
- Troubleshooting: When a UDF runs, the only information available to the execution log is that the UDF was executed – there is no way of looking at the log and seeing what happened within the UDF’s execution (other than a single, often vague, error message). As a result, when something goes wrong, it can be challenging to figure out the scenario that caused undesirable behavior and to fix it.
- Performance: The InRule Decision Engine is designed to be as fast as possible, maintaining full awareness of dependency networks and optimizations within the Entity schema. However, since you can do anything within a UDF, by default, the engine will have to re-scan the dependency graph to see if anything changed outside what it was aware of – and this process can cause some performance issues. This can be mitigated by changing some settings that will be mentioned later, but is something to be aware of.
What’s the purpose of UDFs, then?
OK, I’ve sold you that UDFs shouldn’t be used for everything – now you’re probably wondering in what situations they should be used. In short, UDFs are appropriate to use when the logic or functionality you’re implementing cannot be created using Rules, Vocab, or other native InRule constructs.
- If you’re calling a static method in a bound assembly that requires extensive context information about the current Rule Application and Entity (like the Salesforce and Dynamics RuleHelper assemblies)
- If you need to implement recursive logic (and a loop or Member Ruleset won’t work for the scenario)
- If you need to address Rule Application elements dynamically (using text-based identifiers for fields, collections, data elements, etc.)
- If you need to interrogate the schema of the Rule Application (like identifying the name of the current RuleSet or Entity dynamically)
- If you need to perform extensive or explicit text manipulation or parsing (and are unable to combine native Vocabulary elements to implement the logic)
This list is by no means comprehensive, but it should give you a perspective on the level of requirements where a UDF might start coming into play. There is absolutely a place for UDFs in Rule Applications – it just shouldn’t be the first thing that’s attempted.
How can I make sure the UDFs I implement are written properly?
You’ve found yourself in a situation where a UDF is, in fact, the appropriate tool for the job. The important thing for you to do now is to ensure that your UDF is written in a way that it is maintainable and reliable. To that end, there are a couple core tenets to keep in mind.
UDFs should be…
- Single-purposed: Do one thing, and do it well; as soon as you introduce branches of behavior, it stops being maintainable and testable. This should also allow the UDF to be short – which aids in it being able to be debugged.
- Well-Named: It should be obvious to the executor (who only sees the UDF name) what the function does. “TestMethod” is fine for development, but it should never see the light of another computer.
- Reusable: One of the root items you’re able to access from a UDF is – wait for it – other UDFs in the same library! As a result, your single-purpose UDFs can be called by other UDFs to begin to compound the functionality into more complex logic structures.
- Internally Well Named: When you go back to the UDF in 6 months, a variable named “var1” is going to tell you very little about what it is or how it’s used. Even if it makes perfect sense right now, I promise you that it won’t in a few days. It’s well worth spending the extra time to avoid having to re-write the whole function in 6 months when you (or a team member) can’t decipher what it’s doing.
- Understandable more than Clever: Similar to well-named variables, implementing logic in a “clever” way is simply a euphemism for implementing logic in a way that is nearly impossible to debug or update.
- Well Commented: You’ve written something with well-named variables and understandable logic – as a final way to be nice to your future self, make sure that the function is commented, both describing what it does (for any non-technical Rule Authors trying to look and see what the method they’re calling should be doing) as well as describing any bits inside that may be remotely confusing to a future developer. In short, as with any piece of code, use sound development practices throughout UDFs, and you’ll be a happier person down the road.
- Properly configured with State Refresh Options: This is the one that can have a major performance impact if your UDF is called frequently during rule application execution. The only time that “Enable refresh of all bound objects” should be left enabled in a UDF is if your entity schema is bound to a .NET assembly, and the UDF makes a call to a static method in the bound assembly that mutates the state of the bound object.
If those principles sound suspiciously like fundamentals of well-written code… you’re right! Since a UDF is simply code that lives within a Rule Application, any code standards applied to an application can also apply to a UDF – some even more so given the difficulty of testing and debugging a UDF.
If you’re looking to get started, I would highly recommend taking a look at the UDF Examples that we have available, which can be used as a starting point for building out the functionality you’re looking to implement.
Good Luck, and happy UDF-writing (in the correct scenarios, of course)!