RuleSet.cs
RuleSet.cs copied to clipboard
Simplify hideous conditional blocks with declarative logic
RuleSet
Simplify hideous conditional blocks with declarative logic
Why?
There are few things more rage-inducing than encountering huge chunks of conditional logic, which are often responsible for hugely important components of a given piece of software.
Rather than tightly coupling the given piece of logic with an explicit conditional flow, we can instead define the logic independently of any specific code, allowing for a much more flexible approach.
Install
You can install RuleSet using NuGet:
PM> Install-Package RuleSet
RuleSet is a PCL and supports the following platforms:
- .NET 4.0 or later (incl. Mono)
- Xamarin.iOS
- Xamarin.Android
- Windows Store 8
- Windows Phone 8
- Silverlight 5
Example
Wrong
Writing a simple calculator using only nested conditionals can get very ugly, very quickly.
if (operation != null) {
if (operation == Operation.Add) {
return x + y;
} else if (operation == Operation.Subtract) {
return x - y;
} else if (operation == Operation.Multiply) {
return x * y;
} else if (operation == Operation.Divide) {
return x / y;
} else {
return null;
}
} else {
return null;
}
Right
Fortunately, RuleSet lets us refactor this giant blob of code using a much
more manageable approach.
var calculator = new RuleSet<Operation,Func<int,int,int>>(defaultResult: null);
calculator.When(o => o == Operation.Add).Then((x,y) => x + y);
calculator.When(o => o == Operation.Subtract).Then((x,y) => x - y);
calculator.When(o => o == Operation.Multiply).Then((x,y) => x * y);
calculator.When(o => o == Operation.Divide).Then((x,y) => x / y);
return calculator.First(Operation.Add)(1,2); // returns 3
This is but a simple, highly contrived example, yet it has massive implications to avoid writing terrible blocks of imperative, nested conditional logic.
Usage
RuleSet
The first step of using RuleSet is to instantiate a
RuleSet<T1,T2,…,TN,TResult> object with the desired generic types.
var ruleSet = new RuleSet<Operation,Func<int,int,int>>();
The RuleSet constructor takes one optional argument, which is the default
object of type TResult to be returned when no Rules are matched.
The first few generic types define the inputs required to identify a matching
Rule. In this case, we've defined that an Operation object will be
used to try matching each Condition. As you can see, many generic input types
can be specified.
After all the generic input types comes the TResult type, which is returned
from the Rule whose Condition is matched. In this case, a Func is
returned, which takes two ints and returns another int.
This RuleSet allows us to define Conditions which match against different
Operation enum values and return the respective Func to perform the given
mathematical operation.
When
The When method is called on a RuleSet to define the Condition for a given
Rule. When method returns a builder object which lets us finish building
a Rule by calling the Then method.
In the case of more advanced logic being required to evaluate the Condition
being defined, your own Func<T1,T2,…,TN,bool> can be supplied, which allows
you to implement the logic required to match the Condition.
Then
Passing an object of type TResult to the Then method will complete the
process of defining a Rule within our RuleSet.
The type you define TResult to be is only limited by your imagination. It
could be a simple primitive type, or it could be an Action or Func of your
own making, allowing you to invoke complex logic after matching against a
collection of Conditions.
First
Now that we've defined one or more rules, we can find the first matching Rule.
This takes arguments of types T1, T2 and so on, up to TN.
The given arguments are used to invoke each Condition to find the first one
which evaluates to true. The Result property of the matching Rule is returned.
All
If we instead want all Result objects to be returned from all matching
Rules, we can call All instead of First. This instead evaluates all Rule
objects within the RuleSet and returns the Result properties for each Rule
whose Condition evaluates to true.
Development
A copy of .NET or Mono is required to build the solution. You can install Mono
on OS X by running brew bundle, assuming you have Homebrew installed.
The supplied Makefile defines the following targets:
make cleanmake buildmake test