Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Design Pattern for dealing with a complex conditional evaluation

I am designed to maintain a system that takes in account the value of three variables to determine which action it will take.

I want to refactor it to use a design pattern, but could not find one suitable for it needs.

To explain the situation, I will use as an example a gym system.

Every gym user has a TYPE_OF_CONTRACT, that could be:

  • PLATINUM_MEMBERSHIP
  • GOLD_MEMBERSHIP
  • SILVER_MEMBERSHIP

The gym has some GYM_CLASSES:

  • WEIGHT_LIFTING
  • BODY_BALANCE
  • STEP
  • SPINNING
  • ZUMBA
  • PERSONAL_TRAINING

Every gym user has a PHYSICAL_CONDITION

  • NO_RESTRICTIONS
  • OVER_65
  • LIMITED_MOBILITY
  • MEDICAL_CONDITION
  • BELOW_18

For each combination of these three characteristics, a arbitrary set of actions should be executed. For example:

if PLATINUM_MEMBERSHIP + PERSONAL_TRAINING + OVER_65:

  1. medical approval needed
  2. signed form

if GOLD_MEMBERSHIP + PERSONAL_TRAINING + OVER_65:

  1. medical approval needed
  2. signed form
  3. extra monthly fee

if SILVER_MEMBERSHIP + PERSONAL_TRAINING + OVER_65:

  1. refuse subscription

if (any membership) + STEP + MEDICAL_CONDITION:

  1. medical approval needed
  2. signed form

if PLATINUM_MEMBERSHIP + WEIGHT_LIFTING + LIMITED_MOBILITY:

  1. medical approval needed
  2. signed form
  3. dedicated staff member assist

And so on.

The combination of characteristics can have a set of actions, that are not exclusive and not all of the combinations are ensured.

The legacy code uses nested switches as implementation. Example:

switch (contractType):

    case PLATINUM_MEMBERSHIP:

        switch (gymClass):            

            case (PERSONAL_TRAINING):

                switch (physicalCondition):            

                    case (OVER_65):

                        requiresMedicalApproval();
                        requiresSignedForm();

...

My problem is:

  • there are 3 conditions that combines to define a set of rules;
  • these rules are not necessarily unique;
  • not every combination defines a set;

I refactored a little using extract method technique and cleaning the code a little, but could not get rid of the 3 switches.

I wish to use design patterns to improve the design but so far I was unsuccessful.

I thought about polymorphism and Strategy, but could not figure a way to use any of them.

I also researched in google but haven't found anything that I could use.

What do you suggest?

Thank you.


EDIT:

A solution I reached, while researching @Paul's decision tree approach. After testing with a decision tree, I tried a three dimensional array, to define the conditions of the rules. I also used the Command pattern to define the actions that needed to be performed if the rule is activated.

In brief:

1) Enums to define the variables:

public enum TypeOfContract { ... }
public enum GymClasses { ... }
public enum PhysicalCondition { ... }

Every possible condition would be put in the enums.

2) The Command interface to define the actions

public interface Command {
    public void execute(Map<String, Object> parametersMap);
}

Every action would be an implementation of Command. The Map parameter will be used to pass runtime context to the methods.

3) A Procedures class to hold the actions needed for each condition.

public class Procedures {

    private List<Command> actionsToExecute = new LinkedList<Command>();

    public static final Procedures NO_ACTIONS_TO_EXECUTE = new Procedures();

    private Procedures() {}

    public Procedures(Command... commandsToExecute) {

        if (commandsToExecute == null || commandsToExecute.length == 0) {
            throw new IllegalArgumentException("Procedures must have at least a command for execution.");
        }

        for (Command command : commandsToExecute) {
            actionsToExecute.add(command);
        }
    }

    public List<Command> getActionsToExecute() {
        return Collections.unmodifiableList(this.actionsToExecute);
    }   
}    

The Procedures class represent the Commands that needed to be executed. It has a LinkedList of Command, to ensure that the Commands are executed in the desired order.

It has the NO_ACTIONS_TO_EXECUTE to be sent instead of a null, in case a combination of the three variables does not exist.

4) A RulesEngine class, to register the rules and its commands

public class RulesEngine {

    private static final int NUMBER_OF_FIRST_LEVEL_RULES = TypeOfContract.values().length;
    private static final int NUMBER_OF_SECOND_LEVEL_RULES = GymClasses.values().length;
    private static final int NUMBER_OF_THIRD_LEVEL_RULES = PhysicalCondition.values().length;

    private static final Procedures[][][] RULES =
            new Procedures[NUMBER_OF_FIRST_LEVEL_RULES]
                    [NUMBER_OF_SECOND_LEVEL_RULES]
                    [NUMBER_OF_THIRD_LEVEL_RULES];

    { //static block
        RULES
            [TypeOfContract.PLATINUM_MEMBERSHIP.ordinal()]
            [GymClasses.PERSONAL_TRAINING.ordinal()]
            [PhysicalCondition.OVER_65.ordinal()] =
                new Procedures(new RequireMedicalApproval(), 
                               new RequireSignedForm() );

        RULES
            [TypeOfContract.GOLD_MEMBERSHIP.ordinal()]
            [GymClasses.PERSONAL_TRAINING.ordinal()]
            [PhysicalCondition.OVER_65.ordinal()] =
                new Procedures(new RequireMedicalApproval(), 
                               new RequireSignedForm(), 
                               new AddExtraMonthlyFee() );

        ...             

    }

    private RulesEngine() {}

    public static Procedures loadProcedures(TypeOfContract TypeOfContract, 
            GymClasses GymClasses, PhysicalCondition PhysicalCondition) {
        Procedures procedures = RULES
                                [TypeOfContract.ordinal()]
                                [GymClasses.ordinal()]
                                [PhysicalCondition.ordinal()];
        if (procedures == null) {
            return Procedures.NO_ACTIONS_TO_EXECUTE;
        }
        return procedures;
    }

}

(Unusual code formatting done for the sake of visualization in this site)

Here the meaningful associations of variables are defined in the RULES three dimensional array.

The rules are defined by employing the corresponding enums.

For the first example I gave, PLATINUM_MEMBERSHIP + PERSONAL_TRAINING + OVER_65, the following would apply:

RULES
    [TypeOfContract.PLATINUM_MEMBERSHIP.ordinal()]
    [GymClasses.PERSONAL_TRAINING.ordinal()]
    [PhysicalCondition.OVER_65.ordinal()]

(the ordinal() is needed to return the int corresponding to the position of the enum)

To represent the actions needed to perform, a Procedures class is associated, wrapping the actions that are to be executed:

new Procedures(new RequireMedicalApproval(), new RequireSignedForm() );

Both RequireMedicalApproval and RequireSignedForm implement the Command interface.

The whole line for defining this combination of variables would be:

RULES
        [TypeOfContract.PLATINUM_MEMBERSHIP.ordinal()]
        [GymClasses.PERSONAL_TRAINING.ordinal()]
        [PhysicalCondition.OVER_65.ordinal()] =
            new Procedures(new RequireMedicalApproval(), 
                           new RequireSignedForm() );

To check if a particular combination has actions associated to them, the loadProcedures is called, passing the enums representing that particular combination.

5) Usage

    Map<String, Object> context = new HashMap<String, Object>();
    context.put("userId", 123);
    context.put("contractId", "C45354");
    context.put("userDetails", userDetails);
    context.put("typeOfContract", TypeOfContract.PLATINUM_MEMBERSHIP);
    context.put("GymClasses", GymClasses.PERSONAL_TRAINING);
    context.put("PhysicalCondition", PhysicalCondition.OVER_65);
    ...

    Procedures loadedProcedures = RulesEngine.loadProcedures(
                                        TypeOfContract.PLATINUM_MEMBERSHIP, 
                                        GymClasses.PERSONAL_TRAINING, 
                                        PhysicalCondition.OVER_65);

    for (Command action : loadedProcedures.getActionsToExecute()) {
        action.equals(context);
    }

All information the actions need to execute are now inside a Map.

The conditions, represented by the three enums, are passed to the RulesEngine.

The RulesEngine will evaluate if the combination has associated actions and it will return a Procedures object with the list of these actions that needs to be executed.

If not (the combination has no action associated to it), the RulesEngine will return a valid Procedures object with an empty list.

6) Pros

  • The usage code is much cleaner
  • The duplication of code in the switches of the legacy code are now gone
  • The actions are now standardized and well defined (each one in its own class)
  • The rules used are now much easier to discern (a developer just needs to look at the RULES array to know which rules are set and what will happen in each one of them)
  • New rules and actions can be easily added

7) Cons

  • Easy to make mistakes in the definition of the rules, as the declaration of them is verbose and not semantically analysed - it will accepted repetitions, for example, possibly overwriting previous definitions.
  • Instead of 3 switches nested inside each other, now I have several classes. The maintenance of the system is a little more complex than before, the learning curve a little steeper.
  • procedures and rules are not good names - still looking for better ones ;-)
  • Map as parameter can promote bad coding, cluttering it with a lot of content.
like image 559
Quaestor Lucem Avatar asked Oct 19 '22 18:10

Quaestor Lucem


2 Answers

How many options will you have? Let's say you have 8 per category, perhaps you can represent a particular combination as a 24-bit number, with 8 bits per category. When you receive a set of options, convert it over to a bit-pattern than then AND against bit-masks to figure out if a certain action needs to be done.

This still requires you to perform tests, but at least they are not nested, and you simply need to add a new test if/when you add a new feature.

like image 191
Vivin Paliath Avatar answered Oct 30 '22 18:10

Vivin Paliath


You could use a decision-tree and build it from tuples of values.

This would be a lot simpler and if properly implemented even faster than hard-coded conditions and in addition provides higher maintainability.

like image 30
Paul Avatar answered Oct 30 '22 17:10

Paul