Friday, July 17, 2009

Design Review and the Open/Closed Principle

In my last blog I talked about the Single Responsibility Principle. SRP it is an easy concept to understand but breaking down a system into the “correct” objects is difficult to do well. It is imprecise and there is a lot of room for opinions. You seldom know the correct breakdown until you are finished. In this blog I want to take a look at the OCP or Open/Closed Principle. The Open/Closed principle states that Software entities (classes, modules, functions etc.) should be open for extension but closed for change. (“Agile Principles, Patterns, And Practices in C#” Robert C Martin and Micah Martin). In other words this means that we would like to be able to change the behavior of existing entities without changing the code or binary assembly. This concept is not as easy to understand. Did I just say to change something without changing it?

Let’s use a configurable rule engine as an example. Assume the simple rule engine has 2 types of entities, a Condition and an Action. The Action is simple, execute some logic and return true on success or false on failure. The condition is also simple, it checks some binary condition and returns true or false. The rules engine takes a list of conditions and actions and executes them. If an Action returns false, the rules stop executing and a rollback is performed. If a condition returns true, the next action is executed. If the condition is false, the next action is skipped and the following action is executed. Pretty simple so let’s look at some sample code:

Define our Conditions, Actions, and Rule Type with enums



Code Snippet



  1.     public enum Actions
  2.     {
  3.         CreateOrder,
  4.         CreateBackorder,
  5.         CloseOrder,
  6.         ShipOrder,
  7.         StoreOrder,
  8.         ReduceInventory
  9.     }
  10.     public enum Conditions
  11.     {
  12.         IsComplete,
  13.         IsInStock,
  14.         CanShip
  15.     }
  16.     public enum RuleType
  17.     {
  18.         Condition,
  19.         Action
  20.     \




Here is a very crude implementation of a rule engine



Code Snippet



  1.     public class RuleEngine
  2.     {
  3.         
  4.         public void ExecuteRules(int rulesId)
  5.         {
  6.             //Gather all the conditions and actions
  7.             //assume we have a Ruleset table that looks like this:
  8.             //ID - Int
  9.             //Index - Int
  10.             //RuleType - Int (corresponds to RuleTypeEnum)
  11.             //Rule - Int (corresponds to Conditions or Actions enum)
  12.             //Note, transaction logic, creating the parameter
  13.             //Error handling, and safe DataReader.Read() logic
  14.             //has been omitted for brevity
  15.             using (SqlConnection connection = new SqlConnection("ConnectString"))
  16.             {
  17.                 using ( SqlCommand cmd = new SqlCommand
  18.                     ("SELECT RuleType, Rule FROM Ruleset where ID = ? ORDER BY Index"
  19.                     , connection))
  20.                 {
  21.                     SqlDataReader rules = cmd.ExecuteReader();
  22.                     bool executing = true;
  23.                     while (executing)
  24.                     {
  25.                         
  26.                         rules.Read();
  27.                         if ((int)rules[0] == (int)RuleType.Action)
  28.                         {
  29.                             executing = ExecuteAction((int)rules[1]);
  30.                         }
  31.                         else
  32.                         {
  33.                             if(ExecuteCondition((int)(rules[1])))
  34.                             {
  35.                                 executing = true;
  36.                             }
  37.                             else
  38.                             {
  39.                                 rules.Read();
  40.                                 executing = true;
  41.                             }
  42.                         }
  43.                         //Implement some exit strategy here
  44.                     }
  45.                 }
  46.             }
  47.         }
  48.         public bool ExecuteCondition(int theCondition)
  49.         {
  50.             switch (theCondition)
  51.             {
  52.                 case (int)Conditions.IsComplete:
  53.                     //Is the order complete?
  54.                     return true;            //or false
  55.                 case (int)Conditions.CanShip:
  56.                     //Can we ship the order?
  57.                     return true;            //or false
  58.                 case (int)Conditions.IsInStock:
  59.                     //Is this item in stock?
  60.                     return true;            //or false
  61.                 default:
  62.                     throw new Exception("Unsupported Condition");
  63.             }
  64.         }
  65.         
  66.         public bool ExecuteAction(int theAction)
  67.         {
  68.             switch (theAction)
  69.             {
  70.                 case (int)Actions.CreateOrder:
  71.                     //Execute order create logic
  72.                     return true;            //or false
  73.                 case (int)Actions.CreateBackorder:
  74.                     //Execute backorder logic
  75.                     return true;            //or false
  76.                 case (int)Actions.ShipOrder:
  77.                     //Execute shipping logic
  78.                     return true;            //or false
  79.                 case (int)Actions.StoreOrder:
  80.                     //send to warehouse
  81.                     return true;            //or false
  82.                 case (int)Actions.CloseOrder:
  83.                     //Execute order close logic
  84.                     return true;            //or false
  85.                 case (int)Actions.ReduceInventory:
  86.                     //Remove item from inventory
  87.                     return true;            //or false
  88.                 default:
  89.                     throw new Exception("Unsupported Action");
  90.             }
  91.         }
  92.     \




Never mind the problems with error handling, transactions, and the lack of support for nested conditions, the point of the blog is OCP, not creating a rule engine. What we have will work but does it satisfy the OCP? No, there are a couple problems with this type of implementation that will make it difficult to maintain. The first problem is that the logic for the Rule Engine, Conditions, and Actions are all in one class and therefore one assembly. Any system that wants to use any of this logic will be tied to all of this logic. The second problem is that any time you want the rule engine to do something new, you have to modify this assembly which is a violation of the Open/Closed Principle.

Let’s take a look at a more robust design.

We will still use an enum to distinguish between Actions and Conditions:



Code Snippet



  1.     public enum RuleType
  2.     {
  3.         Condition,
  4.         Action
  5.     \




Now let’s declare an interface



Code Snippet



  1.     public interface ISupportRules
  2.     {
  3.         public bool Execute();
  4.         public RuleType TypeOfRule();
  5.     \




We will put both the enum and the Interface in an assembly called Rule.Types.

Now lets add a few classes



Code Snippet



  1.     public class CreateOrderAction:ISupportRules
  2.     {
  3.         #region ISupportRules Members
  4.         public bool Execute()
  5.         {
  6.             //Order Create Logic Here
  7.         }
  8.         public RuleType TypeOfRule()
  9.         {
  10.             return RuleType.Action;
  11.         }
  12.         #endregion
  13.     \




We will put this in an assembly called Rule.Actions.CreateOrder.



Code Snippet



  1.     public class CanShipCondition:ISupportRules
  2.     {
  3.         #region ISupportRules Members
  4.         public bool Execute()
  5.         {
  6.             //Execute Can Ship Logic here
  7.         }
  8.         public RuleType TypeOfRule()
  9.         {
  10.             return RuleType.Condition;
  11.         }
  12.         #endregion
  13.     \




We will put this class in Rules.Conditions.CanShip.

In fact we will create a separate assembly for each condition and action we defined in the enums.

Here is our new rules engine which goes in the Rule.Engine assembly:



Code Snippet



  1.         public void ExecuteRules(List<ISupportRules> rules)
  2.         {
  3.             bool executing = true;
  4.             int ruleIndex = 0;
  5.             while (executing)
  6.             {
  7.                 if (rules[ruleIndex].TypeOfRule() == RuleType.Action)
  8.                 {
  9.                     executing = rules[ruleIndex].Execute();
  10.                     ruleIndex++;
  11.                 }
  12.                 else
  13.                 {
  14.                     if (rules[ruleIndex].Execute())
  15.                     {
  16.                         ruleIndex++;
  17.                         executing = true;
  18.                     }
  19.                     else
  20.                     {
  21.                         ruleIndex += 2;
  22.                         executing = true;
  23.                     }
  24.                 }
  25.                 //Implement some exit strategy here
  26.             }
  27.         }
  28.     \




Notice that the ExecuteRules method takes a generic list of type ISupportRules as a parameter but has no reference to any of the conditions or actions. Also notice that the condition and action classes have no reference to each other or the rules engine. This is key to both code reuse and extensibility. the refactored rule engine, condition, and action classes are completely independent of each other. All they share is a reference to Rule.Type. Some other system may use any of these assemblies independent of each other with the only caveat being they will need to reference the Rule.Type assembly. The other thing we gained with this approach is we can now Extend the rule engine (make it execute new conditions and actions) by simply adding a new Condition or Action that implements ISupportRules and passing it into the ExecuteRules method as a part of the generic list. We can do all of this without recompiling the RefactoredRulesEngine which is the goal of the OCP. By the way, this design approach is called the Strategy Pattern.

If you haven’t noticed yet I’m leaving out one major piece of the puzzle. How does the generic list of rules get generated? I’m going to wave my hands here a little bit and save the details for another blog. We would use a creational pattern (one of the factory patterns). If we assume we are consuming the table outlined in our first solution this factory would accept a Ruleset ID and magically return the generic List<IsupportRules> of rules. The implementation of the factory pattern could be written in such a way that each time you add a Condition or an Action the factory would need to be recompiled or we could use a provider pattern and use a configuration file to allow us to create these new Conditions and Actions without a recompile.

To summarize things a bit: conceptually we have this RulesEngine that is relatively complex (much more complex than I have written) and we want to write it, test it, and leave it alone. At the same time though we have this need to enhance the system by adding more rules. By using using the strategy pattern we now have this stable rule execution engine that can execute any condition or action that implements the ISupprotRules interface. Because we inject a list of conditions and rules into the ExecuteRules method we can do all of this without recompiling the refactored rules engine. Another approach we might have taken to satisfy the OCP is the Template Method pattern. In the template method pattern we would make use of an abstract class to define the skeleton of an algorithm, then allow the concrete classes to implement subclass specific operations.

My design review checklist

One of my favorite design pattern web sites: http://www.dofactory.com/Patterns/Patterns.aspx

No comments:

Post a Comment