Introduction
This is the first in a four part series of articles on the Specification Design Pattern.
Background
The Specification Design Pattern was created by Eric Evans. He provides a description of the pattern in his book 'Domain Driven Design'. This series of articles shows how to implement the pattern using VB.NET.
The Specification Design Pattern
The Specification Design Pattern uses an object to represent a statement about another object. Once a specification has been implemented, it can be used to check whether a candidate object conforms to the specification or not.
A very simple example would be a specification of what it means for an invoice to be overdue.
Dim isOverdue as New OverdueInvoiceSpec
If isOverdue.IsSatisfiedBy(invoice) then
End If
The IsSatisfiedBy
method contains the logic for figuring out if the invoice is overdue. This might be as simple as checking the date of the invoice, or it may be more complex. The method may have to examine the payment history of the customer that the invoice applies to.
The job of the specification may be even simpler. In some cases, the candidate object may actually have a method or property that directly answers the very question that the specification is asking. For example, the invoice might provide an IsOverdue
method. In that case, the IsSatisfiedBy
method can we written as follows:
Public Function IsSatisfiedBy(ByVal candidate As Invoice) As Boolean
Return candidate.IsOverdue
End Function
Now, you might wonder why we'd bother with a specification if the target object already has a method that answers the question. The reason is that it can be useful to encapsulate a desired state of an object separate from the object itself. This allows us to think about the state of an object in an abstract way, not concerned with a specific instance.
We can pass the desired state as a parameter to a method, or attach it as a property of a class. The method or class can then use the specification to check any candidate objects that it wants.
A good example of this can be found in Eric Evans' excellent book, “Domain Driven Design”. Evans uses an example of a chemical warehouse system that packs chemicals into different types of containers. Containers can be Armored, Ventilated, both or neither. Each chemical has specific requirements for the type of container that can be used to hold it.
The container object can have simple methods or properties that indicate whether it is Armored or Ventilated, but how do we define the type of container that's required by a chemical?
Let's look at the class that we'll use to represent a drum of chemicals. A drum will use a string to store the name of the chemical; and for the quantity, we'll use an integer.
Public Class Drum
Private _chemical As String
Private _size As Int32
Public ReadOnly Property Chemical() As String
Get
Return _chemical
End Get
End Property
Public ReadOnly Property Size() As Int32
Get
Return _size
End Get
End Property
Public Sub New(ByVal chemical As String, ByVal size As Int32)
_chemical = chemical
_size = size
End Sub
End Class
We could try to implement a method in this class called IsSuitableContainer
which accepts a container. The method could then look at the type of chemical in the drum and decide if the provided container is suitable.
The problem with this approach is that our chemical drum class would need to be aware of every kind of chemical that we work with in the future, and it would need to know the packaging requirements for each.
The IsSuitableContainer
method could end up looking something like this:
Public Function IsSuitableContainer(ByVal candidate As Container) As Boolean
If _chemical = "TNT" then return candidate.IsArmored
If _chemical = "Uranium" then return candidate.IsArmored
If _ chemical = "Sulpher" then return candidate.IsVentilated
...
...
End Function
That's not a good solution. A better option would be to add a property to the Drum
class that specifies the type of container that it requires. In other words, we need to attach a container specification to our chemical drum as a property.
This means creating a class to represent a container specification. We'll look at how to implement the specification later, but for now, let's add a property to our chemical Drum
class which will hold the specification.
Private _requiredContainer As ContainerSpecification
Public ReadOnly Property RequiredContainer() As ContainerSpecification
Get
Return _requiredContainer
End Get
End Property
And, let's modify the constructor so that the specification can be set when the chemical drum is being created.
Public Sub New(ByVal chemical As String, ByVal size As Int32, _
ByVal requiredContainer As ContainerSpecification)
_chemical = chemical
_size = size
_requiredContainer = requiredContainer
End Sub
Creating a specification is just a matter of inheriting from ContainerSpecification
. We'll look at this in more detail in part 2 of this series. For now, it's enough to know that we're creating a class called IsArmored
which can check if a container is Armored, and because it inherits from ContainerSpecification
, it can be passed to the constructor of a chemical drum.
Public Class IsArmored
Inherits ContainerSpecification
Public Overrides Function IsSatisfiedBy(ByVal candidate As Container) As Boolean
Return candidate.IsArmored()
End Function
End Class
Let's create a drum to see the specification in action.
Dim tntDrum As New Drum("TNT", 3000, New IsArmored)
We can now check any container object against the drum's required container to see if it is suitable for the chemical.
If tntDrum.RequiredContainer.IsSatisfiedBy(container) then
End If
We can implement an IsSafelyPacked
method on a container that cycles through all the chemical drums in the container, checking that the container is suitable for each drum.
Public Function isSafelyPacked() As Boolean
Dim blnIsSafe As Boolean = True
For Each drum As Drum In _drums
blnIsSafe = blnIsSafe And drum.RequiredContainer.IsSatisfiedBy(Me)
Next
Return blnIsSafe
End Function
If the container fails to pass the requiredContainer
specification of any drum, then the container is not safely packed.
There may be other attributes that we haven’t even thought of yet. IsWatertight
, IsAirtight
, IsVacuumSealed
, IsLeadLined
etc. As long as the new specifications inherit from ContainerSpecification
, we can pass them to the constructor of our chemical drum and indicate that the drum can only be placed in a container of that type.
In part 2 of this series, we'll implement ContainerSpecification
and show how to inherit from it to create concrete container specifications. In part 3, we'll go a step further and create a generic specification that can be used as the basis for specifications for any kind of object.