Welcome to today’s post.
In today’s post I will be discussing another software design pattern, and this pattern is not quite as familiar as some of the more popular design patterns I have discussed. This is the Specification pattern. What might you ask is the specification pattern? The specification pattern is a behavioral pattern that provides a conditional structure to your code such that complex logical business requirements that model user behavior can be encapsulated into a reusable class. Not only can we encapsulate conditions, but we can also encapsulate setters of class objects. This has the benefit of cleaning up our conditional logic for domain-based classes, and in addition to provide encapsulation for our domain data reading and writing. The pattern is also ideally optimized for BDD based testing. I introduced some BDD unit test tools in previous posts where I used Fluent Assertions and SpecFlow that use a variation of the specification pattern to structure their unit test classes.
Explaining the Specification Pattern
As I mentioned, the purpose of the specification pattern is to define conditions of classes and the setting of properties within classes with an optimized set of interfaces.
The class diagram for the specification pattern is shown below:
The specification pattern I will show is a variation of the more complex composite pattern that is introduced by Fowler and Gamma. I have removed the logical sub-conditions and left the IsSatisfiedBy() method. I have also added a SetPropertiesFor() method that allows modification of a target entity object. Rather than using the more complex form of the pattern and fit that into the requirements of a system I have chosen to modify the pattern to suit a domain and context data that can be conditionally encapsulated and allow modification of domain data within the same pattern.
Defining Abstract Interfaces and Factory Classes for the Specification Pattern
The abstract interface that defines the conditional operation and the setter operation is shown below:
namespace BookLoan.Catalog.API.Specification
{
public interface ISpecification<T> where T: class
{
bool IsSatisfiedBy(T entity);
void SetPropertiesFor(T entity);
}
}
In our Book Loan library, I have a basic requirement to allow for the creation of books. The requirement goes like this:
- The library has recently stocked new books for loan.
- Library staff administrators are permitted to create book data.
- Library staff administrators are permitted to amend book data.
So, from the above requirement I can create a specification factory which is used to encapsulate the business rules I have defined for the Book entity.
I then define two specifications, CanSaveBook and SetSaveBook. These are classes that define when we can save a book and how we will save a book. They fulfill the purpose above of providing encapsulation of conditional logic and data modification. Let see how the factory interface looks:
using BookLoan.Models;
using BookLoan.Catalog.API.Services;
namespace BookLoan.Catalog.API.Specification
{
public interface ISpecificationFactory
{
public ISpecification<BookViewModel> CanSaveBook(IUserContextService userContext);
public ISpecification<BookViewModel> SetSaveBook(BookViewModel request, IUserContextService userContext);
}
}
The specification factory is implemented as shown:
using BookLoan.Models;
using BookLoan.Catalog.API.Services;
namespace BookLoan.Catalog.API.Specification
{
public class SpecificationFactory: ISpecificationFactory
{
public ISpecification<BookViewModel> CanSaveBook(IUserContextService userContext)
{
return new CanSaveBook(userContext);
}
public ISpecification<BookViewModel> SetSaveBook(BookViewModel request, IUserContextService userContext)
{
return new SetSaveBook(request, userContext);
}
}
}
The parameters for the above specification methods are:
- A user context class which contains populated details of the current user that is accessing the application or API services. I recently showed how to obtain and encapsulate user details in a .NET Core application.
- A domain entity such as a table that has field properties can be used for conditional logic and be modified if needed.
In the next section, I will show how to implement the specification factory within a host class.
Implementation of the Specification Factory Class
An implementation for the CanSaveBook conditional specification is shown below:
using System;
using BookLoan.Models;
using BookLoan.Catalog.API.Services;
namespace BookLoan.Catalog.API.Specification
{
public class CanSaveBook: ISpecification<BookViewModel>
{
private readonly IUserContextService _userContext;
public CanSaveBook(IUserContextService userContext)
{
_userContext = userContext;
}
public bool IsSatisfiedBy(BookViewModel entity)
{
return (_userContext.IsMemberUser() || _userContext.IsAdminUser());
}
public void SetPropertiesFor(BookViewModel entity)
{
throw new NotImplementedException();
}
}
}
As you can see, I have left the setter method as unimplemented as it is not relevant to the conditional specification.
An implementation for the SetSaveBook setter specification is shown below:
using System;
using BookLoan.Models;
using BookLoan.Catalog.API.Services;
namespace BookLoan.Catalog.API.Specification
{
public class SetSaveBook: ISpecification<BookViewModel>
{
private readonly IUserContextService _userContext;
private readonly BookViewModel _request;
public SetSaveBook(BookViewModel request, IUserContextService userContext)
{
_userContext = userContext;
_request = request;
}
public bool IsSatisfiedBy(BookViewModel entity)
{
throw new NotImplementedException();
}
public void SetPropertiesFor(BookViewModel entity)
{
_request.DateCreated = DateTime.Now;
_request.DateUpdated = DateTime.Now;
}
}
}
As you can see, I have left the getter method as unimplemented as it is not relevant to the setter specification.
Configuration and Usage of the Specification Classes in a.NET Core Application
To bind the above specifications using dependency injection we use the following code in the ConfigureServices() startup:
services.AddTransient(typeof(ISpecification<CanSaveBook>), typeof(CanSaveBook));
services.AddTransient<ISpecificationFactory, SpecificationFactory>();
Once we have setup up our specifications, developers can extend the specification by adding additional specification class implementations as needed, making it compliant with the SOLID design principles.
To use the specifications within a class we declare the dependent interfaces as shown:
public class BookService: IBookService
{
private readonly ILogger _logger;
private readonly IBookRepository _bookRepository;
private readonly IUserContextService _userContextService;
private readonly ISpecificationFactory _specificationFactory;
public BookService(
IUserContextService userContextService,
ILogger<BookService> logger,
IBookRepository bookRepository,
ISpecificationFactory specificationFactory)
{
_userContextService = userContextService;
_logger = logger;
_bookRepository = bookRepository;
_specificationFactory = specificationFactory;
}
..
}
Note that since we encapsulated all our data context dependencies into a repository, the EF Core dependency that use used to have:
readonly ApplicationDbContext _db;
Is no longer in our class. It is replaced with the repository dependency:
private readonly IBookRepository _bookRepository;
The SaveBook() method is modified to apply the above specifications as shown:
public async Task<BookViewModel> SaveBook(BookViewModel vm)
{
if (!this._specificationFactory.GetCanSaveBook(
this._userContextService).IsSatisfiedBy(vm))
throw new Exception(
"User must be at least be a Member to Create new Books.");
this.specificationFactory.SetSaveBook(vm, this._userContextService)
.SetPropertiesFor(vm);
var newRecord = await thisa_bookRepository.AddAsync(vm);
var book = await _bookRepository.GetByIdAsync(newRecord.ID);
if (book == null)
return;
return book;
}
In addition to checking the user roles, we could also have checked other conditions relating to the books data properties to decide if they are valid and then decide if the book can be saved. For example, we might only stock books that are a specific genre or exclude a particular media type such as magazines. We might even amend the save rules to modify the location based on the media type. The specification pattern can be flexibly utilized to accommodate more complex business rules.
That is all for today’s post.
I hope you found this post useful and informative.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.