![]() |
Development Lifecycle »
Design and Architecture »
Design Patterns
Intermediate
License: The Code Project Open License (CPOL)
A Simple Specification in VB.NETBy daltonrPart two of a four part series of articles on the Specification Design Pattern. |
VB8.0, VB9.0, .NET (.NET2.0, .NET3.0, .NET3.5), Architect, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
Following on from the introduction to Specifications in part one, this article shows a simple implementation of the design pattern.
The sample code is based on the Chemical Packaging example in Eric Evans' book, 'Domain Driven Design'.
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.
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.
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 5 Apr 2009 Editor: Smitha Vijayan |
Copyright 2009 by daltonr Everything else Copyright © CodeProject, 1999-2010 Web18 | Advertise on the Code Project |