Use cases of the versatile Specification pattern include validation, creation, and querying.
There are not many code structures that bring me joy whenever I need to write them. The first time I implemented such
code was with a lightweight ORM in Go, back when we didn’t have one. However, I used ORM for many years, and at some
point, when you rely on ORM, using
becomes inevitable. Here, you may notice terms like “predicates”, and that’s where we can find the Specification pattern.
It’s hard to find any pattern we use as Specification, yet we do not hear its name. I think the only thing harder is
to write an application without using this pattern. The Specification has many applications. We can use it for
querying, creation, or validation. We may provide a unique code that can do all this work or provide different
implementations for different use cases.
The first use case for the Specification pattern is validation. Typically, we validate data in forms, but this is at
the presentation level. Sometimes, we perform validation during creation, such as for
Value Objects. In the context of the domain layer, we
can use Specifications to validate the states of Entities and
filter them from a collection. So, validation at the domain layer has a broader meaning than for user inputs.
In the example above, there is an interface called ProductSpecification. It defines only one method, IsValid,
which expects instances of Product and returns a boolean value as a result if the Product passes validation
rules. A simple implementation of this interface is HasAtLeast, which verifies the minimum quantity of the Product.
More interesting validators are two functions, IsPlastic and IsDeliverable. We can wrap those functions with a
specific type, FunctionSpecification. This type embeds a function with the same signature as the two mentioned.
Besides that, it provides methods that respect the ProductSpecification interface. This example is a nice feature
of Go, where we can define a function as a type and attach a method to it so that it can implicitly implement some
interface. In this case, it exposes the method IsValid, which executes the embedded function.
In addition, there is also one unique Specification, AndSpecification. Such a struct helps us use an object that
implements the ProductSpecification interface but groups validation from all Specifications included.
In the code snippet above, we may find two additional Specifications. One is OrSpecification, and it, like
AndSpecification, executes all Specifications which it holds. Just, in this case, it uses the “or” algorithm
instead of “and”. The last one is NotSpecification, which negates the result of the embedded Specification.
NotSpecification can also be a functional Specification, but I did not want to complicate it too much.
I have already mentioned in this article the application of the Specification pattern as part of ORM. In many cases,
you will not need to implement Specifications for this use case, at least if you use any ORM. Excellent implementations
of Specification, in the form of predicates, I found in the Ent library from Facebook.
From that moment, I did not have a use case to write Specifications for querying. Still, when you find out that your
query for Repository on the domain level can be too complex, you need more possibilities to filter desired Entities.
Implementation can look like the example below.
In the new implementation, the ProductSpecification interface provides two methods, Query and Values. We use
them to get a query string for a particular Specification and the possible values it holds. Once again, we can see
additional Specifications, AndSpecification and OrSpecification. In this case, they join all underlying queries,
depending on the operator they present, and merge all values. It is questionable to have such Specifications on the
domain layer. As you may see from the output, Specifications provide SQL-like syntax, which delves too much into
technical details. In this case, the solution would probably be to define interfaces for different Specifications
on the domain layer and have actual implementations on the infrastructure layer. Or to restructure the code so
that Specifications hold information about field name, operation, and value. Then, have some mapper on the
infrastructure layer that can map such Specifications to an SQL query.
One simple use case for Specifications is to create a complex object that can vary a lot. In such cases, we can combine
it with the Factory pattern or use it inside a Domain Service.
In the example above, we can find a third implementation of Specification. In this scenario, ProductSpecification
supports one method, Create, which expects a Product, adapts it, and returns it back. Once again, there is
AndSpecification to apply changes defined by multiple Specifications, but there is no OrSpecification. I could
not find an actual use case for the OR algorithm during the creation of an object. Even if it is not present,
we can introduce NotSpecification, which could work with specific data types like booleans. Still, in this small
example, I could not find a good fit for it.
Specification is a pattern that we use everywhere, in many different cases. Today, it isn’t easy to provide validation
on the domain layer without the usage of Specifications. Specifications can also be used in querying objects from the
underlying storage, and today, they are part of ORM. The third usage is for creating complex instances, where we can
combine it with the Factory pattern.