Creating your Business Logic Layer: 10 Years of .Net Compressed into Weeks #5
on
This post is part of a blog series ASP.Net 10 Years On. Even though this is part of a series I have tried to make each post standalone.
In our code review we slammed the original code for not having any form of abstract data types to encapsulate the logic.
Here is some offending code from an ASPX page*:
Sub Page_Load()
Dim mobNo, tmeDate, txtkeyword, txtAnswer As String
'get variables from SMS server
mobNo = Request.Form("sender")
tmeDate = Request.Form("time")
txtAnswer = Request.Form("message")
txtkeyword = Request.Form("keyword")
'database connection settings
Dim conSMS As OleDbConnection
Dim strInsert, strConString As String
Dim cmdInsert As OleDbCommand
If Not string.IsNullOrEmpty(mobNo) And Not string.string.IsNullOrEmpty(tmeDate) And _
Not string.IsNullOrEmpty(txtAnswer) And Not string.IsNullOrEmpty(txtkeyword) Then
' assign connection string from web.config file
strConString = ConfigurationSettings.AppSettings("IncomingConnectionString")
conSMS = New OleDbConnection(strConString)
strInsert = "INSERT into contestants ( answer, mobile, timeDate, keyword ) Values ( @answer, @mobile, @timeDate, @keyword )"
conSMS.Open()
cmdInsert = New OleDbCommand(strInsert, conSMS)
cmdInsert.Parameters.Add("@answer", txtAnswer)
cmdInsert.Parameters.Add("@mobile", mobNo)
cmdInsert.Parameters.Add("@timeDate", tmeDate)
cmdInsert.Parameters.Add("@keyword", txtkeyword)
cmdInsert.ExecuteNonQuery()
conSMS.Close()
End If
End Sub
* Note: for readers following the series, I've added some validation to this page as it better illustrates the example.
What's wrong with this code?
There is no encapsulation of logic in this code snippet. The code is in a line by line transactional style with no cohesion. We have code to fetch parameters from the form collection mixed in with logic to validate, create, and save a competition entrant to the database.
Code Complete 2 describes an abstract data type (ADT):
An abstract datatype is a collection of data and operations that work on that data.
Abstract data types form the foundation of the concept of classes. We should aim to implement each ADT as its own class.
Code Complete 2 also highlights that:
A key to being an effective programmer is maximising the portion of a program that you can safely ignore while working on any one section of code. Classes are the primary tool for accomplishing that objective.
If we were to apply this principle to our first code example we could break this down into an ADT/class named Contestant. This class would contain the data for a competition entrant including any validation logic.
If we model our contestant ADT into a class the code would look as follows:
public class Contestant
{
public string Answer { get; set; }
public string Number { get; set; }
public DateTime TimeDate { get; set; }
public string Keyword { get; set; }
public bool IsValid
{
get
{
// answer and number are required
return (!string.IsNullOrEmpty(Answer) && !string.IsNullOrEmpty(Number));
}
}
public int Insert()
{
if (IsValid) {
SqlConnection conSMS = new SqlConnection();
string strInsert = null;
string strConString = null;
SqlCommand cmdInsert = new SqlCommand();
strConString = GetConnectionString();
conSMS = new SqlConnection(strConString);
strInsert = "INSERT into contestants ( answer, mobile, timeDate, keyword ) Values ( @answer, @mobile, @timeDate, @keyword )";
conSMS.Open();
cmdInsert = new SqlCommand(strInsert, conSMS);
cmdInsert.Parameters.Add("@answer", Answer);
cmdInsert.Parameters.Add("@mobile", Number);
cmdInsert.Parameters.Add("@timeDate", TimeDate);
cmdInsert.Parameters.Add("@keyword", Keyword);
cmdInsert.ExecuteNonQuery();
conSMS.Close();
}
}
}
This new class hides the complexity of a contestant. Revisiting our first example the code in the .ASPX page would now look as follows:
Sub Page_Load()
Dim contestant As New Contestant()
contestant.Number = Request.Form("sender")
contestant.TimeDate = Request.Form("time")
contestant.Answer = Request.Form("message")
contestant.Keyword = Request.Form("keyword")
contestant.Insert()
End Sub
This is a big improvement from where we started. But we can do better.
The Single Responsibility Principle
The Single Responsibility Principle (SRP) states that:
There should never be more than one reason for a class to change.
A "reason" is also known as a responsibility. A class should have a single responsibility. The Contestant class we created actually has two responsibilities:
- Represents the business logic for this ADT.
- Responsible for accessing the database and saving the record.
We could improve upon our previous class by pulling out the specific data access code into its own class. This means our Contestant class doesn't have to know about how the data access works and we can make changes independently to the two classes. Here is how it would look:
public class Contestant
{
public string Answer { get; set; }
public string Number { get; set; }
public DateTime TimeDate { get; set; }
public string Keyword { get; set; }
public bool IsValid
{
get
{
// answer and number are required
return (!string.IsNullOrEmpty(Answer) && !string.IsNullOrEmpty(Number));
}
}
public int Insert()
{
if (IsValid)
{
EntrantDataStore.Insert(Answer, Number, TimeDate, Keyword);
}
}
}
// The data access class
public class EntrantDataStore
{
public static void Insert(string answer, string number, string timeDate, string keyword)
{
SqlConnection conSMS = new SqlConnection();
string strInsert = null;
string strConString = null;
SqlCommand cmdInsert = new SqlCommand();
strConString = GetConnectionString();
conSMS = new SqlConnection(strConString);
strInsert = "INSERT into contestants ( answer, mobile, timeDate, keyword ) Values ( @answer, @mobile, @timeDate, @keyword )";
conSMS.Open();
cmdInsert = new SqlCommand(strInsert, conSMS);
cmdInsert.Parameters.Add("@answer", answer);
cmdInsert.Parameters.Add("@mobile", number);
cmdInsert.Parameters.Add("@timeDate", timeDate);
cmdInsert.Parameters.Add("@keyword", keyword);
cmdInsert.ExecuteNonQuery();
conSMS.Close();
}
}
This is an improvement, but technically speaking this class still has two responsibilities of representing the basic business logic and co-ordinating saving to the database. In the code that follows any reference of saving has been removed. We of course need to save the entity to a database at some stage but we will revisit this when we design a data access layer. Below is what we are left with and all we now concern ourselves with is the business logic of our domain:
public class Contestant
{
public string Answer { get; set; }
public string Number { get; set; }
public DateTime TimeDate { get; set; }
public string Keyword { get; set; }
public bool IsValid
{
get
{
// answer and number are required
return (!string.IsNullOrEmpty(Answer) && !string.IsNullOrEmpty(Number));
}
}
}
Begin with a Test
Now that we've established the basic principles of how we plan to structure the business logic within the system it's time to turn our focus to ensuring our business logic is correct, and remains correct during future changes.
Let's begin with a simple test to get the process started. Here is the first test that ensures the validation logic is correct:
[TestMethod]
public void EntrantIsValid()
{
// Arrange
Entrant entrant = new Entrant();
entrant.Answer = "A";
// Act
bool isValid = entrant.IsValid;
// Assert
Assert.IsTrue(isValid);
}
We just want to test the logic that verifies that a contestant is in a valid state. Note the class name has been changed to Entrant. This name came from describing what operations the class has in natural language:
The entrant enters a competition.
And
The entrant votes in a poll.
The name contestant didn't make sense in the second context. As a best practise all class names should be a noun and describe the classes central purpose. Trying to read out your chosen class name with "The" prefixed is a good test. (The) Entrant class sounds ok, (The) SaveCustomers class not so great. See the following MSDN link for more information about class naming.
We need to create the new class and write just enough code to make the test pass:
public class Entrant
{
public string Answer { get; set; }
public bool IsValid
{
get
{
return (!string.IsNullOrEmpty(Answer));
}
}
public Entrant()
{
this.EntryDate = DateTime.Now;
}
}
We continue to flesh out the test until all the functionality required for the validation has been covered:
[TestMethod]
public void EntrantIsValid()
{
// Arrange
Entrant entrant = new Entrant();
entrant.Answer = "A";
entrant.CompetitionKey = "WINPRIZE";
entrant.Source = EntrantSource.Sms;
entrant.Contact = new SmsContact() { Contact = "12341234" };
// Act
bool isValid = entrant.IsValid;
// Assert
Assert.IsTrue(isValid);
}
The Entrant class now looks as below:
public class Entrant
{
public string Answer { get; set; }
public string CompetitionKey { get; set; }
public EntrantContact Contact { get; set; }
public DateTime EntryDate { get; set; }
public EntrantSource Source { get; set; }
public bool IsValid
{
get
{
return (!string.IsNullOrEmpty(Answer) &&
!string.IsNullOrEmpty(CompetitionKey) &&
EntryDate != DateTime.MinValue &&
Source != EntrantSource.NotSet &&
Contact.IsValid);
}
}
public Entrant()
{
this.EntryDate = DateTime.Now;
}
}
Note that the property names have been changed from the first attempt to better state their intent. For example we renamed TimeDate (TimeDate of what?) to EntryDate and Number (what number?) became Contact. We also used nouns for property names. See the MSDN property naming guidelines for further reading.
To make our abstraction cleaner and more structured we've made use of enums where there is a static list of possible options for a property. Entrant.Source indicates how the user entered the competition e.g via Sms, Email or from an App:
public enum EntrantSource : int
{
NotSet = 0,
Sms = 1,
Email = 2,
App = 3
}
You may have noticed we have introduced our own type called EntrantContact (see the property Entrant.Contact). Our requirements state users can enter via Sms, Email or via an App. The method in which Apps can enter is still unclear, it could be via a Twitter handle, Facebook ID or so on. Assuming we wanted to validate a mobile phone number or email address we would have to add this to our IsValid property. Here's how it might look:
public bool IsValid
{
get
{
bool isValid = (!string.IsNullOrEmpty(Answer) &&
!string.IsNullOrEmpty(CompetitionKey) &&
EntryDate != DateTime.MinValue &&
Source != EntrantSource.NotSet);
bool contactIsValid = false;
// we assume Contact to be a string value in this context.
switch (Source)
{
case EntrantSource.Sms:
contactIsValid = new Regex(@"^REGEX TO VALIDTE PHONE NUMBER$").IsMatch(Contact);
break;
case EntrantSource.Email:
contactIsValid = new Regex(@"^REGEX TO VALIDATE EMAIL$").IsMatch(Contact);
break;
default:
break;
}
return isValid && contactIsValid;
}
}
The code smell in this example is the switch statement. Visualise how this code would look if we had 10 possible Sources? If we consider SRP for a moment we may also conclude that this now has three reasons to change: validating the entrant, the phone number format and the email address.
The book Refactoring: Improving the Design of Existing Code suggests:
Most times you see a switch statement you should consider polymorphism.
To refactor this code using polymorphism we first need to represent an entrants contact details in its base form. Here is the class:
public abstract class EntrantContact
{
public string Contact { get; set; }
public abstract EntrantContactType ContactType { get; }
public abstract bool IsValid { get; }
}
We can now create specific classes that inherit from EntrantContact that will contain the specific validation logic. Here is how the SMS Contact looks:
public class SmsContact : EntrantContact
{
public override EntrantContactType ContactType
{
get
{
return EntrantContactType.Sms;
}
}
public override bool IsValid
{
get
{
return !string.IsNullOrEmpty(Contact) && ValidateNumberFormat(Contact);
}
}
///
/// Indicates whether the number is 11 digits in length
///
///The contact number
///A value indicating whether the number format is valid
private static bool ValidateNumberFormat(string value)
{
return new Regex(@"^\d{11}$").IsMatch(RemoveCountryPrefix(value));
}
///
/// Returns the phone number with the +44 UK area code replaced with 0
///
///The contact number
/// The contact number with the prefix removed
private static string RemoveCountryPrefix(string value)
{
return value.Replace("+44", "0");
}
}
This makes it possible to work with this class in isolation and write specific unit tests such as:
///
/// A test that verifies a number must be all digits and 11 digits in length to be valid.
///
[TestMethod()]
public void SmsContactIsValidWithAllDigitsNumber()
{
// Arrange
SmsContact smsContact = new SmsContact();
smsContact.Contact = "02345612345";
// Act
bool isValid = smsContact.IsValid;
// Assert
Assert.IsTrue(isValid);
}
///
/// A test that verifies a number with the area code prefix must originate from the UK and be 11 digits in length to be valid.
///
[TestMethod()]
public void SmsContactIsValidWithCountryCodePrefixNumber()
{
// Arrange
SmsContact smsContact = new SmsContact();
smsContact.Contact = "+442345612345";
// Act
bool isValid = smsContact.IsValid;
// Assert
Assert.IsTrue(isValid);
}
The end product of all this work is that the Entrant class only needs to know about EntrantContact:
public class Entrant
{
// other properties removed for brevity...
public EntrantContact Contact { get; set; }
}
The desired type of contact entity can be initialised and assigned as follows:
Entrant entrant = new Entrant();
// For Sms
entrant.Contact = new SmsContact() { Contact = "12341234" };
// Or an Email contact
entrant.Contact = new EmailContact() { Contact = "dev@example.com" };
// Or a Twitter contact
entrant.Contact = new TwitterContact() { Contact = "@bradoncode" };
Each type of contact class will be responsible for validating itself keeping our Entrant class very clean and reducing its reasons to change as per the SRP. This method of design is known as domain-driven design or DDD for short.
Further Reading
We covered a lot of ground in this post and if these concepts are new to you then it may be there are some gaps in the explanations of the concepts covered. The following additional material may help to fill those gaps. The books in particular have had a big impact on my progression as a developer:
Code Complete 2nd Edition
Code Complete serves very well as a continual point of reference for coding quality and standards. Code Complete is a must read for any coding standard conscious developer.
Don't just take my word for it. At the time of writing Code Complete was top of the list in this StackOverflow answer to the question "What is the single most influential book every programmer should read?".
Refactoring: Improving the Design of Existing Code
Does as the title suggests. If you are overseeing code created by other developers or have to improve a legacy system then this is the book for you. It begins with a simple example of a code base that has “code smells” and then looks to refactor away these issues. It continues to list some best practices and methodologies to refactoring and is a great asset for a developer’s toolbox.
TDD Master Class Screen Casts : Roy Osherove [Screencast]
If you are new to TDD/Unit Tests then I recommend watching the TDD Master Class production from Tekpub.com. The production is a series of videos from a week long training seminar presented by Roy Osherove.
In addition to the videos take a look at Roy’s blog post on the string calculator TDD kata.
If you are interested you can see a video of my efforts of the string calculator kata in JavaScript.
Get the Source Code
The code for this blog post is available on Github. <p class="quote">In the next post we will take this business logic further and code out the competition logic. See the RSS subscription links at the bottom of the page to follow this series.</p>