This post is a continuation of our previous article, a response to this smelly code.
Remember those columns and rows everywhere, and how tough it was to swap them everywhere? Well this is a prime time to fix that. How else can we reduce parameter count, while still passing in the same information? Enter complex object.
In this case, we can gain:
- Reduction of column / row parameters to just location
- Give the CreateCellKey(x,y) method a home (really, it gives a key for a particular coordinate.
So our first step is to create the new object and move in the CreateCellKey method; note this is also implementing a static create method:
public class GridCoordinate
{
public int Column { get; set;}
public int Row { get; set; }
public static string CreateCellKey(GridCoordinate location)
{
return location.Column + "," + location.Row;
}
public static GridCoordinate Create(int column, int row)
{
return new GridCoordinate {Column = column, Row = row};
}
}
And then we have to fix the calls that just deal with columns and rows. Note the code is longer for now, but we’ll address this with reusability in the near future!
Our resulting changed code is as follows:
private static string GetRightKey(IGrid grid, GridCoordinate location, CellValues clickedValue)
{
string rightKey = String.Empty;
bool columnIsNotLastColumn = location.Column < grid.Width - 1; ;
if (columnIsNotLastColumn
&& grid.Columns[location.Column + 1][location.Row].CellValue == clickedValue
&& IsCellInMarkableState(grid.Columns[location.Column + 1][location.Row]))
{
rightKey = GridCoordinate.CreateCellKey(GridCoordinate.Create(location.Column + 1, location.Row));
}
return rightKey;
}
private static string GetLeftKey(IGrid grid, GridCoordinate location, CellValues clickedValue)
{
string leftKey = String.Empty;
bool columnIsNotFirstColumn = location.Column > 0;
if (columnIsNotFirstColumn
&& grid.Columns[location.Column - 1][location.Row].CellValue == clickedValue
&& IsCellInMarkableState(grid.Columns[location.Column - 1][location.Row]))
{
leftKey = GridCoordinate.CreateCellKey(GridCoordinate.Create(location.Column - 1, location.Row));
}
return leftKey;
}
private static string GetDownKey(IGrid grid, GridCoordinate location, CellValues clickedValue)
{
string downKey = String.Empty;
bool rowIsNotLastRow = location.Row < grid.Height - 1;
if (rowIsNotLastRow
&& grid.Columns[location.Column][location.Row + 1].CellValue == clickedValue
&& IsCellInMarkableState(grid.Columns[location.Column][location.Row + 1]))
{
downKey = GridCoordinate.CreateCellKey(GridCoordinate.Create(location.Column, location.Row + 1));
}
return downKey;
}
private static string GetUpKey(IGrid grid, GridCoordinate location, CellValues clickedValue)
{
string upKey = String.Empty;
bool rowIsNotFirstRow = location.Row > 0;
if (rowIsNotFirstRow
&& grid.Columns[location.Column][location.Row- 1].CellValue == clickedValue
&& IsCellInMarkableState(grid.Columns[location.Column][location.Row - 1]))
{
upKey = GridCoordinate.CreateCellKey(GridCoordinate.Create(location.Column, location.Row - 1));
}
return upKey;
}
private int IfContainsClickedValueThenMark(string key, IGrid grid, GridCoordinate location, CellValues clickedValue)
{
int countOfMarkedCells = 0;
if (key != String.Empty && !_searchedCells.Contains(key))
{
_searchedCells.Add(key, String.Empty);
countOfMarkedCells += MarkConnectingCellsRecursive(grid, location, clickedValue);
}
return countOfMarkedCells;
}
private int MarkConnectingCellsRecursive(IGrid grid, GridCoordinate location, CellValues clickedValue)
{
//set the current location to be clicked.
grid.Columns[location.Column][location.Row].CellState = CellStates.Down;
//we seed the count at one here, because at least the current cell is to be
//marked.
int countOfMarkedCells = 1;
//look all around our current cell for other cells of the same value.
string rightKey = GetRightKey(grid, location, clickedValue);
string leftKey = GetLeftKey(grid, location, clickedValue);
string downKey = GetDownKey(grid, location, clickedValue);
string upKey = GetUpKey(grid, location, clickedValue);
//if the keys ARE the same value, look around them for the same value, mark them, and add them
//to the count
countOfMarkedCells += IfContainsClickedValueThenMark(rightKey, grid, GridCoordinate.Create(location.Column + 1, location.Row), clickedValue);
countOfMarkedCells += IfContainsClickedValueThenMark(leftKey, grid, GridCoordinate.Create(location.Column - 1, location.Row), clickedValue);
countOfMarkedCells += IfContainsClickedValueThenMark(downKey, grid, GridCoordinate.Create(location.Column, location.Row + 1), clickedValue);
countOfMarkedCells += IfContainsClickedValueThenMark(upKey, grid, GridCoordinate.Create(location.Column, location.Row- 1), clickedValue);
return countOfMarkedCells;
}
Now that we’ve done that, we can really do some cool things. Since we have a reference to a distinct location as an object, that object knows where it is. You can treat it like a plotter. Given a location, move up, down, left, right.(Note, we’ll probably want to rename this object to something like GridPlotter, so it makes more sense… this would have a GridCoordinate object, but for now, we’ll place the methods right in the GridCoordinate object:
public GridCoordinate MoveUp()
{
return Create(Column, Row - 1);
}
public GridCoordinate MoveDown()
{
return Create(Column, Row + 1);
}
public GridCoordinate MoveLeft()
{
return Create(Column - 1, Row);
}
public GridCoordinate MoveRight()
{
return Create(Column + 1, Row);
}
public string CreateCellKey()
{
return CreateCellKey(this);
}
This lets you refactor code like follows. Instead of the +1 and –1 everywhere:
countOfMarkedCells +=
IfContainsClickedValueThenMark(
rightKey, grid,
GridCoordinate.Create(location.Column + 1,
location.Row),
clickedValue);
You can just put:
countOfMarkedCells += IfContainsClickedValueThenMark(
rightKey, grid,
location.MoveRight(),
clickedValue);
At this point, unit tests all still pass, and app feels pretty good still, so this is a pretty successful refactoring process thus far. For the time until the next post, I’ll be making sure all areas of code referencing this class (the GridController class) no longer use pure “int column, int row” values, but rather use the more robust GridCoordinate class.
After this is complete, the GridCoordinate class will be renamed, and co-located near the Grid itself. Right now, this is only referenced by the GridController, but in reality, the GridCoordinate should serve as the base of all Grid cell lookups. (Rather than always using the Grid.Columns[column][row]:Cell).