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:
The gym has some GYM_CLASSES:
Every gym user has a PHYSICAL_CONDITION
For each combination of these three characteristics, a arbitrary set of actions should be executed. For example:
if PLATINUM_MEMBERSHIP + PERSONAL_TRAINING + OVER_65:
if GOLD_MEMBERSHIP + PERSONAL_TRAINING + OVER_65:
if SILVER_MEMBERSHIP + PERSONAL_TRAINING + OVER_65:
if (any membership) + STEP + MEDICAL_CONDITION:
if PLATINUM_MEMBERSHIP + WEIGHT_LIFTING + LIMITED_MOBILITY:
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:
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
7) Cons
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With