Design Pattern: How to Structure a Rule

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:

  1. Check to see if the Rule has to run at all, if not exit.
  2. Calculate what needs to be done.
  3. 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
23 Likes

I really like this pattern, but sometimes it is not easy to stick on the 1-2-3 approach. Currently i am developing some irrigation rules. Start is a very simple rule that starts the irrigation pump and sends a reminder after some minutes that the irrigation is still on.

By the way i introduced a Phase 0 that defines variables and constants
Below you will find the rule

//----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
// Start or stop manual irrigation mode. After 15 minutes a pushover message should be send to remind
//----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
rule "Start/Stop Manual Irrigation"
when

    Item VT_IrrigationManual_State changed

then

    // Phase 0: Basic value preparation
    val String ruleIdentifier = "rule.StartStopManualIrrigation"

    var OnOffType irrigationPumpCmd = OFF

    // Phase 1: Decide if action is required
    // Because this is a toggle rule there is no special decision required

    // Phase 2: Calculate required actions and values
    if (VT_IrrigationManual_State.state === ON) {

        irrigationPumpCmd = ON
        if (remindMeTimer === null) {

            remindMeTimer = createTimer(now.plusMinutes(remindMeDuration), [ |
                sendPushoverMessage(pushoverBuilder(String::format("Bewässerung ist seit %d min an!", remindMeDuration)).withDevice("Disorganiser"))
                sendPushoverMessage(pushoverBuilder(String::format("Bewässerung ist seit %d min an!", remindMeDuration)).withDevice("Froschzilla"))
                remindMeTimer.reschedule(now.plusMinutes(remindMeDuration))
           ])

       } else {

            remindMeTimer.reschedule(now.plusMinutes(remindMeDuration))

        }

    } else { // OFF or NULL

        irrigationPumpCmd = OFF
        remindMeTimer?.cancel()

    }

    // Phase 3: Perform actions
    // Just switch the irrigation pump unless no valves are installed
    BM_SC_IrrigationPump_State.sendCommand(irrigationPumpCmd)

end

My question is now. is setting the time some thing that should be done in Phase 2 or 3. If i move the timer studd to phase three i have to duplicate the whole if thing. What do you think?

Thomas

DPs are tools and tools are only applicable in certain circumstances. It is certainly possible to driver in a screw with a hammer, but a screw driver is a better tool to use.

This DP more than any other is for the benefit of the human. What makes more sense to you now? What will make more sense to future Thomas?

For me, everything that causes a side effect goes in section three. This includes:

  • calls to postUpdate and sendCommand
  • changing any global var
  • calling any Action to include createTimer

All the stuff in your section 0 typically goes in section 2 in my rules. I will only define variables above section 1 if I need them to do section 1. And then I’ll only define those necessary to do section 1. The purpose of section 2 is to calculate what to do in section 3 and the bulk of that is defining and populating variables and local vals.

@rlkoshak Thanks for your insights. This helps a lot, because typically you had something in mind while designing a pattern, that is not always obvious to others.

With the tool example you are completly right. That why i have always a hammer in my house :slight_smile:

Thanks Thomas