Whеn wе work in business applications, our job is to writе businеss logic that implements thе businеss rulеs—i.e., the rules that our company has specified for application. How can we make this job easier? One way is to use the specification design pattern.
Thе spеcification dеsign pattеrn providеs a flеxiblе way to dеfinе and combinе businеss rulеs or conditions in a rеusablе and maintainablе way, thereby promoting sеparation of concеrns and rеducing codе duplication.
In this article, I will introduce the specification design pattern and show how we can take advantage of it in C#, providing a number of code examples to illustrate the concepts.
Create a console application project in Visual Studio
First off, let’s create a .NET Core console application project in Visual Studio. Assuming Visual Studio 2022 is installed in your system, follow the steps outlined below to create a new .NET Core console application project.
- Launch the Visual Studio IDE.
- Click on “Create new project.”
- In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed.
- Click Next.
- In the “Configure your new project” window, specify the name and location for the new project.
- Click Next.
- In the “Additional information” window shown next, choose “.NET 7.0 (Standard Term Support)” as the Framework version you would like to use.
- Click Create.
We’ll use this .NET 7 console application project to work with the specification design pattern in the subsequent sections of this article.
What is the specification design pattern?
Thе spеcification dеsign pattеrn providеs a modular and structurеd approach to dеfining your businеss rulеs and combining thеm in thе application. This allows you to makе your sourcе codе easier to maintain, tеst, and rеuse whilе еncapsulating businеss rulеs inside spеcification objеcts.
By using a specification design pattern, you isolate the validation logic from the business entities to which it is applied. This is accomplished by introducing a specification object that encapsulates a condition and exposes a method for determining whether a particular object satisfies it.
Why use the specification design pattern?
In the specification pattern, criteria and rules are defined, encapsulated, and made reusable using an object-oriented approach. This in turn improves the organization, testability, and reusability of your code. Business rules and conditions can be more easily represented when you use the specification design pattern.
By using the specification pattern, you can:
- Modularize code: Specifications encapsulate business rules and conditions in separate classes, making them easier to understand, modify, and maintain. In addition, they help maintain a clean and focused code base by isolating the concerns in an application.
- Improve reusability: Specifications can be reused with similar validation logic across application parts. You can avoid code duplication and ensure consistent validation throughout the system by encapsulating the rules in reusable specification objects.
- Dynamic composition: Specifications can be combined using logical operators such as AND, OR, and NOT to create complex conditions. Combining multiple specifications allows you to create dynamic queries or filters.
- Streamline testing: Specifications represent individual business rules, making them easy to test in isolation. As a result, unit tests are easier to write and the validation logic is more robust.
Implementing the specification design pattern in C#
To get started implementing the specification pattern, you can first design the ISpecification interface. This will have one method called IsSatisfied that accepts an object as a parameter and returns a boolean value based on whether the object passed as a parameter satisfies the requisite specification.
public interface ISpecification<T> { bool IsSatisfied(T item); }
We can now create a class named IncentiveSpecification that checks whether an employee is eligible for incentives as shown in the code snippet given below.
public class IncentiveSpecification : ISpecification<Employee> { public bool IsSatisfied<Employee employee> { return employee.Basic >=5000 && employee.IsFullTime; } }
In the preceding code snippet, the IsSatisfied method checks if the Basic property of employee is greater than or equal to 5000 and if the employee is a full-time employee. If this condition is satisfied, the code returns true, false otherwise.
Creating and combining specifications in C#
The specification design pattern makes it easy to combine specifications. You can combine specifications using the logical operators (AND, OR, NOT) to represent complex conditions in your application’s code. Create a new C# abstract base class named Specification and enter the following code.
public abstract class SpecificationBase<T> : ISpecification<T> { public abstract bool IsSatisfied(T item); public Specification<T> And(Specification<T> specification) { return new AndSpecification<T>(this, specification); } public Specification<T> Or(Specification<T> specification) { return new OrSpecification<T>(this, specification); } public Specification<T> Not() { return new NotSpecification<T>(this); } }
Create classes to implement conditions
Now create concrete implementation classes of the AndSpecification, OrSpecification, and NotSpecification specifications as shown in the code listing given below.
public class AndSpecification<T> : SpecificationBase<T> { private readonly SpecificationBase<T> _leftSpecification; private readonly SpecificationBase<T> _rightSpecification; public AndSpecification(SpecificationBase<T> leftSpecification, SpecificationBase<T> rightSpecification) { _leftSpecification = leftSpecification; _rightSpecification = rightSpecification; } public override bool IsSatisfied(T item) { return _leftSpecification.IsSatisfied(item) && _rightSpecification.IsSatisfied(item); } } public class OrSpecification<T> : SpecificationBase<T> { private readonly SpecificationBase<T> _leftSpecification; private readonly SpecificationBase<T> _rightSpecification; public OrSpecification(SpecificationBase<T> leftSpecification, SpecificationBase<T> rightSpecification) { _leftSpecification = leftSpecification; _rightSpecification = rightSpecification; } public override bool IsSatisfied(T item) { return _leftSpecification.IsSatisfied(item) || _rightSpecification.IsSatisfied(item); } } public class NotSpecification<T> : SpecificationBase<T> { private readonly SpecificationBase<T> _specification; public NotSpecification(SpecificationBase<T> specification) { _specification = specification; } public override bool IsSatisfied(T item) { return !specification.IsSatisfied(item); } }
Create classes to implement specifications
Now, create two additional classes that represent concrete specifications—one for full-time status and the other for the basic specification as shown in the code snippet given below.
public class FullTimeSpecification : Specification<Employee> { public override bool IsSatisfied(Employee employee) { return employee.IsFullTime; } } public class BasicSpecification : Specification<Employee> { public override bool IsSatisfied(Employee employee) { return employee.Basic >= 5000; } }
Validate specifications against conditions
Finally, you can write the following piece of code to check if the conditions are satisfied.
Employee employee = new Employee(); employee.FirstName = "Joydip"; employee.LastName = "Kanjilal"; employee.Basic = 1000; employee.IsFullTime = true; var basicSpecification = new BasicSpecification(); var fullTimeSpecification = new FullTimeSpecification(); var compositeSpecification = new AndSpecification<Employee>(basicSpecification, fullTimeSpecification); var isSatisfied = compositeSpecification.IsSatisfied(employee); if (isSatisfied) Console.WriteLine("The conditions are satisfied..."); else Console.WriteLine("The conditions are not satisfied..."); Console.ReadLine();
Because the value of the Basic property of the employee object is 1000, when you execute the above piece of code, you’ll see the text “The conditions are not satisfied…” displayed in the console window.
The specification design pattern enables you to write code that is structured, modular, and reusable while defining the business rules of the application in a flexible way. You can use the specification design pattern in scenerios that require the validation of conditions, querying and filtering data, enforcing business rules, and evaluating complex conditions. It’s a very useful pattern to have in your toolkit.