Consider the following example.

Bob’s Ultimate Garage

Let’s say you are one of Bob’s lucky customers and park your car at Bob’s Ultimate Garage.  This Garage is highly efficient and has mechanisms in place to help you get to your optimal parking spot faster, reduce car congestion, and give you favorable pricing for features that you want and nothing more.  We’ll assume the following properties for the Garage:

  • Has three levels (1-3)
  • Each level has three sections
    • Section 1 – for daily parking. Enter in the morning, leave in the evening
    • Section 2 – for over-night parking.  Enter whenever, stay the night, leave in the morning.
    • Section 3 – hourly parking.
  • Each level has different security strategies (for keeping car thieves out)The Garage’s pricing scheme will charge you more for a level with stricter security.
    • Level 1 – need your parking ticket to enter
    • Level 2 – closed circuit cameras in addition to needing a ticket to enter
    • Level 3 – on-site security guard patrolling the premises

To help the driver decide where to park, Bob has integrated in a new parking Ticketing System.

Bob’s fancy new Ticketing System will work in the following way:

  • When a Car enters the Garage, it will pull up to a self-service ticket stand.  User will press a typical button to clock in.
  • Once a car pull up, a scanner will feed the following data into the Ticket System:
    • Current Time (of check in)
    • Year and Model of the car

Based on this input, the Ticketing System will print out a Ticket with the following:

  • Two recommendations for Level and Section of the garage
  • Estimate of hourly cost for each recommendation
  • Confirmation of Check-in timestamp (obviously)

Initial Analysis

To implement Bob’s Ticketing System we will require some conditional logic.  The main drivers for determining what the produced Ticket will recommend are:

  1. The type of car
  2. The time period of the day a customer enters Bob’s garage to park, ie Morning, Afternoon, Evening

The duration of a customer’s stay at the garage may influence the avg hourly rate Bob will charge.  Bob may wish to offer discounts for certain cars, for extended stays, etc.  For the sake of our example we will assume that Bob’s discounts will just be a function of time and the select level/section of the garage.

Basic domain modeling

From the “nouns” discussed so far we know to create objects representing:

  • A Car, capturing the CarType, CarModel, year of it make
  • Enum representing TimeOfDay, ie. Morning, Afternoon, Evening, GarageLevel, GarageSection
  • An HourlyEstimate which will retain the estimated number of hours that a customer is expected to stay at the garage (based on Bob’s data analysis) and the hourly rate determined by associated business rules.
  • A Recommendation for a GarageLevel, GarageSection and Hourly Rate
  • A TicketRequest representing input from the Ticketing system: time of ticket request, details of the customer’s car
  • And of course a TicketResponse which contains all recommendations we promised to deliver to our customer

A quick mock-up of what we need yields:

 Java |  copy code |? 
01
02
enum CarType {
03
        //Using Guava's sets
04
        //Car models are defined in-line so you don't forget to define models for each supported car type when adding new car types
05
        //see Effective Java chapters on Enums for further reading
06
        FERARI(CarClass.SPORT, Sets.newHashSet("california", "458 italis", "f12 berlinetta", "ff")),
07
        LEXUS(CarClass.LUXURY, Sets.newHashSet("is", "gs", "ls")),
08
        OLDSMOBILE(CarClass.CLASSIC, Sets.newHashSet("pirate", "55", "defender"));
09
10
}
11
 
12
enum TimeOfDay {
13
        MORNING,
14
        AFTERNOON,
15
        EVENING,
16
        ANY;
17
}
18
 
19
enum CarClass {
20
        LUXURY,
21
        SPORT,
22
        CLASSIC
23
}
24
 
25
enum GarageLevel {
26
        LEVEL1,
27
        LEVEL2,
28
        LEVEL3
29
}
30
 
31
enum GarageSection {
32
        SECTION1,
33
        SECTION2,
34
        SECTION3
35
}
36
 
37
class Car {
38
        final CarType carType;
39
        final int makeYear;
40
}
41
 
42
class HourlyEstimate {
43
        final int numHoursEst;
44
        final BigDecimal avgHourlyPrice;
45
}
46
 
47
class Recommendation {
48
        final GarageLevelSection gls;
49
        final HourlyEstimate estimate;
50
}
51
 
52
class TicketRequest {
53
        final DateTime entryTimestamp;
54
        final String modelName;
55
        final int makeYear;
56
}
57
 
58
class TicketResponse {
59
        final DateTime entryTimestamp;
60
        final Car car;
61
        final Recommendation recommendation1;
62
        final Recommendation recommendation2;
63
}
64

For full code see https://github.com/Betterment/BetterDev/blob/master/BobsUltimateGarage.java.

Capturing behaviors and defining rules

One way to begin our solution is to envision conditionals that capture all system behaviors as they were envisioned by Bob.  Let’s mock up some business rules:

  1. All Classics such as the 1902 Oldsmobile Pirate should favor Level 1, Section 1 in the Morning, and Level 1, Section 2 otherwise.  (Bob doesn’t like Oldsmobiles and doesn’t think they require monitoring)
  2. All Luxury cars such as the Lexus GS should favor Level 3, Section 1 in the Morning, Level 3, Section 2 in the Afternoon, and Level 3, Section 3 in the Evening.

And so on for other classes of cars we wish to support…

Since pricing is based on the chosen Level, Section and Time we can contrive some rules here as well:

  1. Level 1 is the cheapest
  2. Level 2 is average, unless it’s in Section 3 where it is as expensive as Level 3
  3. Level 3 is the most expensive

Since the duration of stay may influence the hourly avg price, we need some way of associating the Time of Day (Morning, Afternoon, Evening) with the estimated number of hours Bob believes a car is likely to remain parked in the garage.

To model this in code, our instinct may be to try out some if/else blocks.  Here is a snippet from BobsUltimateGarage.evaluateProcedurally() method:

 Java |  copy code |? 
01
02
TicketResponse evaluateProcedurally(TicketRequest request) {
03
        log.info("Processing procedurally: " + request);
04
        CarType carType = CarType.fromModel(request.modelName);
05
        CarClass carClass = carType.getCarClass();
06
        Car car = new Car(carType, request.makeYear);
07
 
08
        TimeOfDay timeOfDay = TimeOfDay.fromDateTime(request.entryTimestamp);
09
        ...
10
 
11
        //with just two variables, we have two levels of nested if/elses
12
        if (CarClass.CLASSIC.equals(carClass)) {
13
            //Bob doesn't value classics much...
14
            if (TimeOfDay.MORNING.equals(timeOfDay)) {
15
                //this can be refactored more... for compostional object building, Builder method is preferred.
16
                HourlyEstimate estimate1 = calculateHourlyEstimateImperatively(timeOfDay, GarageLevel.LEVEL1,
17
                        GarageSection.SECTION1);
18
                r1 = new Recommendation(new GarageLevelSection(GarageLevel.LEVEL1, GarageSection.SECTION1), estimate1);
19
 
20
                HourlyEstimate estimate2 = calculateHourlyEstimateImperatively(timeOfDay, GarageLevel.LEVEL2,
21
                        GarageSection.SECTION1);
22
                r2 = new Recommendation(new GarageLevelSection(GarageLevel.LEVEL2, GarageSection.SECTION1), estimate2);
23
            } else {
24
                HourlyEstimate estimate1 = calculateHourlyEstimateImperatively(timeOfDay, GarageLevel.LEVEL1,
25
                        GarageSection.SECTION2);
26
                r1 = new Recommendation(new GarageLevelSection(GarageLevel.LEVEL1, GarageSection.SECTION2), estimate1);
27
 
28
                HourlyEstimate estimate2 = calculateHourlyEstimateImperatively(timeOfDay, GarageLevel.LEVEL2,
29
                        GarageSection.SECTION2);
30
                r2 = new Recommendation(new GarageLevelSection(GarageLevel.LEVEL2, GarageSection.SECTION2), estimate2);
31
            }
32
 
33
        } else if (CarClass.LUXURY.equals(carClass)) {
34
            if (TimeOfDay.MORNING.equals(timeOfDay)) {
35
                ...
36
 
37
            } else if (TimeOfDay.AFTERNOON.equals(timeOfDay)) {
38
                ...
39
 
40
            } 
41
42
        }
43
        return new TicketResponse(request.entryTimestamp, car, r1, r2);
44
}
45

And our simulated methods for estimating the duration of stay and the avg hourly rate:

 Java |  copy code |? 
01
02
private int getHourEstimateFromTimeOfDay(TimeOfDay timeOfDay) {
03
        int result = 1; //made up avg stay
04
        if (TimeOfDay.AFTERNOON.equals(timeOfDay)) {
05
            result = 2; //maybe an errand?
06
        } else if (TimeOfDay.MORNING.equals(timeOfDay)) {
07
            result = 8; //8-hr work day?
08
        } else if (TimeOfDay.EVENING.equals(timeOfDay)) {
09
            result = 12; //overnight?
10
        }
11
        return result;
12
    }
13
 
14
private BigDecimal getHourlyRate(GarageLevel level, GarageSection section) {
15
        BigDecimal result = new BigDecimal(5); //$5.00/hr default
16
        if (GarageLevel.LEVEL1.equals(level)) {
17
            if (GarageSection.SECTION1.equals(section)) {
18
                result = new BigDecimal(5);
19
            } else {
20
                result = new BigDecimal(7);
21
            }
22
        } else if (GarageLevel.LEVEL2.equals(level)) {
23
            if (GarageSection.SECTION3.equals(section)) {
24
                result = new BigDecimal(10);
25
            } else {
26
                result = new BigDecimal(5);
27
            }
28
        } else if (GarageLevel.LEVEL3.equals(level)) {
29
            result = new BigDecimal(10);
30
        }
31
        return result;
32
    }
33

Observations on our first coding attempt

The above code works but should feel very procedural and overly complex – even with good commentary and studious refactoring.  For every additional variable introduced into our business rules, the complexity of our conditionals increases manifold. If we were to introduce a new time slice, say LUNCH, each CarClass codified in our conditional will have to provide support for the new time period.  For every nuance in rate calculations, we’ll have to work on correctly updating behavior without introducing regression bugs.

To sum up, the if/else approach suffers from:

  • Redundancy.  For each outer grouping, we must repeat inner groupings or find clever ways to refactor logic into helper methods.
  • Explosive complexity.  As new variables are introduced the decision tree becomes very complex, quickly.
  • Challenged Maintainability.  If we go beyond our contrived example and work with more nuanced rules, the resulting code may become brittle and susceptible to careless mistakes.  For example, if we were to add the LUNCH TimeOfDay, a developer supporting this code may easily overlook a helper method that performs conditional processing on the TimeOfDay enum.  In such a case, refactoring may have hurt maintainability of the code!
  • Compromised Readability and Comprehension.  With enough values or states that a given variable can take, the decision tree can quickly become difficult to reason about.

An alternative (Better) approach

A better way to capture Bob’s Ultimate Garage rules is to actually model out a Rule Engine.  To do this in a manner which avoids all of the aforementioned pitfalls we introduce Guava’s Function object and the Apache Commons MultiKeyMap implementation.

MultiKeyMaps are super neat because they permit multiple discrete objects (including primitives) to act as a single composite key into the underlying map implementation.  Applied, a MultiKeyMap will allow us to collapse nested if/else statements into a single, flat, composite key.

Guava’s Functions are delegate mechanisms with some of the Java nastiness encapsulated away.  For C++ buffs, you can think of these Functions as function pointers (but they are references).  Functions will allow us to elegantly implement multiple service contracts while encapsulating nuanced functionality of Bob’s business rules.

Combining Functions with Maps will allow us to configure multiple Strategies for our Business Rules in a declarative, flexible, and maintainable manner.  To illustrate some variations, BobsUltimateGarage uses MultiKeyMaps for modeling Garage Level and Section rules without any Functional idioms while a simple HashMap is used to configure hourly rate calculation strategies for supported Car Classes (ie Classical, Luxury, Sport) using Functional idioms.

Let’s take a look at the CarClass-driven representation of rules pertaining to avg hourly rate calculations:

 Java |  copy code |? 
01
02
void initializeBusinessRules() {
03
        BIZ_RULES.put(CarClass.CLASSIC, new BusinessRule(complexDateConverterFunction, simpleDurationEstimateFunction,
04
                simpleRateEstimateFunction));
05
 
06
        BIZ_RULES.put(CarClass.LUXURY, new BusinessRule(complexDateConverterFunction, minDurationEstimateFunction,
07
                saleRateEstimateFunction));
08
 
09
        BIZ_RULES.put(CarClass.SPORT, new BusinessRule(simpleDateConverterFunction, simpleDurationEstimateFunction,
10
                saleRateEstimateFunction));
11
 
12
    }
13

Here, the BusinessRule object is nothing more than a container for multiple Functions.  These functions comprise a Strategy for calculating estimated duration of stay and the avg hourly rate for each CarClass.

Functions like “simpleDateConverterFunction” and “complexDateConverterFunctions” share signatures and therefore act as implementations of a shared interface.  (In fact, under the hood, Functions are nothing more than a genericized implementation of an Interface with a single apply() method.)

Further, these functions cleanly encapsulate different behaviors we wish to model. For example, we’ve contrived a “minDurationEstimateFunction” and a “simpleDurationEstimateFunction” to help us imagine the many different pricing strategies that Bob may come up with.  Here is a closer look at these two functions:

 Java |  copy code |? 
01
02
    //no minimum stay
03
    private final Function simpleDurationEstimateFunction = new Function() {
04
        @Override
05
        public Integer apply(TimeOfDay timeOfDay) {
06
            //reusing functionality from elsewhere... why not?
07
            return getHourEstimateFromTimeOfDay(timeOfDay);
08
        }
09
    };
10
 
11
    //introduce a 2 hr minimum stay
12
    private final Function minDurationEstimateFunction = new Function() {
13
        @Override
14
        public Integer apply(TimeOfDay timeOfDay) {
15
            //reusing functionality from elsewhere... why not?
16
            int estimate = getHourEstimateFromTimeOfDay(timeOfDay);
17
            return Math.max(estimate, 2);
18
        }
19
    };
20

Each strategy can be reapplied to other CarClass types in a declarative manner, with ease.  There is also no limit to how fancy the inner implementation of each Function can be.  Our production code using similar techniques makes database calls, web services calls, etc.

Rules to produce a Recommendation are similarly encoded into a MultiKeyMap.  Although we don’t use any Functional strategies here, we can easily envision doing so.

 Java |  copy code |? 
01
02
void initializeRecommendations() {
03
        RECOMMENDATIONS.put(CarClass.CLASSIC, TimeOfDay.MORNING, 1, new GarageLevelSection(GarageLevel.LEVEL1,
04
                GarageSection.SECTION1));
05
        RECOMMENDATIONS.put(CarClass.CLASSIC, TimeOfDay.MORNING, 2, new GarageLevelSection(GarageLevel.LEVEL2,
06
                GarageSection.SECTION1));
07
        RECOMMENDATIONS.put(CarClass.CLASSIC, TimeOfDay.ANY, 1, new GarageLevelSection(GarageLevel.LEVEL1,
08
                GarageSection.SECTION2));
09
        RECOMMENDATIONS.put(CarClass.CLASSIC, TimeOfDay.ANY, 2, new GarageLevelSection(GarageLevel.LEVEL2,
10
                GarageSection.SECTION2));
11
12
}
13

Note how much easier it is to reason about this code than it is to reason about the if/else mess we previously created.

The clincher however, comes in constructing the final TicketResponse object.

Building the TicketResponse

The functional/declarative approach to modeling business rule strategies lends itself to a more elegant technique for constructing our response.  Since everything is either pre-packaged into a value object or is a collection of Functions, we simply need to sequence construction of the result correctly.  To illustrate, consider this quick-and-dirty Builder for producing a TicketResponse.  Its only responsibilities are to parse the Request and then “apply” our rules in the proper sequence (everything that an API will provide documentation for).  In fact, the entire ingestion of a TicketRequest and the construction of a TicketResponse is now but a few lines of code:

 Java |  copy code |? 
01
02
TicketResponse evaluateFunctionally(TicketRequest request) {
03
        log.info("Processing functionally: " + request);
04
        Car car = new Car(CarType.fromModel(request.modelName), request.makeYear);
05
 
06
        //can be further refactored into a helper Builder class
07
        BusinessRule bizRule = getBizRule(car, request.entryTimestamp);
08
        TimeOfDay timeOfDay = bizRule.timeOfDayFunction.apply(request.entryTimestamp);
09
        GarageLevelSection glsReco1 = getRecommendation(car.getCarClass(), timeOfDay, 1);
10
        HourlyEstimate estimate1 = new HourlyEstimate(bizRule.durationEstimateFunction.apply(timeOfDay),
11
                bizRule.rateEstimateFunction.apply(glsReco1));
12
        Recommendation r1 = new Recommendation(glsReco1, estimate1);
13
 
14
        GarageLevelSection glsReco2 = getRecommendation(car.getCarClass(), timeOfDay, 2);
15
        HourlyEstimate estimate2 = new HourlyEstimate(bizRule.durationEstimateFunction.apply(timeOfDay),
16
                bizRule.rateEstimateFunction.apply(glsReco2));
17
        Recommendation r2 = new Recommendation(glsReco2, estimate2);
18
        return new TicketResponse(request.entryTimestamp, car, r1, r2);
19
    }
20

The above can be further simplified with a bit more refactoring.  Even without additional cleanup, we still see the effect.

When perusing the full code sample (https://github.com/Betterment/BetterDev/blob/master/BobsUltimateGarage.java) note how much simpler and more readable this method is compared to the original “evaluateProcedurally” method.  The “evaluateFunctionally” method doesn’t care about the underlying strategies chosen, nor any of the implementation details what-so-ever.  It only cares about computing the result and packaging everything into a TicketResponse.

Should Bob introduce new pricing schemes or new scheduling slices, we will only have to make updates to our Rules Map definitions and write new Functions for capturing new behaviors.

In Conclusion

We’ve covered two very different approaches to solving one common problem.  The first approach, using nested if/else conditionals, suffers from complexity creep, maintenance, and comprehension challenges.  A logic tree which combinatorially expresses the impact of multiple variables can quickly result in repetitive, procedural code that is not pleasant to work with.

An alternate approach is to leverage functional idioms available to us in Java along with single-key or multi-key HashMap implementations.  This “Better” approach allows us to not only represent our state machine in a declarative manner but also to encapsulate rule variations cleanly.  Since all the heavy lifting is abstracted away, the API for such a Rule Engine can remain litter-free.  A response Builder of some sort can be provided to “apply” selected strategies and to construct the result while remaining completely ignorant of the underlying implementation.

When might such a design choice be appropriate?

  1. When a decision tree is likely to consider multiple variables
  2. and the states of these variables can be expressed declaratively
  3. and when the response object requires non-trivial construction – meaning that the values it takes on requires the completion of several business processes
  4. and the strategy for executing a business process may change dynamically depending on the given state of the application
  5. and finally, when it is natural to describe rules as a concatenation of “if this and that and this other thing, then we should do so and so”

Thinking Ahead

Once conditional logic is expressed as a table of rules and lookups, we can consider maintenance (code upkeep) challenges inherent to such a solution.  The rules map, although defined in a quasi-declarative way, is still hard-coded!  Any change to a rule will require a patch, build, and deploy cycle.  If you are like Betterment and your organization is agile enough to push out multiple patch releases in a week (or a day) without breaking a sweat, perhaps you need go no further.  For best-practice enthusiasts, purists, or those of us bogged in a lengthier release cycle, we would naturally strive for a rules engine that is configurable outside of our code-base.

A few options exist.

One option is to consider modeling the whole data structure as a Spring bean.  The xml will likely be messy but some namespace tricks in Spring might make it less verbose.  The challenge lies in dependency-injecting Functions with Spring.  Interested in thinking through this problem further? See our BetterDev challenge below.

Another option is to codify the rules engine into a Rules table of a relational (or maybe even a No-SQL) database.  Lookup keys used in our multi-key maps are easily convertible into composite keys in our database table.  Pulling the right rule is as simple as running a Select query.  Again, the challenge is in coming up with an elegant way of correlating a Rule with the Strategy used to operate on your data at runtime.  We’ve thought about the solution a bit but again leave it up to you to take this further.  Interested? See our BetterDev challenge below.

Yet another option is to consider a full-fledged rule modeling framework, such as DRools and MVEL if keys into a rule are not constant but require runtime evaluation.

Additional Readings

MultiKeyMap: http://commons.apache.org/collections/apidocs/org/apache/commons/collections/map/MultiKeyMap.html

Guava’s Functional Idioms:

http://code.google.com/p/guava-libraries/wiki/FunctionalExplained

Cool Rules Engine using Drool and MVEL:

http://java.dzone.com/articles/really-simple-powerful-rule

Lambda expressions and higher-order functions in Java 8 coming soon!

http://www.infoq.com/articles/java-8-vs-scala

Download the full sample code

https://github.com/Betterment/BetterDev/blob/master/BobsUltimateGarage.java

BetterDev Challenge!

1) Port the included example to Java 8 and replace Guava library calls with functional idioms available in Java 8.

2) Convert the in-memory rules data struct (multi-key map and hashmap) to rules tables in your favorite database.  (Relational or No-SQL).  How would you model functions (function pointers) as part of your rule definition?

3) Want to make our solution Better or wish to demo this example with a 3rd party rule engine frmework? Submit a patch to BobsUltimateGarage.java on our GitHub.

We will post all complete and elegant solutions.  Happy coding towards BetterDev.