If you are unfamiliar with Design Patterns, please see Design Pattern: What is a Design Pattern and How Do I Use Them.
Problem Statement
Rules developers will frequently encounter situations where they have to perform only slightly different tasks depending on the states of two or more Items or variables. The most obvious approach is to have a series of nested if/else statements or switch statements and repeat the same code over and over with the slight variations in each section.
This approach results in overly complicated Rules with lots of indents and lots of duplicated code.
Concept
One way to avoid the extra complexity and duplicated code is to use the 1-2-3 Rule structure:
- Check to see if the Rule has to run at all, if not exit.
- Calculate what needs to be done.
- Do it. Only do actions in like sendCommand in one place at the end of the Rule.
Often there will be conditions in which the Rule doesn’t need to do anything. Rather than go through a bunch of if conditions and such, just check up front and exit. This alone will often reduce the indents in your Rule by one.
Next, you will likely have a bunch of conditionals. Rather than calling sendCommand or publish or such inside the conditionals save a variable for use later.
Finally, once you have calculated what to do, you have a single place at the end of the Rule where you call the actions or do other things that causes side effects.
Example
Let’s start with a contrived example. We have Four Switch Items: Foo, Bar, Baz, and Buzz and each Item can have three values ON, OFF, or NULL.
If all of them are OFF we sendCommand(OFF) to the Buzz Item.
If one of them is ON we sendCommand(OFF) to the Buzz Item
If two of them are ON we sendCommand(ON) to the Buzz Item.
If three of them are ON we sendCommand(ON) to the Buzz Item.
If any one of the first three is NULL then we send an alert.
NOTE: This particular scenario would be best implemented using a Group:Number:SUM, but I want an example to use that is easy to understand for illustrative purposes.
Naieve Implementation
A naieve coder might use a bunch of complicated if statements:
rule "Naieve implementation"
when
Item Foo changed or
Item Bar changed or
Item Baz changed
then
if(Foo.state == NULL || Bar.state == NULL || Baz.state == NULL) {
logWarn("Test", "One of the Items is NULL: " + Foo.state + ", " + Bar.state + ", " + Baz.state)
// send alert using you preferred service
}
else {
if(Foo.state == OFF && Bar.state == OFF && Baz.state == OFF) {
logInfo("Test", "Sending Buzz OFF, all Items are OFF")
Buzz.sendCommand(OFF)
}
else if(Foo.state == ON && Bar.state == OFF && Baz.state == OFF) {
logInfo("Test", "Sending Buzz OFF, only Foo is ON")
Buzz.sendCommand(OFF)
}
else if(Foo.state == OFF && Bar.state == ON && Baz.state == ON) {
logInfo("Test", "Sending Buzz OFF, only Bar is ON")
Buzz.sendCommand(OFF)
}
else if(Foo.state == OFF && Bar.state == OFF && Baz.state == ON) {
logInfo("Test", "Sending Buzz OFF, only Baz is ON")
Buzz.sendCommand(OFF)
}
else if(Foo.state == ON && Bar.state == ON && Baz.state == OFF) {
logInfo("Test", "Sending Buzz ON, Foo and Bar are ON")
Buzz.sendCommand(ON)
}
else if(Foo.state == ON && Bar.state == OFF && Bax.state == ON) {
logInfo("Test", "Sending Buzz ON, Foo and Baz are ON")
Buzz.sendCommand(ON)
}
else if(Foo.state == OFF && Bar.state == OFF && Bax.state == OFF) {
logInfo("Test", "Sending Buzz ON, Bar and Baz are ON")
Buzz.sendCommand(ON)
}
else if(Foo.state == ON && Bar.state == ON && Bax.state == ON) {
logInfo("Test", "Sending Buzz ON, Foo, Bar and Baz are ON")
Buzz.sendCommand(ON)
}
}
end
45 lines of code, two levels of indentation.
Observations:
- The if statements clauses are actually pretty complex. They appear relatively simple mainly as a side effect of the simplicity of the example rule. But imagne if you had three clauses and one was a String that can have six potentail values and the other is a Number and you need to test that it is inside a given range.
- For something like this two levels of indentation feels like too many but it isn’t a huge problem.
- The same two lines of code that causes actions repeat over and over again. Imagine what would be required if you needed to add an alert before sending ON or OFF to Buzz. You would need to add eight more lines of code.
Are Switches Better?
A more experienced developer may try to use a switch statement and come up with something like this:
rule "Let's Use Switches"
when
Item Foo changed or
Item Bar changed or
Item Baz changed
then
switch Foo.state {
case NULL: {
logWarn("Test", "One of the Items is NULL: " + Foo.state + ", " + Bar.state + ", " + Baz.state)
// send alert using you preferred service
}
case ON: {
switch Bar.state {
case NULL: {
logWarn("Test", "One of the Items is NULL: " + Foo.state + ", " + Bar.state + ", " + Baz.state)
// send alert using you preferred service
}
case ON: {
switch Baz.state {
case NULL: {
logWarn("Test", "One of the Items is NULL: " + Foo.state + ", " + Bar.state + ", " + Baz.state)
// send alert using you preferred service
}
case ON: {
logInfo("Test", "Sending Buzz ON, Foo, Bar, and Baz are ON")
Buzz.sendCommand(ON)
}
case OFF: {
logInfo("Test", "Sending Buzz ON, Foo and Bar are ON")
Buzz.sendCommand(ON)
}
}
}
case OFF: {
switch Baz.state {
case NULL: {
logWarn("Test", "One of the Items is NULL: " + Foo.state + ", " + Bar.state + ", " + Baz.state)
// send alert using you preferred service
}
case ON: {
logInfo("Test", "Sending Buzz ON, Foo and Baz are ON")
Buzz.sendCommand(ON)
}
case OFF: {
logInfo("Test", "Sending Buzz OFF, only Foo is ON")
Buzz.sendCommand(OFF)
}
}
}
}
}
case OFF: {
switch Bar.state {
case NULL: {
logWarn("Test", "One of the Items is NULL: " + Foo.state + ", " + Bar.state + ", " + Baz.state)
// send alert using you preferred service
}
case ON: {
switch Baz.state {
case NULL: {
logWarn("Test", "One of the Items is NULL: " + Foo.state + ", " + Bar.state + ", " + Baz.state)
// send alert using you preferred service
}
case ON: {
logInfo("Test", "Sending Buzz ON, Bar, and Baz are ON")
Buzz.sendCommand(ON)
}
case OFF: {
logInfo("Test", "Sending Buzz ON, Bar and Baz are ON")
Buzz.sendCommand(ON)
}
}
}
case OFF: {
switch Baz.state {
case NULL: {
logWarn("Test", "One of the Items is NULL: " + Foo.state + ", " + Bar.state + ", " + Baz.state)
// send alert using you preferred service
}
case ON: {
logInfo("Test", "Sending Buzz OFF, only Baz is ON")
Buzz.sendCommand(OFF)
}
case OFF: {
logInfo("Test", "Sending Buzz OFF, all Items are OFF")
Buzz.sendCommand(OFF)
}
}
}
}
}
}
end
94 lines of code and 6 lines of indentation.
Observations:
- The individual clauses are less complex, but the conditions are now spread out over the entire Rule. For example, to figure out what to do in the very last embedded case you have to look at lines 85, 75, and 53. That many lines rarely fits on the screen at the same time. That makes them even more difficult to read and understand.
- Six levels of indentation is way way too many, particularly for something like this
- The same two lines of code that causes actions repeat over and over again even more so than before as the NULL cases now occur in six places rather than just one.
- It is more than twice as many lines of code.
No, switches are much worse in this case.
1-2-3 Rule Structure
rule "1-2-3 Rule Structure"
when
Item Foo changed or
Item Bar changed or
Item Baz changed
then
// 1. Check to see if the Rule has to run at all, if not exit.
if(Foo.state == NULL || Bar.state == NULL || Baz.state == NULL){
logWarn("Test", "One of the Items is NULL: " + Foo.state + ", " + Bar.state + ", " + Baz.state)
// send alert using you preferred service
return;
}
// 2. Calculate what needs to be done.
var newState = ON // default to sending the ON command
// If two or more of the Items are OFF send OFF
if((Foo.state == OFF && Bar.state == OFF) ||
(Foo.state == OFF && Baz.state == OFF) ||
(Bar.state == OFF && Baz.state == OFF)) newState == OFF
// 3. Do it. Only do actions in like sendCommand in one place at the end of the Rule.
logInfo("Test", "Foo = " + Foo.state.toString + " Bar = " + Bar.state.toString +
" Baz = " + Baz.state.toString + ". Sending " + newState.toString + " to Buzz")
Buzz.sendCommand(newState)
end
26 lines of code, 1 level of indentation.
Observations:
- The actions take place in only to place in the Rule, up at the start when one of the Items is NULL and at the very end.
- The if statement in section 2 is admitedely as complex as the naieve approach, but there is only one if statement in this case and fewer cases because we only need to check to see if our default value needs to be overridden, eliminating more than half of the cases that need to be checked.
- The log statement and sendCommand only occurs in one place at the end of the Rule. If one needed to perform more calculations before sending the command or wanted to send and alert or do some other action, now it only has to be performed in one place in this Rule instead of eight.
- There are almost half as many lines of code, though this DP does not guarantee shorter Rules in all cases.
- I had to make the log statement a little more generic but it gets across the same information as the other approaches.
Advantages and Disadvantages
Advantages:
- It usually requires less code to implement.
- It usually requires fewer indentations reducing the likelihood of missmatched closing brackets.
- You have less to explicetly check for because you can use default values and then only check to see if the default values need to be changed.
- The resultant code is often much easier to maintain because side effects are centralized and the code overall is shorter.
Disadvantages:
- Some may find the flow a little less intuitive because a lot of things are not explicetly in the code. For example, in the above, we have no code to check if all three are ON because we assumed that the newState will be ON.
- Calculating what to do in step 2 may require a number of variables to store the state(s) to send or booleans to change certain behavior (e.g. one variable to store the new state and a boolen to indicate if it should be sent twice). You should rarely need more than three.
Related Design Patterns
None-identified yet.
Addendum
As mentioned above, an even better way to implement the above example would be to use a Group. In the interest of completeness, here is the Group approach.
Group:Number:SUM MyGroup
Switch Foo (MyGroup)
Switch Bar (MyGroup)
Switch Baz (MyGroup)
rule "Group Approach"
when
Member of MyGroup changed
then
if(MyGroup.members.filter[ i | i.state == NULL ].size != 0) {
logWarn("Test", "One of the Items is NULL: " + Foo.state + ", " + Bar.state + ", " + Baz.state)
// send alert using you preferred service
return;
}
val newState = if(MyGroup.state >= 2) ON else OFF
logInfo("Test", "Foo = " + Foo.state.toString + " Bar = " + Bar.state.toString +
" Baz = " + Baz.state.toString + ". Sending " + newState.toString + " to Buzz")
Buzz.sendCommand(newState)
end
Notice how even with the Group approach we apply the same 1-2-3 Rules strucuture. The big change is the if conditionals are simpler.
Special note: newState is set using the trinary operator. It is a kind of one line if statement which you will find very useful when using this DP. The above line is the equivalent to:
var newState = OFF
if(MyGroup.state >= 2) newState = ON