In the previous "Expressing Ideas" article, we had left the code looking like this:
public void InsertParagraphTag() {
//
// On Enter, we change the TextModel lines to insert, after the line containing
// the cursor, a blank line, and a line with <P></P>. We set the new cursor
// location to be between the P tags: <P>|</P>.
//
int cursorLine = LineContainingCursor();
// allocate a new line array with two more lines
String[] newlines = new String[lines.Length+2];
// copy line 0 through cursor line to output
for(int i = 0; i <= cursorLine; i++) {
newlines[i] = lines[i];
}
// insert blank line
newlines[cursorLine+1] = "";
// insert P tag line
newlines[cursorLine+2] = "<P></P>";
// copy lines after cursor lien to output
for (int i = cursorLine+1; i < lines.Length; i++) {
newlines[i+2] = lines[i];
}
// save new lines as result
lines = newlines;
// set cursor location
selectionStart = NewSelectionStart(cursorLine + 2);
}
To make this better, we just kept pecking away at it, changing things around to make the code more expressive, and in some cases to use approaches that seemed better to us in C#. The first of these ideas was the best. This version uses two arrays of strings, creating a new array, "newlines" to be two more lines than the existing "lines" array. We knew there was an object in C#, ArrayList, which is basically an array that can grow, have insertions, and so on. So our first step was to change to object to use two ArrayLists instead of two arrays. To do this, we needed to change the Lines property:
public String[] Lines {
get {
// there must be a way to cast this??
String[] textlines = new String[newlines.Count];
for (int i = 0; i < newlines.Count; i++) {
textlines[i] = (String) newlines[i];
}
return textlines;
}
set {
lines = new ArrayList(value);
newlines = lines;
}
}
There's nothing very magical about this. The interface we chose expects arrays of string, so we are converting to and from ArrayList internally. There may be a better way to do the get operation, but at the Michigan Union, with no C# books, we sure couldn't find it. So we create an array whose size matches newlines.Count (for reasons known only to Microsoft, ArrayList uses Count where Array uses Length). Then we copy the newlines ArrayList into the array. We're hoping to find a way to just "cast" the ArrayList to an array of String, and when we do, we'll change this code to be cleaner.
We also had to change the TextModel constructor to initialize the lines and newlines variables to ArrayLists. (I think we had actually written the constructor during the testing phase described previously, when we built the test of an empty TextModel.
public TextModel() {
lines = new ArrayList();
newlines = lines;
}
It also turns out that there's some special case code in the InsertParagraphTag method, handling the empty case. I'll just show it here for completeness. We're hoping that when the refactoring is done, it will fall into the general case, but for now it's separate:
// handle empty array special case (yucch)
if ( newlines.Count == 0 ) {
newlines.Add( "<P></P>" );
selectionStart = 3;
return;
}
Now this really sets us up to do some better work. ArrayList has a method AddRange that lets you add to one ArrayList, a contiguous section of another. So we can rewrite all that array copying this way:
newlines = new ArrayList();
newlines.AddRange(LinesThroughCursor());
newlines.AddRange(NewParagraph());
newlines.AddRange(LinesAfterCursor());
Note that we extracted two methods, LinesThroughCursor and LinesAfterCursor. They look like this:
public ArrayList LinesThroughCursor() {
return lines.GetRange(0,LineContainingCursor()+1);
}
public ArrayList LinesAfterCursor() {
int cursorLine = LineContainingCursor();
return lines.GetRange(cursorLine+1,lines.Count - cursorLine - 1);
}
And we wrote a method NewParagraph to answer the blank line and paragraph tag pair:
public ArrayList NewParagraph() {
ArrayList temp = new ArrayList();
temp.Add("");
temp.Add("<P></P>");
return temp;
}
The net result of all this is to replace this code:
// allocate a new line array with two more lines
String[] newlines = new String[lines.Length+2];
// copy line 0 through cursor line to output
for(int i = 0; i <= cursorLine; i++) {
newlines[i] = lines[i];
}
// insert blank line
newlines[cursorLine+1] = "";
// insert P tag line
newlines[cursorLine+2] = "<P></P>";
// copy lines after cursor lien to output
for (int i = cursorLine+1; i < lines.Length; i++) {
newlines[i+2] = lines[i];
}
with this:
newlines = new ArrayList();
newlines.AddRange(LinesThroughCursor());
newlines.AddRange(NewParagraph());
newlines.AddRange(LinesAfterCursor());
We think that's a lot more expressive. Here's the whole method. See whether you agree that we have made it more expressive, and if we're justified in taking out the comments that we removed:
public void InsertParagraphTag() {
//
// On Enter, we change the TextModel lines to insert, after the line containing
// the cursor, a blank line, and a line with <P></P>. We set the new cursor
// location to be between the P tags: <P>|</P>.
//
// handle empty array special case (yucch)
if ( newlines.Count == 0 ) {
newlines.Add( "<P></P>" );
selectionStart = 3;
return;
}
newlines = new ArrayList();
newlines.AddRange(LinesThroughCursor());
newlines.AddRange(NewParagraph());
newlines.AddRange(LinesAfterCursor());
// set cursor location
selectionStart = NewSelectionStart(LineContainingCursor() + 2);
}
This code still isn't done, as we'll suggest in a moment. But you can see what has happened: we have taken code that clearly needed some explanation, and replaced it with code that surely needs less explanation. You might prefer a bit more than we do, but we'll wager that most readers won't want one line of comment per line of code any more.
As I say, the code doesn't look done to us: we have a sense of duplication from that special case that inserts the paragraph tag and such. We'd like to consolidate the special case and the general case. Furthermore, now that we're more familiar with ArrayList, we think we can probably do the editing right in the "lines" ArrayList and get rid of "newlines" altogether. We're going to leave that for another day, because ...
That's right. Our first story is now working. Remember, it was:
So we're finished and can move on to the next story, right?
We don't have a Customer Test for this story. We've satisfied ourselves as programmers that it works, but how can we satisfy our customer. We owe our customer a Customer [acceptance] Test. In the next article, we'll show how we create a Customer Test for this, our very first story. No framework, no waiting for QA to show up: we'll just create a test that we think our Customer will be able to understand. Watch for it!