Specification Design Pattern
Yazının Türkçe versiyonuna bu link ile ulaşabilirsiniz.
Specification design pattern allows us to check whether our objects meet certain requirements. Through this design pattern, we can reuse expression specifications and combine those specifications to easily question whether more complex requirements are satisfied or not.
In this article, we will talk about in which situations we can use the specification design pattern. Then we will talk about how we can create our specification classes, how we can link these classes with logical operators. Finally, we will look at how we can query data from the database via the Entity Framework using the specification classes we created.
You can access all of the codes that can be seen in the implementation section and the sample project via this Github link.
You can access the implementation of the design pattern via NuGet using the name Spectacular.
When to Use Specification Design Pattern?
Many reasons may cause us to use this design pattern. One of these is to avoid repeating the condition rules in the code. For example, you’ve written a condition check to see if a person can get a driver’s license in several different places in your code. The irony of fate, a new law has been passed and the age of license acquisition has been changed. If you were not using the specification design pattern, you would have to search through the code and change the age in the places you can find. Places that you miss will be the reason for a bug fix.
Another reason to use the specification design pattern is the situations in which the developer cannot interfere with the object. Such situations may occur when you use classes that you access from external packages or through the framework. For example, suppose you download the IdentityServer NuGet package and use the ApplicationUser class directly, and you need to check whether the user is an adult or not. In this case, since the ApplicationUser class is not a class that you can make changes to, you will not be able to add a method called IsAdult. You can encapsulate your condition statement by creating a class called IsAdultSpecification.
Another moment of need may be that there are too many specifications of the object. Let’s continue with the User example. “Is s/he an adult?”, “Is s/he married?”, “Does s/he have a child?”, “Does s/he work?”, “Does s/he do sports?”. Let’s assume that you have prepared your class to answer all of these questions. This class will continue to grow as your application evolves, that is, as new queries are required. By using the specification design pattern, you can prevent your class from being too long to read.
Specification Design Pattern Implementation
When implementing this design pattern, we can first start from the ISpecification interface. Classes that implement the interface will accept a T type object as a parameter and will be able to check whether the object has the requested qualification.
Let’s prepare a specification class that will check that the date data it receives is 21st century. When we look at this class, we can see that it is a small piece of code and it is easy to write. However, we can see that this is a large block of code to repeat without a specification class.
One of the beautiful things about the specification design pattern is that it is reusable. If we use the date of birth of a user object that we have as a parameter, we can learn whether the person was born in the 21st century. In the example below, we obtain a random movie from the movie service and check whether this movie is published in the 21st century.
Querying Data via Specification Classes
We have seen the basic implementation and usage scenario of the specification design pattern. After this point, we will see how we can query data from external sources such as database by using the specification design pattern.
Let’s first take a look at our example model. We have a simple user class where we keep height and age information.
Now let’s start by preparing an abstract class from which the specification classes will be derived. This class will implement our ISpecification<T> interface. By doing so, it will also allow us to access query criteria from the Expression type.
We can now prepare the YoungerThanSpecification class, which will allow us to find people younger than X using the ExpressionSpecification class.
We can write extension methods to make the use of the specification class pleasing to the eye. Below we can see three different extension methods. The first one will allow us to check if single object meets a certain requirement or not. We will use the second method to filter out collection in memory, such as lists or arrays. We will use the last one when filtering data from external data sources. If we do not create our queries via IQueryable, Entity Framework will first try to store the data into memory and then filter it. This is a situation we would never want.
We can meet our need to filter data through the extension methods we designed. As an example scenario, we can consider an endpoint as below, where we filter our users according to age criteria.
Sometimes we restrict access to DbContext object belonging to Entity Framework and have special IRepository interfaces. In this case, merging specification criterias cannot be done on IQueryable object as above. We can take our first step with the IUserRepository interface for the solution we will develop.
You may have noticed that we accept a single specification in our repository interface. In this case we will need a dynamic specification class where we combine multiple specification criterias. Let’s start by creating this class. What our new class will do is actually save our new expression object created with the combination.
Let’s continue our process by creating the signature of the method that takes two ExpressionSpecification objects to combine their criterias.
By implementing the IExpressionSpecificationOperator interface, we can create a class as follows, where we have prepared the And operation. We will be using this logical operator to satisfy a request such as “I want our object to have X and Y features”.
We can now combine our specification classes and apply our business rules one after the other in a way that people can read more easily.
You can reach sample codes via this link. Considering the criterias we combined in the sample project, you can follow the queries that are executed in the database on the console screen.
I hope it has been a useful post. You can click this link to browse my other articles.