Design Pattern: DRY, How Not to Repeat Yourself in Rules DSL

Tags: #<Tag:0x00007f74530887d8>

Please see Design Pattern: What is a Design Pattern and How Do I Use Them for details on what a DP is and how to use it.

Problem Statement

Do Not Repeat Yourself (DRY)

Often an inexperienced programmer will find that a naive and straight forward approach to writing Rules DSL Rules results in a lot of duplicated code. Often experienced programmers find that the usual tools to avoid duplicated code like data structures and classes are not available in Rules DSL and they end up fighting the language trying to shoehorn something that works like these into the language, often unsuccessfully.

Concept

This Design Pattern presents a number of tips and techniques for ways to avoid duplicated code in Rules DSL using those features and techniques that work best. These are not intended to be used in isolation. Instead they are intended to use in combination.

Iterate

Before going into the approaches, I want to write a little bit about the approach one can take to improving their Rules. Rules never emerge fully formed and armored like Athena from Zeus’ head. Usually the first draft of a Rule will be excessively long, vary complex, and hide logical errors. So how do we get from an ugly mess to something elegant by applying the approaches below and other techniques?

Iterate over the Rule. You will likely already be unhappy with the Rule in the first place which is step one. Look for little improvements you can make. Is there duplicated or nearly duplicated code? Are their lines that are really long or have lots of conditions and clauses? Just address those to start. Then do it again. Repeat looking for and making little changes (or big changes) and modifying the Rule until you are happy.

A great illustration of this iteration can be seen in this thread. The OP started with a Rule that was broken that was 9 lines of code but the Rule was incomplete. Then a 17 line version is posted which grows to 28 lines of code (with some errors). Then, gradually, post by post the users of that thread rework the Rule until it shrinks down to a mere 7 lines of code.

As you gain experience with the language, your first draft of a Rule may more closely resemble some of the shorter Rules at the end of the thread, but even experienced programmers need to iterate over their code more than once to refine it to a point where it is concise, easy to understand, and clear.

Approach 1: Encapsulation

Often one will have a collection of values and variables that are all associated with one another. For example, one might have a door sensor and have an associated DateTime to keep track of the last time the door was opened, a Timer to generate an alert if the door remains open for too long, and an integrated deadbolt.

In a more traditional Object Oriented programming language one might make a class to keep all of these values and the Rules and methods that operate on those methods together. We do not have classes in OH so instead we use Design Pattern: Associated Items. See this DP for full details but at a high level:

  • store all of these values in Items
  • name the Items so it is easy to construct the name of all the associated Items given the name of any one of them
  • put all the Items into a Group
  • use Member of Rule triggers and the triggeringItem implicit variable to write one Rule that processes events for all of the sets of Items (i.e. for a door, there is one Rule to process events for ALL doors)

Groups do a good job of encapsulating values in other ways as well. See Design Pattern: Working with Groups in Rules for all the operations one can perform on a Group.

Approach 2: Reusable Functions

Sometimes there is a need for cross cutting code. This is code that gets used over and over again in multiple Rules and which are not specific to any one specific functionality of the home automation (e.g. lighting). Examples include alerting and time calculations.

In a more traditional language one might create a library of classes or methods to call from Rules. The Rules DSL does not support custom libraries so instead we have two approaches to creating reusable functions.

Separation of Behaviors

Design Pattern: Separation of Behaviors implements these reusable functions as Rules and uses Design Pattern: Proxy Item to trigger the Rules. See the DP for full details but at a high level:

  • create a proxy Item that you sendCommand to from your various Rules to “call” the method; a String Item makes a good choice as the String will be the argument to the method
  • create a Rule that triggers when the proxy Item receives a command and does the action, using the receivedCommand implicit variable as the argument

This works great for methods that need to do something on demand. There is another subclass of this approach for those cases where the duplicated cross cutting code is checking state. Most often this is testing something like checking to see if it is currently between two times of day. In this case, put those time calculations in one Rule and store the result of that calculation in an Item. This is documented in Design Pattern: Time Of Day. See the DP for the full details but at a high level:

  • create an Item to store the result of the state calculation; I recommend using a String so the state’s are more human readable
  • create a Rule that triggers anytime there is an event that might change that state and recalculates the state
  • Rules that need to test against that state tests the Item, Rules that need to be triggered based on the state triggers using that Item

Lambdas

Occasionally there will be a case where there is duplicated code that isn’t cross cutting and cannot be addressed through the other techniques in this DP. In this case one can use Reusable Functions: A simple lambda example with copious notes. See the tutorial for full details but at a high level:

  • create a global val that holds a lambda, which is just a fancy name for a method that can be passed around like a value
  • call the lambda from Rules within the same .rules file.

Lambdas have some severe limitations and should be avoided unless no other approach will work (here is an example where lambdas are needed to avoid duplicate code OAuth2 using just OH Rules and myopenhab.org). Here is a list of just some of the limitations:

  • can only be used by Rules in the same .rules file
  • cannot see other globals, if you have a lambda that needs to call another lambda you must pass the second one as an argument
  • only allows up to six arguments
  • are not threadsafe; if two Rules call the same lambda at the same time they will stomp over each other
  • errors and exceptions are reported differently when they occur within a lambda making it much more difficult to discover why it failed

Approach 3: Shorter Rules

Often a Rules developer, particularly one who is not trained or experienced, will find they are writing long Rules made up a if/else clauses or switch statements that all do largely the same thing but with different values. This can be addressed using Design Pattern: How to Structure a Rule. See the Design Pattern for details but at a high level:

  • use a 1-2-3 Rule structure
  • 1st determine if the Rule needs to run at all and exit if not
  • 2nd calculate what needs to be done
  • 3rd do it.

Any call that causes side effects (e.g. sendCommand, postUpdate, calling an Action, etc) only occurs in section 3 and only appear in the Rule once. Section 2 populates variables that are used in the calls in section 3.

That’s Just a Bunch of Hacks

That is a subjective opinion. These are tried and true ways to write short, concise, and non-duplicative code in Rules DSL. If you can’t or won’t apply these techniques to your code perhaps the Rules DSL isn’t for you. Luckily there are many many alternatives.

Rules Engine Comment
JSR223 This is a separate Rules Engine that allows one to write Rules using Jython, Nashorn JavaScript, or Groovy instead of Rules DSL. Experienced programmers will be more comfortable using one of these languages
Experimental Next Gen Rules Engine (NGRE) Eventually this will replace the Rules DSL as the default. This actually runs on top of the JSR223 Rules Engine for execution and consequently JSR223 and NGRE are compatible (e.g. a library written in JSR223 can be used in NGRE) but instead of writing separate script files, NGRE Rules are created through a GUI in PaperUI. You can find the start of full documentation for NGRE here: Experimental Next-Gen Rules Engine Documentation 1 of : Introduction.
Node-Red 3rd party tool that provides a flows type environment and GUI for developing Rules. There is a node for interaction with OH.
Other 3rd Party Rules Engines Search the forum and github. A number of individuals have taken it upon themselves to write their own Rules engine, usually in Python.

Related Design Pattern

Design Pattern How It’s Used
Design Pattern: Associated Items Approach 1 Encapsulation
Design Pattern: Working with Groups in Rules Approach 1 Encapsulation
Design Pattern: Separation of Behaviors Approach 2 Reusable Functions
Design Pattern: Proxy Item Approach 2 Reusable Functions
Design Pattern: Time Of Day Approach 2 Reusable Functions
Reusable Functions: A simple lambda example with copious notes Approach 2 Reusable Functions, not really a Design Pattern but a Tutorial.
OAuth2 using just OH Rules and myopenhab.org An example where the use of lambdas makes sense
Design Pattern: How to Structure a Rule Approach 3 Shorter Rules
7 Likes
Best way to write repeating stuffs
Grouping items in rules - customer data structures like pojo
[SOLVED] BBC weather RSS Latest Title split to <64 characters for Velbus OLED (AKA Splitting Strings)
[SOLVED] Using groups to shorten code
Global functions for use in rules
Why have my Rules stopped running? Why Thread::sleep is a bad idea
Why have my Rules stopped running? Why Thread::sleep is a bad idea
How can I access generic items in a function?
<solved>Help with rule for heating or humidifier
Handling Hue dimmer switch events
[SOLVED] Rules: only primitive types working in Variable Declaration Section
Using one rule file code for different item setups
Activate a switch/Relay at a specific time
Dynamic Alarmtext
[SOLVED] Regex in rule trigger
Adding custom Java code
[SOLVED] Design Pattern recommendation
Same rule for different rooms
Extended Motion Sensor Rule: any Suggestions?
Looking for some advice on creating rules
Timers in a rule
Conditional statement with a Lambda function variable
My Garadget + MQTT + OpenHAB Setup
Roller shutter with lambda function
Dynamic persistence
How to calculate the time of a specific azimuth
Qubino Smart Meters (ZMNHXD, ZMNHTD) endless problems
[JSR223][Jython]What is the correct way to avoid a race condition getting an item value inside a /received update/ triggered rule?
[JSR223][Jython]What is the correct way to avoid a race condition getting an item value inside a /received update/ triggered rule?
Arguments to createTimer
One item connected to multiple (modbus data) things: Best practice/Design Pattern wanted
Filter sensor values (only values between min and max)

This is an essential concept that should be written in bold, when explaining OOP in openHAB framework.

and this as well.

Well, using Items to store data like this really isn’t OOP. If it were OOP we would be able to create classes with data and methods. Rules DSL doesn’t really let us write our Rules in an OOP way. It gets confusing as all the stuff we use in the Rules are Objects themselves.

This is one of the areas that experienced programmers struggle with mightily when using Rules DSL.

Using Items and Groups to build something like data structures is a bit odd and it is the reason why I wrote that “This is just a bunch of hacks” section. One of the big things that OOP emphasizes is strong encapsulation. Objects present a well defined interface to access and manipulate the data they carry. Rules DSL flies in the face of that and makes all data public and accessible to everyone (i.e. Items).

Not with Rules DSL, true!

But,

still requires bolding. :sunglasses:

But it still has to do with OOP! Your “hacks” are the best yet! :wink: