Introduction
Following on from the introduction to Specifications in part one, this article shows a simple implementation of the design pattern.
Background
The sample code is based on the Chemical Packaging example in Eric Evans' book, 'Domain Driven Design'.
Using the code
The attached sample project includes Unit Tests in the HandRollesTests.vb file. These tests illustrate how to use the Specification. The project uses NUnit for Unit Testing. If you do not have NUnit installed, you should remove HandRolledTests.vb from the project and remove the reference to nunit.framework.
Building a simple Specification
In part one of this series of articles, we looked at the Specification Design Pattern and why you might want to use it. In this article, we'll implement a working example.
The specification we build here will be created for the Container object that we discussed in the first article. In part 3 of this series, we'll look at how to go a step further and create a generic specification that will work with any object.
So, let's get started. You'll recall from part one that we're dealing with drums of chemicals. Our challenge is to load these drums into containers. Each drum can have requirements for the type of container that can hold it. Our drum class is coded as follows:
Public Class Drum
Private _chemical As String
Private _size As Int32
Private _requiredContainer As ContainerSpecification
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 ReadOnly Property RequiredContainer() As ContainerSpecification
Get
Return _requiredContainer
End Get
End Property
Public Sub New(ByVal chemical As String, ByVal size As Int32, _
ByVal requiredContainer As ContainerSpecification)
_chemical = chemical
_size = size
_requiredContainer = requiredContainer
End Sub
End Class
There's virtually no logic in the drum class, just a string and integer for the name and quantity of the chemical, and a ContainerSpecification
object that defines the type of container that the chemical requires. It is this ContainerSpecification
object that we're concerned with in this article.
Before we get to that, we also need a Container
class. The Container is the thing that will be examined by the specification.
A container can hold multiple chemical drums, and the specification will provide a way of checking that each drum is in the right kind of container.
For the purposes of this example, I'm using a flags enum which lists the attributes of a container that a drum might require. Note that the enum values increase in powers of 2 (each is double the previous entry). This allows values to be combined. E.g. Armored is 1, Airtight is 4, Armored AND Airtight is 5.
<Flags()> _
Public Enum ContainerFeature
None = 0
Armored = 1
Ventilated = 2
Airtight = 4
LeadLined = 8
End Enum
The Container
class has three properties - Features
, Capacity
, and Drums
.
Features
uses the ContainerFeature
enum described above. Capacity
is the overall capacity of the container, the combined capacity of the drums can't exceed this value. The Drums
property is simply a list of the drums that have been added to the container.
Public Class Container
Private _features As ContainerFeature
Private _capacity As Int32
Private _drums As List(Of Drum)
Public ReadOnly Property Features() As ContainerFeature
Get
Return _features
End Get
End Property
Public ReadOnly Property capacity() As Int32
Get
Return _capacity
End Get
End Property
Public ReadOnly Property Drums() As List(Of Drum)
Get
Return _drums
End Get
End Property
Public Sub AddDrum(ByVal drum As Drum)
_drums.Add(drum)
End Sub
Public Function RemainingSpace() As Int32
Dim usedSpace As Int32 = 0
For Each drum As Drum In _drums
usedSpace += drum.Size
Next
Return _capacity - usedSpace
End Function
Public Function HasSpaceFor(ByVal drum As Drum) As Boolean
Return RemainingSpace() >= drum.Size
End Function
Public Function CanAccommodate(ByVal drum As Drum) As Boolean
Return hasSpaceFor(drum) And drum.RequiredContainer.IsSatisfiedBy(Me)
End Function
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
Public Sub New(ByVal capacity As Int32, _
ByVal features As ContainerFeature)
_capacity = capacity
_features = features
_drums = New List(Of Drum)
End Sub
End Class
The class contains methods for adding a drum (AddDrum
), checking the remaining space in the container (RemainingSpace
), checking if a drum can fit in the container (HasSpaceFor
), checking if the container is suitable for a particular drum (CanAccommodate
), and a function that looks at all drums in a container to ensure that the container is safely packaged (IsSafelyPacked
). Each of these functions is fairly simple, as can be seen from the listing above.
So, we have drums, and we have containers to put the drums in. It's time to create a specification that links these two objects together. The good news is that this is a very simple task.
We know that the Drum
class has a property of type ContainerSpecification
, so we need to create that class. We also know that the only behaviour this class provides is a method called IsSatisfiedBy
which accepts a container and returns a boolean. So, we already know what the ContainerSpecification
object should look like:
Public MustInherit Class ContainerSpecification
Public MustOverride Function IsSatisfiedBy(ByVal candidate As Container) As Boolean
End Class
Note that we declare the class using MustInherit
, and we require the IsSatisfiedBy
function to be overridden. ContainerSpecification
is an abstract base class. We need to implement concrete classes that inherit from ContainerSpecification
and which check for the actual desired attributes. Here's an example of such a concrete class:
Public Class IsArmored
Inherits ContainerSpecification
Public Overrides Function IsSatisfiedBy(ByVal candidate As Container) As Boolean
Return CType(candidate.Features And ContainerFeature.Armored, Boolean)
End Function
End Class
And, that's all there is to it. We've defined an abstract ContainerSpecification
class, and inherited from it to define a specification that can check if a container is Armored. We can use this in our code as follows:
Dim tntDrum As New Drum("TNT", 3000, New IsArmored)
We pass a new instance of the IsArmored
specification to the constructor of our drum, and in doing so, we ensure that TNT can only be stored in an Armored container. Now, a container can check itself to make sure it’s safely packed.
If we need to check that a container is ventilated, we just create another specification that inherits from ContainerSpecification
.
Public Class IsVentilated
Inherits ContainerSpecification
Public Overrides Function IsSatisfiedBy(ByVal candidate As Container) As Boolean
Return CType(candidate.Features And ContainerFeature.Ventilated, Boolean)
End Function
End Class
The use of a flags enum to represent features of a container is something specific to this example, it is not important to the overall idea of specifications. The IsSatisfiedBy
method could just as easily be looking at any other property or method.
If you've come this far, you may find yourself wondering about how specifications can be combined. What if we needed a container that is both Armored AND LeadLined, or what if a chemical could be in a container that was either Armored OR LeadLined, but not Ventilated?
In part 3 of this series, we'll look at an elegant way of combining individual specifications into these kinds of composite statements. For now, we'll look at a less elegant solution.
If we want to create a specification that tests if a container is either Armored OR LeadLined, we can do so in exactly the same way we created the IsArmored
or IsVentilated
specifications. The only difference lies in the way we implement the IsSatisfiedBy
method.
Public Class IsArmoredOrLeadLined
Inherits ContainerSpecification
Public Overrides Function IsSatisfiedBy(ByVal candidate As Container) As Boolean
Dim IsArmoredSpec As New IsArmored
Dim IsLeadLinedSpec As New IsLeadLined
Return IsArmoredSpec.IsSatisfiedBy(candidate) Or _
IsLeadLinedSpec.IsSatisfiedBy(candidate)
End Function
End Class
We simply use the IsArmored
and IsLeadLined
specs, and we combine the result from each to create a new spec called IsArmoredOrLeadLined
.
The result is isn't a bad solution. It's readable, easy to understand, but it could be better. We shouldn't need to create a whole new specification just to combine two others.
In the next part of this series, we'll create a generic specification that solves the two major problems with this solution. It will be generic so it can be used with any kind of object, not just containers. It will also allow specifications to be combined into more complex specifications without the need to create a new class.
Once you start working with specifications, you will find that they provide a powerful vocabulary for capturing real world requirements. The Specification Design Pattern allows you to create code that very closely matches the language used in the real world domain.