I work for a University, though not as a professor. I do have the privilege to work with students in a variety of capacities, including web application development. There are several departments on campus that offer web development courses but they tend to focus on basic tasks performed with PHP rather than a greater understanding of the industry and the options within. Over the years I have been able to introduce many students to using Ruby and Rails as well as the underlying concepts perpetuated by agile development.
One of the key concepts is the idea of refactoring. Core to test driven development and essential to keeping your code DRY1, this concept is often not even discussed in the introductory development courses or, if it is taught, is overlooked by the students. Simply defined, refactoring is the process of rewriting code to make it easier to maintain while not altering the behavior of the application. When done well you eliminate complexities while making the code easier to work with. As I look at the attributes of a great craftsmen I see two that can be attributed with refactoring; aspire to improve and maintain cleanliness.
The first weeks of employment of a new developer on my staff consists of going through online courses2 and reading well-written books.3 Then, we spend some time pair-programming where we share one workstation with one of us driving and the other navigating.4 Through this process the new developer often gets their first taste of refactoring code. I have repeated this cycle many times as I train new developers and, the most recent time, I had the idea to document our refactoring for future reference.
Refactoring In Practice
The following is a summary of conversation and actions performed by myself and one of the student developers.
Our active user story5 was to provide date ranges for an application period, interview sign up period, and interview session period for an employment application feature of the website. The current implementation was displaying the beginning and ending dates on individual lines. You can also see that the interviews_begin_on
and interviews_end_on
attributes are optional.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Our customer6 requested we simplify this display, using a format similar to “10/01/12 - 10/09/12”. The first step is to write a our failing test7 with the syntax we plan to use in our view. We also focus only on one piece at a time, so we start with available_at
and unavailable_at
. We plan to create a method called application_period
, so we add the following test.
1 2 3 4 5 6 |
|
Predictably, this test fails since we’ve not created the application_period
method. So, we switch to the model and add the method.
1 2 3 |
|
Our test now passes so we move to the next set of attributes related to interview sign ups. Switching back to our spec file we add another test.
1 2 3 |
|
Skipping forward to making this test pass we add the interview_sign_up_period
method to the model.
1 2 3 |
|
The test passes and the astute student points out that we could refactor these methods, simplifying the long term maintenance of our code. At this point he becomes the driver and updates the model.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
We run our test which returns two passing examples. That’s refactoring in a nutshell. What happened next made this little change extremely important. The phone rang and it was our customer. He expresses that they’d like to add a notice to any applicant letting them know when the interview sign up period is using the format of “October 14 until October 23.” Our refactor meant we could focus our efforts on the new display_date_period
method and not the individual date range methods.
I switched to the spec and add the following test and then hand the keyboard over to the student.
1 2 3 4 5 6 |
|
My student developer drives the following change to our model to handle the now optional date format.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
The model changes just made are a prime example as to why we write tests. After I kept quiet and let the tests run my student was surprised to see our previously successful tests were failing, while the new test passed. I pointed out to him that the output from the display_date_period
method is expecting options[:date_format]
to contain a value.
At this point I take the liberty to point out a couple of other Rubyisms we could use to better equip ourselves for future development. I explain the splat operator8, extract_options!
, and join
. These will each help us handle the latest request from the customer.
I take over as driver and update display_date_period
method to handle an options hash.
1 2 3 4 5 6 7 |
|
We re-run the test suite and we have three passing tests. My student drives again and writes the next test to handle passing optional joining text.
1 2 3 4 5 6 7 8 9 |
|
The previous update to display_date_period
set the method up to handle an optional separator being defined, so our new tests run without any issue. My student, interested in seeing whether my code would actually handle multiple options adds one additional test.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
The test results all come back green, 5 passing tests. I mention that we haven’t written the test to handle the interview session period. Our initial story requested this feature for application period, interview sign up period, and interview session period. Before I finish talking the student has already written the next test.
1 2 3 |
|
We both notice a problem. According to the original design it is optional to have an end date for the interview period. Our test fixture had a value for interviews_begin_on
but had a null value for interviews_end_on
. We know the first time running the test will fail because we haven’t defined interview_period
, so we switch to the model to add that method. We also decide to handle null values within display_date_period
, so that this model looks similar to the other specific date period methods.
1 2 3 |
|
Now we have to determine the best way to handle the possible null date attribute. I suggest we run our test to see what errors we get. The error is wrong number of arguments(1 for 0) in #to_s
. I point out that if you call to_s
on a null value the result is an empty string. However, unlike the to_s
method of a Date
or Time
object, you cannot provide an argument.
To correct this we can explicitly test that the provided value is a Date
or Time
object or we can use an inline rescue
to handle the exception. I take over driving and use the inline rescue
to demonstrate another Rubyism.
1 2 3 4 5 6 7 |
|
We run the test suite and our test it fails on our expectation and not because of an exception. The returned result was "10/25/12 - "
which means our null value is being accounted for in the join
method. We open Ruby-doc.org and look into Array
methods to strip out a null element and find two that would work: reject
and delete_if
. Because we do not need the behavior to be destructive we go with reject
and use the proc shortcut9 with the blank?
method to check for null values.
1 2 3 4 5 6 7 |
|
All the tests are now passing. The student, ready to continue with adding edge cases asks “What happens if they pass more than two dates?” and reaches for the keyboard to start driving our next test. I quickly put the proverbial brakes on. We could spend the rest of the day working to accept every possible combination of arguments, but as it stands now we have refactored redundant code and have met the customer’s request. We instead switch gears and update the view to use the new methods.
1 2 3 4 |
|
We pull open the page in our browser and everything works as expected. The final step is to add the notice to applicants that they can sign up for an interview.
The previous example was relatively simple, but it is a scenario I see many times while teaching new developers. We refactored early in the process and because we did when the code was still simple, adding more complexity was relatively simple. Ultimately, this could have been handled by a presenter class to keep presentation out of the model and that is exactly what we did in a future update.
The Model
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
The Spec
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
- Don’t Repeat Yourself ↩
- Codeschool is my curriculum of choice ↩
- The Pragmatic Bookshelf provides a fantastic selection of topics ↩
- An excellent overview of pair programming is available from The Art of Agile Development’s Pair Programming chapter ↩
- Once again take a look at The Art of Agile Development, this time at the Stories chapter. ↩
- We develop web applications for different units within our department, which we refer to as customers ↩
- We practice Test Driven Development (TDD) after all ↩
- The splat operator is used to handle an unknown number of arguments. The collection is assigned to an array ↩
- A great way to save code when doing simple tasks. See Shortcut Blocks with Symbol to_proc for more information ↩