Adaptive sample: Sprint 2 - Adaptive sample - Adaptive Code via C# Agile coding with design patterns and SOLID principles (2014)

Adaptive Code via C# Agile coding with design patterns and SOLID principles (2014)

Part III: Adaptive sample

Chapter 12. Adaptive sample: Sprint 2

In this chapter, you will

Image Observe the team’s second sprint planning session.

Image Follow the implementation and evolution of the next user stories.

Image Observe the team’s second sprint demo and retrospective.

In this chapter, the Trey Research team continues to implement user stories for the Proseware project. The direction has changed slightly due to the feedback received from the client at the sprint demonstration during sprint 1. The team has set the following sprint goal:

To add optional markdown-formatted text to conversations, to filter message content so that it is appropriate, and to ensure that 300 users can be served concurrently

This sprint goal incorporates all of the stories that the team has committed to completing during the sprint. As usual, the team will conclude the sprint with a demonstration to the client—to elicit feedback and to inform the client of progress—and a sprint retrospective in which the team will address any problems that the sprint presented and to acknowledge the good work that the team did during the sprint.

First, though, the team begins the sprint with a planning session.

Planning

The second planning meeting for the project allows the members of the team to discuss the user stories that they have committed to in their sprint goal. The second sprint includes the following user stories:

Image I want to send markdown that will be correctly formatted.

Image I want to filter message content so that it is appropriate.

Image I want to serve hundreds of users concurrently.

With the whole team assembled in a meeting room, the discussion begins:

PETRA: We’ve got a new story on the backlog as a result of the feedback from the sprint demo in the last sprint.

STEVE: Instead of implementing read-only conversations, the client wants us to prioritize the content filter.

DIANNE: So we should estimate this story now.

STEVE: Yes, if we get an estimate for this story we can understand how much capacity we have for this sprint.

DIANNE: Okay, so on the count of three, let’s show our estimates.

Everyone shuffles their cards before showing them.

Image

STEVE: Wow, we have a bit of variety here. Tom, could you explain your three?

TOM: I chose three mainly because I can automate the testing for this fairly easily. Providing a text message with a disallowed word in it and asserting that you could not post the message is simple enough. If there’s more technical complexity to the implementation, I’d be happy to increase the estimate.

STEVE: David, would you like to explain your eight? Then I’ll explain mine.

DAVID: Yes, I think this is difficult because we need to add another table to data storage for the disallowed words. This is going to take some time to implement.

STEVE: Yes, I thought that, too. I also considered that we don’t want to limit disallowed words to the messages users write in conversations—we should also include the names of the rooms. In fact, any time we take input from the user we should submit it to the content filter.

DIANNE: How about instead of implementing a data-driven content filter right away, we just simplify it and hardcode the blocked list of words?

STEVE: Okay, I think that’s a good idea. Later we can add stories to target administration of the content filter.

PETRA: Great—shall we re-estimate or take the five?

TOM: I’m happy with a five.

STEVE: Yes, a five seems fair.

DAVID: I agree, five it is.

PETRA: Excellent, thanks everyone. Let’s make sure we hit all our goals this sprint so we can show the client a great demo this week.

Everyone files out of the meeting room ready to get to work.

“I want to send markdown that will be correctly formatted.”

Before he starts this story, David asks Steve about parsing markdown.

DAVID: I assume I should be using a third-party library for parsing markdown and transforming it into HTML, but I’m not sure which library to use.

STEVE: Okay, hold on. I think Dianne has some experience in this area—we’ll ask her.

Steve beckons Dianne over to quiz her about markdown libraries.

STEVE: Dianne, you’ve used some markdown libraries before, right? Which one did you prefer?

DIANNE: I evaluated a few previously. Try MarkdownDeep—it seemed simple enough to use. There’s a NuGet package for it, too.

STEVE: Thanks, Dianne. David, try MarkdownDeep. Also, make sure you create a new class library project for any classes that depend on this library.

DAVID: Okay—thanks.

David sets to work on implementing the markdown transform for the application. After a few hours, he is ready to show Steve and Dianne his work. He asks them to peer review what he has done.

DAVID: I wondered where the best place to put the implementation of the markdown transform was. I knew I wanted to intercept the room message text by implementing a decorator for an existing interface. I thought that if I implemented it on the AddMessageToRoom method on the IMessageRepository interface we could save on processing reads. If we just transform the markdown to HTML as the message is saved, we don’t need to worry about it again.

DIANNE: That would save us from transforming markdown to HTML on every read, but it wouldn’t really work.

DAVID: Yes, I realized that we wouldn’t be able to edit messages if we did that. I know that we don’t have that feature yet, but I thought we might in the future and didn’t want to artificially prevent users from editing.

STEVE: Good—we will almost certainly want that feature in the future, so it’s probably correct to perform the transform when reading, not writing.

DAVID: The other question I had was about client-side or server-side transforms. I’ve kept it server-side at the moment, but I wondered whether we might prefer doing it client-side in the browser.

DIANNE: Perhaps we could use that in the future for a side-by-side preview of the markdown and HTML as the user is typing a message.

STEVE: Great idea, Dianne—I’ll make a note of that for the demo and find out what the client thinks.

DAVID: In the end, I implemented the transform as a decorator on the IRoomViewModelReader interface. This is because markdown and HTML are user interface concerns and the IRoomViewModelReader is a UI contract. The other option was decorating theIRoomRepository and IMessageRepository—but these are data contracts. Still, despite this, I’m not entirely happy with it at the moment, but here it is.

David shows Steve and Dianne the code for this markdown decorator, as shown in Listing 12-1.

LISTING 12-1 The markdown decorator transforms user-entered markdown to HTML.


public class RoomViewModelReaderMarkdownDecorator : IRoomViewModelReader
{
public RoomViewModelReaderMarkdownDecorator(
IRoomViewModelReader @delegate,
Markdown markdown)
{
this.@delegate = @delegate;
this.markdown = markdown;
}

public IEnumerable<RoomViewModel> GetAllRooms()
{
return @delegate.GetAllRooms();
}

public IEnumerable<MessageViewModel> GetRoomMessages(int roomID)
{
var roomMessages = @delegate.GetRoomMessages(roomID);

foreach(var viewModel in roomMessages)
{
viewModel.Text = markdown.Transform(viewModel.Text);
}

return roomMessages;
}

private readonly IRoomViewModelReader @delegate;
private readonly Markdown markdown;
}


STEVE: I like it—it looks good to me. What weren’t you sure about?

DAVID: Two things, really. The first is that we’re dependent directly on the Markdown class from MarkdownDeep. Should this not be placed behind its own interface?

DIANNE: I think injecting that class as a dependency of this decorator is sufficient. The class is small enough to be replaced if we need to use a different library.

DAVID: Okay, that’s good. It also allowed me to write a simple unit test with a test case for each expected transform. Here’s what I have so far.

David opens the file containing his markdown unit tests, as shown in Listing 12-2.

LISTING 12-2 The unit tests for the markdown transform decorator.


[TestFixture]
public class MarkdownTests
{
[Test]
[TestCase(
"This message has only paragraph markdown...",
"<p>This message has only paragraph markdown...</p>\n")]
[TestCase(
"This message has *some emphasized* markdown...",
"<p>This message has <em>some emphasized</em> markdown...</p>\n")]
[TestCase(
"This message has **some strongly emphasized** markdown...",
"<p>This message has <strong>some strongly emphasized</strong>
markdown...</p>\n")]
public void MessageTextIsAsExpectedAfterMarkdownTransform(string markdownText,
string expectedText)
{
message1.Text = markdownText;
var markdownDecorator = new
RoomViewModelReaderMarkdownDecorator(mockRoomViewModelReader.Object, markdown);

var roomMessages = markdownDecorator.GetRoomMessages(12345);

var actualMessage = roomMessages.FirstOrDefault();

Assert.That(actualMessage, Is.Not.Null);

Assert.That(actualMessage.Text, Is.EqualTo(expectedText));
}

[SetUp]
public void SetUp()
{
markdown = new Markdown();
message1 = new MessageViewModel
{
AuthorName = "Dianne",
ID = 1,
RoomID = 12345,
Text = "Test!"
};
mockRoomViewModelReader = new Mock<IRoomViewModelReader>();
var roomMessages = new MessageViewModel[]
{
message1
};
mockRoomViewModelReader.Setup(reader =>
reader.GetRoomMessages(It.IsAny<int>())).Returns(roomMessages);
}

private MessageViewModel message1;
private Mock<IRoomViewModelReader> mockRoomViewModelReader;
private Markdown markdown;
}


STEVE: Again, that’s great. Didn’t you say you had a second query?

DAVID: Yeah—notice that the markdown class decorates the IRoomViewModelReader, but that class also has a GetAllRooms method. Is this a good candidate for interface segregation? At the moment, the GetAllRooms method isn’t transformed, so I just delegate straight down to the wrapped instance.

DIANNE: Should we allow the user to use markdown in the room names, too? That way, the GetAllRooms method would be decorated, too.

STEVE: I think we should leave it as it is for now. Let’s not split the interface or allow markdown in the room names. Depending on the client’s feedback during the demo, we can make a decision either way.

“I want to filter message content so that it is appropriate.”

Dianne and David have both been assigned to the message content filtering story. Together they will implement the functionality required by using pair programming.

DIANNE: As mentioned in the planning session, we don’t have time to implement a fully data-driven message content filter during this sprint. Instead, we need to make some progress toward that goal.

DAVID: So will we just implement the data access part for this sprint and come back to it next week?

DIANNE: No, we can’t do that. We need to deliver a vertical slice of functionality: something that is demonstrable to the client, but not necessarily complete. Just implementing the data access would not add any value.

DAVID: I don’t understand how we can deliver some value yet not implement the whole content filter.

DIANNE: What we will do is compromise somewhere so that we can be finished in a short amount of time yet provide some value to the client. For this story, the compromise is simple: we should hardcode the list of values that are considered inappropriate, instead of retrieving them from persistent storage like a database.

DAVID: I guess that makes sense. But I’ve always been told that hardcoding things is bad. Isn’t this poor design?

DIANNE: Sort of, yes. It’s technical debt. We are making a prudent decision to compromise on some desirable functionality in order to deliver something sooner rather than later. Perhaps the client knows exactly the list of words that they want to limit and they will never change. If so, we could complete this story just by hardcoding that list.

DAVID: I suppose it’s quite clever, really. Rather than take a lot of time to implement something, we aim for a simpler solution as an objective on the way to the goal.

DIANNE: Exactly! And, as each objective is met, it is possible that the next objective could be totally different—or that the goal itself changes dramatically.

DAVID: Another thing I’m unsure of: how are we going to implement this story? Should we create a decorator for the message writer that throws an exception when an inappropriate word is included in the message?

DIANNE: The trouble with that solution is that it uses exceptions for control flow. Exceptions are better reserved for situations that are truly “exceptional.” This is more of a validation scenario.

DAVID: Oh, I see! So we could hook into the MVC validation and just fail validation if the message contains something from the blocked words list?

DIANNE: I think that’s a better idea. How about this...?

Dianne starts typing and, a few minutes later, arrives at the class in Listing 12-3.

LISTING 12-3 A custom validation attribute is perfect for the content filter.


public class ContentFilteredAttribute : ValidationAttribute
{
private readonly string[] blockedWords = new string[]
{
"heffalump",
"woozle",
"jabberwocky",
"frabjous",
"bandersnatch"
};

protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
var validationResult = ValidationResult.Success;

if (value != null && value is string)
{
var valueString = (string)value;
if(blockedWords.Any(inappropriateWord =>
valueString.ToLowerInvariant()
.Contains(inappropriateWord.ToLowerInvariant())))
{
var errorMessage = FormatErrorMessage(validationContext.DisplayName);
validationResult = new ValidationResult(errorMessage);
}
}

return validationResult;
}
}


DIANNE: I’ve obviously used words that wouldn’t be included in a proper content filter blocked words list, but I wouldn’t want the demonstration to the client to include anything truly inappropriate!

DAVID: No, of course! I think this will demonstrate the functionality they want well enough. How about some tests?

DIANNE: Oh, of course. Here they are.

The two unit tests from the RoomControllerTests class are shown in Listing 12-4.

LISTING 12-4 Unit tests are added to enforce the validation rule on the room name and message text.


[Test]
[TestCase("Callooh! Callay! O frabjous day!")]
[TestCase("The frumious Bandersnatch!")]
[TestCase("A heffalump or woozle is very confusel...")]
public void PostCreateNewRoomWithBlockedWordsCausesValidationError(string roomName)
{
var controller = CreateController();

var viewModel = new RoomViewModel { Name = roomName };
var context = new ValidationContext(viewModel, serviceProvider: null, items: null);
var results = new List<ValidationResult>();

var isValid = Validator.TryValidateObject(viewModel, context, results, true);

Assert.That(isValid, Is.False);
}
// . . .
[Test]
[TestCase("Callooh! Callay! O frabjous day!")]
[TestCase("The frumious Bandersnatch!")]
[TestCase("A heffalump or woozle is very confusel...")]
public void PostAddMessageWithBlockedWordsCausesValidationError(string text)
{
var controller = CreateController();

var viewModel = new MessageViewModel { AuthorName = "David", Text = text};
var context = new ValidationContext(viewModel, serviceProvider: null, items: null);
var results = new List<ValidationResult>();

var isValid = Validator.TryValidateObject(viewModel, context, results, true);

Assert.That(isValid, Is.False);
}


DAVID: With the two properties on the view models marked with the ContentFiltered attribute, these tests will pass quite nicely.

DIANNE: Absolutely. Still, there are a few things that I’m not happy about with this design at the moment.

DAVID: What?

DIANNE: Obviously, the hardcoding of the blocked word list. I would consider this to be prudent technical debt, but I would still like to request the blocked word list from a provider interface. This way, I could create an implementing class that returns a static, hardcoded list and provide a more data-driven implementation in future.

DAVID: Yes, that would nicely split the data for the blocked word list from the algorithm of validating the property. Could you use dependency injection?

DIANNE: Unfortunately not. These custom attributes are not very flexible and will not be constructed through the controller or any other extension point exposed by MVC.

DAVID: That’s a shame. How about the service locator pattern?

DIANNE: I usually refer to service locator as an anti-pattern, but in this case it might be the best choice available to us.

DAVID: Shall we leave this as is for now and accept that there is some more technical debt associated with this attribute? That way, we get to make progress and revisit this code if necessary.

DIANNE: I agree. I think that this is likely to change quite drastically in the future, so there is no point in guessing which direction it will take at this point.

“I want to serve hundreds of users concurrently.”

The final story of the sprint involves Dianne and Steve increasing the scalability of the application. The client has requested that the application support scaling horizontally, rather than vertically. Horizontal scaling means that the application should be able to support more concurrent users through the addition of extra machines. In comparison, vertical scaling is the ability to support more concurrent users through the addition of extra resources to a single machine.

Dianne and Steve understand that the limiting factor to horizontal scaling is the presence in the architecture of a relational database management system (RDBMS), such as Microsoft SQL Server. In comparison to solutions with purpose-built distributed storage that is intended to scale horizontally, it is difficult to add a new instance of SQL Server to a cluster of machines.

Because of this, Dianne investigates the team’s options for replacing the SQL Server database with document storage. She presents MongoDB to the team as a solid and popular alternative that will allow the team to scale the application by adding new machines to a cluster. The only problem standing in the way is current reliance of the application on SQL Server to store and retrieve room and message data.

Luckily, the team has already prepared for this eventuality by programming to interfaces.

DIANNE: All we need to do is create new implementations for the repository interfaces:
IRoomRepository and IMessageRepository.

STEVE: We certainly could do that, but I think we could cut out the mapping from record data to view model data and, instead, just serialize and deserialize our view models directly.

DIANNE: That sounds interesting! That way we would just need to create new implementations of the IRoomViewModelReader and IRoomViewModelWriter interfaces that delegate down to MongoDB.

STEVE: Exactly.

The pair sets to work implementing the MongoRoomViewModelStorage class, as shown in Listing 12-5.

LISTING 12-5 The implementation of the data persistence layer for MongoDB.


public class MongoRoomViewModelStorage : IRoomViewModelReader, IRoomViewModelWriter
{
public MongoRoomViewModelStorage(IApplicationSettings applicationSettings)
{
this.applicationSettings = applicationSettings;
}

public IEnumerable<RoomViewModel> GetAllRooms()
{
var roomsCollection = GetRoomsCollection();
return roomsCollection.FindAll();
}

public void CreateRoom(RoomViewModel roomViewModel)
{
var roomsCollection = GetRoomsCollection();
roomsCollection.Save(roomViewModel);
}

public IEnumerable<MessageViewModel> GetRoomMessages(int roomID)
{
var messageQuery = Query<MessageViewModel>
.EQ(viewModel => viewModel.RoomID, roomID);
var messagesCollection = GetMessagesCollection();
return messagesCollection.Find(messageQuery);
}

public void AddMessage(MessageViewModel messageViewModel)
{
var messagesCollection = GetMessagesCollection();
messagesCollection.Save(messageViewModel);
}

private MongoCollection<MessageViewModel> GetMessagesCollection()
{
var database = GetDatabase();
var messagesCollection = database.
GetCollection<MessageViewModel>(MessagesCollection);
return messagesCollection;
}

private MongoCollection<RoomViewModel> GetRoomsCollection()
{
var database = GetDatabase();
var roomsCollection = database.GetCollection<RoomViewModel>(RoomsCollection);
return roomsCollection;
}

private MongoDatabase GetDatabase()
{
var connectionString = applicationSettings.GetValue(MongoConnectionString);
var client = new MongoClient(connectionString);
var server = client.GetServer();
return server.GetDatabase(ProsewareDatabase);
}

private readonly IApplicationSettings applicationSettings;
private static string MongoConnectionString = "MongoConnectionString";
private static string ProsewareDatabase = "Proseware";
private static string MessagesCollection = "messages";
private static string RoomsCollection = "rooms";
}


With this implementation in place, the team is able to unleash the potential of horizontal scalability for the Proseware application and the client.

Sprint demo

At the end of the second sprint, the team prepares to give another demonstration to the client. All of the stories completed in this sprint are collated and their functionality is shown. One of the key actions taken from the sprint retrospective meeting for sprint 1 was to improve the preparation of the demonstration. The team follows up on this diligently, meeting ahead of schedule to run through each story’s functionality without the client present. This helps to mitigate any problems due to differences in the development and demonstration environments. The rehearsal proceeds without a problem, so the team is ready to demonstrate its progress to the client.

During the demonstration of the markdown story, the team asks the client representative whether the client would like the room names to be subject to the same formatting as the messages in a room. The client seems receptive to the idea and asks that the team schedule this into the backlog, but indicates that it should only receive a low priority because there are more important features that the client would like to have sooner. The client is also pleased that the team has used markdown, rather than the requested HTML formatting, because the client understands that it is becoming a more popular and informal style of text formatting.

The next story is the message content filtering. The client is happy that the team has already applied the content filter to the room name and message text inputs and concurs that every future input received from users should similarly be subject to the same filtering. However, the client requests that this feature have the ability to be turned on or off via configuration in the future.

The final story of the sprint involves Tom simulating 300 users at a time, with the data storage spanning two separate machines. The client again requests that the data source to be used be controlled by configuration settings.

After all of the stories have been demonstrated, the client representative impresses upon the team how pleased he is with the incremental but tangible progress that the team has made in just two short sprints.

Sprint retrospective

At the end of the second sprint, just as at the end of the first sprint, the team convenes to discuss progress over the week. All team members are present and answer the following questions:

Image What went well?

Image What went badly?

Image Are there any parts of the process that we need to change?

Image Were any new things done in the sprint that we need to keep?

Image Were there any surprises discovered over the course of the sprint?

The aim is to generate a list of actionable items to prioritize and take forward. As usual, the outcome of this meeting is not to generate a lot of discussion without tangible action.

Assembled in a suitable meeting space, the team works through the list, one question at a time.

What went well?

The team starts with what went well during the sprint.

STEVE: What does everyone think went well in this sprint?

PETRA: Personally, I think this sprint was a great success: we met our sprint goal in a timely fashion, we prepared excellently for the demonstration, and we ensured, through preparation, that nothing went wrong. The client was very impressed, and we can be satisfied with our efforts.

DIANNE: I agree. It was a template for how future sprints should progress. Let’s not lose focus, though: we must aim to sustain this same level over time.

STEVE: Absolutely—there should be no room for complacency.

What went badly?

The next question for the team to answer is what went badly during the sprint.

STEVE: How about what went badly? Surely something didn’t go according to plan.

DAVID: I think there were a few questions that went unanswered during the sprint. These questions would have had a minor impact on implementation overall, but I think it would have been good to get these questions answered as soon as possible, rather than at the end of the sprint.

PETRA: What sort of questions, David? Do you have an example?

DAVID: In the markdown story, Dianne asked whether we should be transforming the room names in addition to the messages. We elected not to but, really, we didn’t know the definitive answer.

Things to change?

The team members move on to discussing anything that they feel needs to change about their process or working practices.

DIANNE: Yes, I agree. I think it’s something that we need to change: if we have a question about implementation, we should ask Petra. If she doesn’t know the answer, she can schedule a call with the client and ask them directly.

PETRA: Absolutely, I’m here to ensure that you have all of the knowledge you need about the client’s requirements. If you don’t ask me something and I haven’t been specific enough with the acceptance criteria in the story, something crucial might be missed.

STEVE: In this case, waiting to ask the question during the demonstration didn’t hurt us much, but with other more important questions it will be imperative that we seek the correct answer as soon as possible.

PETRA: Anything else that needs to change? No? Let’s move on, then.

Things to keep?

The team members start talking about the positive actions that they need to form into habits.

STEVE: I’ll pitch in with something to keep. We changed the preparation for the sprint and it worked well. We should make a note to keep that going until it becomes habitual.

PETRA: Great point. This next sprint will determine whether the quality of this sprint’s work is sustainable or is an anomaly, so noting how we achieved such a good sprint and bearing it in mind in the future is a good idea.

STEVE: What else should we keep?

DIANNE: I can’t think of anything else out of the ordinary that we especially need to keep doing.

Surprises?

The “surprises” section of the sprint retrospective aims to capture anything that has surprised the team during the sprint so that these things cannot be classed as surprises in the future.

STEVE: Final question, everyone: were there any surprises in this sprint that we need to investigate or prevent in the future?

DAVID: I’m a bit surprised at how well this sprint went!

The team concludes the meeting and the members file out of the room, buoyant with their recent success.

Summary

The second sprint has been a success and is an improvement for the team. By developing adaptability into the chosen solution’s code, the team has shown that it is able to gracefully handle the sort of change that is expected in Agile software development projects. Had the team not introduced any extension points in the code, it would have been very difficult for the team members to enhance the software by adding functionality without significantly rewriting, refactoring, or contorting the code to the breaking point.