The Specification Pattern
A long while ago I was afforded the pleasure of working on a .Net WinForms call center application for a large company. I’ll call them Acme Finance. The application consisted of fifty or so screens: a screen to verify who was calling, one to show recent transactions, one for payment history, etc.
The problem was that Acme kept buying other companies, and each company (they called them business segments) required extensive customization of the screens. The code became littered with conditional logic for adding, removing, and tweaking features based on the business segment to which the logged-in CSR belonged.
Eventually it grew to the point that the application wasn’t cost effective to maintain and had to be rewritten. Having learned that so much conditional logic isn’t maintainable, Acme decided that each business segment should get their own copy of the screens. The problems created by duplication were far worse than those caused by the conditional logic, but that’s another post.
One of the more ridiculous aspects of the new code base was the mechanism by which the application decided which set of screens to load. There were about seven parameters to this decision, each one having three or four possible values. Some of the more significant parameters were:
- Which business segment is this call center serving?
- What is CSR’s role?
- Which environment are we in?
- What type of customer is on the phone?
To figure out which screens to load, all seven parameters were joined together to form a key into the configuration file. These keys mapped each combination of parameter values to one of about thirty or so “navigation XML files,” each containing a list of screens. Thus we had thousands of lines that all looked very similar to this:
<add key="Acme.Manager.Production.Silver.This.That.Other" value="NavData_026.xml" />
This mapping was hard to make any sense out of, was difficult to maintain, and became a huge source of bugs. When a bug was determined to be caused by a bad mapping, often only that mapping was fixed and the bug would show up again in a slightly diferent context, perhaps when it was a Gold customer calling instead of a Silver customer.
One solution to this problem is to give each navigation XML file a specification of conditions that must be met in order for it to be selected. For instance, you might have something like this:
<NavigationXml filename="NavData_PreferentitalService.xml">
<Or>
<CustomerType>Platinum</CustomerType>
<And>
<Or>
<CSRType>Admin</CSRType>
<CSRType>Manager</CSRType>
</Or>
<Not>
<BusinessSegment>GrouchyBiz</BusinessSegment>
</Not>
</And>
</Or>
</NavigationXml>
This states that the preferential service navigation XML file is to be used when the customer is “platinum level” or if the CSR is an administrator or manager. However, preferential service is never available to those calling GrouchyBiz.
The implementation is straightforward. Say that the context of the call is stored in an instance of the CallingContext class:
class CallingContext
{
public string CustomerType;
public string CSRType;
public string BusinessSegment;
}
We’ll start by defining an interface for evaluating whether or not a CallingContext matches a specification:
interface ICallingContextSpec
{
bool Matches(CallingContext c)
}
Next we build ICallingContextSpec implementations for matching specific CustomerType, CSRType, and BusinessSegment values. For example we might have something like this:
class CustomerTypeSpec : ICallingContextSpec
{
private string _customerType;
public CustomerTypeSpec(string customerType) {
_customerType = customerType;
}
public bool Matches(CallingContext c) {
return c.CustomerType == _customerType;
}
}
This approach really starts to shine when we add a few classes that allow us to specify and, or, and not expressions:
class AndSpec : ICallingContextSpec
{
private ICallingContextSpec[] _specs;
public AndSpec(params ICallingContextSpec[] specs) {
_specs = specs;
}
public bool Matches(CallingContext c) {
foreach (ICallingContextSpec spec in _specs) {
if (!spec.Matches(c))
return false;
}
return true;
}
}
class OrSpec : ICallingContextSpec
{
private ICallingContextSpec[] _specs;
public OrSpec(params ICallingContextSpec[] specs) {
_specs = specs;
}
public bool Matches(CallingContext c) {
foreach (ICallingContextSpec spec in _specs) {
if (spec.Matches(c))
return true;
}
return false;
}
}
class NotSpec : ICallingContextSpec
{
private ICallingContextSpec _spec;
public NotSpec(ICallingContextSpec spec) {
_spec = spec;
}
public bool Matches(CallingContext c) {
return !_spec.Matches(c);
}
}
Now we can compose objects to build specifications. For instance, the XML example above would be built like this:
ICallingContextSpec preferentialSpec = new OrSpec(
new CustomerTypeSpec("Platinum"),
new AndSpec(
new OrSpec(
new CSRTypeSpec("Admin"),
new CSRTypeSpec("Manager")
),
new NotSpec(
new BusinessSegmentSpec("GrouchyBiz")
)
)
);
if (preferentialSpec.Matches(currentCallingContext)) {
// preferential treatment available
}
Of course we need to be able to load these specifications from XML, and that too is simple. Instead of using the constructor to configure the individual specification objects, we’ll add a LoadFromXml method to the ICallingContextSpec interface:
interface ICallingContextSpec
{
void LoadFromXml(XmlNode node);
bool Matches(CallingContext c);
}
Then we’ll introduce a factory for instantiating the correct ICallingContextSpec based on the XML node’s name:
class SpecFactory
{
public static ICallingContextSpec FromXml(XmlNode node) {
ICallingContextSpec spec = (ICallingContextSpec)
Activator.CreateInstance(Type.GetType(node.Name + "Spec"));
spec.LoadFromXml(node);
return spec;
}
}
With this in place, our ICallingContextSpec classes look like these:
class CSRTypeSpec : ICallingContextSpec
{
private string _csrType;
public void LoadFromXml(XmlNode node) {
_csrType = node.InnerText;
}
public bool Matches(CallingContext c) {
return c.CSRType == _csrType;
}
}
class OrSpec : ICallingContextSpec
{
private List<ICallingContextSpec> _specs;
public void LoadFromXml(XmlNode node) {
_specs = new List<ICallingContextSpec>();
foreach (XmlNode child in node.ChildNodes)
_specs.Add(SpecFactory.FromXml(child));
}
public bool Matches(CallingContext c) {
foreach (ICallingContextSpec spec in _specs) {
if (spec.Matches(c))
return true;
}
return false;
}
}