Creating the Business Logic Part 2: 10 Years of .Net Compressed into Weeks #6
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 the previous post we created a business logic layer. This post builds on these foundations as we begin to add more complex logic to our business layer. Since we are using a DDD approach to drive our design we will now use the correct terminology of domain model rather than business logic layer.
The Competition Requirements
In order to start coding the logic for managing a competition let's remind ourselves of what we started with. To save you the potential eye strain and heartache of having to read the original code (below) the requirements for a competition are:
- Count the number of correct entrants that are within the closing date.
- Count the number of incorrect entrants that are within the closing date.
- Show percentage of answers for A, B, C or D (each competition is multiple choice).
- Select a winner at random from the correct entrants.
- Contact the winner advising them of the win via SMS or Email. This will depend on how the winner entered the competition.
- When creating new Competitions validate the required fields.
The original code can be viewed via the following link. Warning: dangerous code ahead, original source code.
We identified the problems with this style of coding and began to rework this into a domain model in the previous post, so we won't repeat this process and will dive straight into our domain model.
Creating the Competition Domain Entity
We begin by creating a new unit test for the Competition domain entity. Starting off simply the test covers the validation requirements:
/// Tests that the competition is valid.
[TestMethod]
public void CompetitionIsValid()
{
// Arrange
Competition competition = new Competition();
competition.Question = "Who is Luke Skywalker's father?";
competition.ClosingDate = DateTime.Now.AddMonths(1);
competition.CompetitionKey = "WINPRIZE";
competition.PossibleAnswers.Add(new PossibleAnswer() { Answer = CompetitionAnswer.A, Description = "Darth Vader", IsCorrectAnswer = true });
competition.PossibleAnswers.Add(new PossibleAnswer() { Answer = CompetitionAnswer.B, Description = "Obi Wan Kenobi" });
competition.PossibleAnswers.Add(new PossibleAnswer() { Answer = CompetitionAnswer.C, Description = "George Lucas" });
competition.PossibleAnswers.Add(new PossibleAnswer() { Answer = CompetitionAnswer.D, Description = "Walt Disney" });
// Act
bool isValid = competition.IsValid;
// Assert
Assert.IsTrue(isValid);
}
Here is the Competition class with enough code to make the test pass:
public class Competition
{
public string Question { get; set; }
public PossibleAnswers PossibleAnswers { get; set; }
public string CompetitionKey { get; set; }
public DateTime CreationDate { get; private set; }
public DateTime ClosingDate { get; set; }
public bool IsValid
{
get
{
return !string.IsNullOrEmpty(Question) &&
!string.IsNullOrEmpty(CompetitionKey) &&
ClosingDate != DateTime.MinValue &&
PossibleAnswers.IsValid;
}
}
public Competition()
{
CreationDate = DateTime.Now;
PossibleAnswers = new PossibleAnswers();
}
}
Following on from the theme of the previous post note we have a PossibleAnswers entity that contains the data and validation rules for competition answers e.g. there can be only one correct answer, there must be an answer for A, B, C and D and there can be no duplicates. You can see the full code for this class in the Github branch.
Next we look at the logic for counting the correct, incorrect and answers by percentage of A, B, C and D. My first instinct was to add methods and/or readonly properties to the Competition class. This class already has several properties and I'm expecting to add some more at a later stage so I opt to create a new class to encapsulate the logic for reporting on the competition statistics.
Taken from Code Complete 2:
The number "7±2" has been found to be a number of discrete items a person can remember while performing other tasks.
With this in mind I opted to create a new class called CompetitionStatistics. I started with a unit test to drive the design:
/// Tests that the number of correct answers for the competition is 10.
[TestMethod]
public void TheNumberOfCorrectAnswersIs10()
{
// Arrange
CompetitionStatistics statistics = GetCompetitionStatisticsInstance();
// Act
int correctAnswerCount = statistics.CorrectAnswers.Count();
// Assert
Assert.AreEqual(10, correctAnswerCount);
}
As we progress with our tests we move into Mock and Stub territory. The method GetMockCompetition() returns a Competition entity in an expected state for our tests. Remember, Mocks Aren't Stubs.
The following code is used to prevent us from repeating the same setup code for the tests:
/// Gets the competition statistics instance.
private static CompetitionStatistics GetCompetitionStatisticsInstance()
{
return new CompetitionStatistics(GetMockCompetition(), GetMockEntrants());
}
A test for calculating the percentage of answers for answer A:
/// Percentages the of answers for A is 52.63 percent.
[TestMethod]
public void PercentageOfAnswersForAIsCorrect()
{
AssertAnswerPercentage(CompetitionAnswer.A, 52.63m);
}
A similar test is also added for B, C and D. Note the method AssertAnswerPercentage() has been added to avoid the duplicate code for each test. Here is the code:
private static void AssertAnswerPercentage(CompetitionAnswer answer, decimal expectedPercentage)
{
// Arrange
CompetitionStatistics statistics = GetCompetitionStatisticsInstance();
// Act
decimal percentage = statistics.GetPercentageOfEntrans(answer);
// Assert
Assert.AreEqual(expectedPercentage, decimal.Round(percentage, 2));
}
Here is the CompetitionStatistics code:
/// Competition Statistics that report on competition data.
public class CompetitionStatistics
{
private IEnumerable<Entrant> Entrants { get; set; }
public Competition Competition { get; private set; }
public IEnumerable<Entrant> CorrectAnswers
{
get
{
return ValidEntrants.Where(e => e.Answer == Competition.PossibleAnswers.CorrectAnswer.Answer);
}
}
public IEnumerable<Entrant> IncorrectAnswers
{
get
{
return ValidEntrants.Where(e => e.Answer != Competition.PossibleAnswers.CorrectAnswer.Answer);
}
}
public IEnumerable<Entrant> ValidEntrants
{
get
{
return Entrants.Where(e => e.EntryDate < Competition.ClosingDate);
}
}
public CompetitionStatistics(Competition competition, IEnumerable<Entrant> entrants)
{
Competition = competition;
Entrants = entrants;
}
public decimal GetPercentageOfEntrans(CompetitionAnswer answer)
{
return (decimal)ValidEntrants.Count(e => e.Answer == answer) / ValidEntrants.Count() * 100;
}
}
Note that in this design we are passing in a list of all the entrants via the constructor of the class. This collection could contain several (hundred?) thousand items. When designing with a data centric mentality this sort of logic would often be pushed down into the SQL layer with either a custom database view or a stored procedure to sum the rows for us.
The approach used here has scope to consume a lot of memory but we will revisit this at a later stage as we are driving ahead with our domain driven design and want to focus on the business logic and not get bogged down with data access concerns. And remember… premature optimisation is the root of all evil!
Get the Code
The full source code is on Github:
In the next post we will cover the logic of selecting a winner at random and contacting the winner. We will investigate the implementation of Domain Events.