Back to top | Next: JobApplicant has multiple responsibilities
The setZipCode method in the JobApplicant class contains hard-coded logic to access an Internet-based service to look up information based on a US zipcode. This creates a number of issues:
- The application code is tightly coupled to the Internet resource.
- Automated checks will be unreliable, as cases may fail when the Internet is not available or when response time is too long
- Automated checks will have long runtimes because of delay waiting for responses from the Internet resource.
- If many automated checks run quickly, the Internet connection may become overloaded. This can cause timeouts during the test run leading to false negatives in the test results. This tends to cause developers to ignore failing checks - they assume failures are due to network timeouts.
The corrective action is to move the code that interacts with the network resource out of the JobApplicant class and define an interface for performing zipcode lookups. The existing lookup code will become the first concrete implementation of that interface.
With the code isolated, the automated checks can specify a test double to represent the network resource. This will enable unit-level automated checks to run fast and to be reliable. At the integration or functional level, automated checks can access the actual network resource to ensure the application interacts properly with it.
Steps (test-first approach):
- Ensure all the automated unit checks are passing.
- Modify unit checks to reflect the behavior you intend to change in the application. You are going to inject an instance of
CityStateLookupon the constructor ofJobApplicant.You can use your IDE's features to create missing classes and methods to help you create the objects to which you are adding references in the test class. This is a good way to avoid mistakes in typing. - Run the automated checks and see that the only failures are due to the changes you made in Step 2.
- Create a new interface named
CityStateLookupthat declares the method lookup and accepts a String argumentzipcode. - Create a class named
CityStateLookupImplthat implements interfaceCityStateLookup. - Add a constructor argument to class
JobApplicantto accept a reference to aCityStateLookupobject. - Move the city and state lookup code from
JobApplicant.setZipCodeto ```CityStateLookupImpl.lookup - Ensure all the automated unit checks are passing.
Steps (traditional approach):
- Ensure all the automated unit checks are passing.
- Create a new interface named
CityStateLookupthat declares the method lookup and accepts a String argumentzipcode. - Create a class named
CityStateLookupImplthat implements interfaceCityStateLookup. - Add a constructor argument to class
JobApplicantto accept a reference to aCityStateLookupobject. - Move the city and state lookup code from
JobApplicant.setZipCodetoCityStateLookupImpl.lookup. - Adjust the unit checks so that they pass an implementation of
CityStateLookupto theJobApplicantconstructor. - Ensure all the automated unit checks are passing.
When creating the interface, you can either
- Create the interface first and then create the concrete class to implement it, or
- Create the concrete class first and then use your IDE's refactoring feature to do an extract interface refactoring.
Notice that regardless of how you approach the code changes, you begin and end with all automated unit checks passing. This is a refactoring - a change to the internal design of the code that does not change the behavior of the application.
When refactoring, all automated must should pass before and after. This is your "safety net" for ensuring you haven't inadvertently changed the behavior of the application when you only intended to change the internal design of the code.
The sample solution is in package com.neopragma.legacy.round2.
Here's what I did for the sample solution. I used the test-first approach to modify the code.
- Added a private member to
JobApplicantTestdeclaring an instance variable of typeCityStateLookup. That will be the name of the interface. References to objects of that type will be at the lowest level of abstraction that makes sense. In this case, we want to refer to the interface and not the specific concrete implementation classes. - Added code in the
@Beforemethod to pass a reference to aCityStateLookupobject to the constructor ofJobApplicant. - Used Eclipse to create interface CityStateClass in
src/main/java. At this point the interface specified no methods. - Used Eclipse to create class CityStateClassImpl in
src/main/java. At this point the class was empty. - Used Eclipse to create JobApplicant constructor with the
CityStateLookupargument. Eclipse generated a reference toCityStateLookupImpl, so I manually changed it toCityStateLookup. Also added code to the constructor method to store the passed-in reference in an instance variable. - Made the same change in
Mainas inJobApplicantTestto pass an instance ofCityStateLookupto the constructor ofJobApplicant. - At this point both ``JobApplicant
andJobApplicantTest``` compiled, so I ran the automated checks. All passed. - Because of the change to
Main, which is not covered by the automated unit checks, I manually checked it by choosing Run as... Java application. It still worked as before. - The city and state lookup logic returns a city name and a state abbreviation. So that the new
lookupmethod can return a single value, I needed a value object to encapsulate these two values. I created a class namedCityStateand defined two fields namedcityandstate. Then I used Eclipse to generate getter methods for these two fields, so that I would not accidently make a mistake typing in the code. The creation of the value class is almost entirely automated by the IDE. That means it is generated code. A rule of thumb is that we don't test-drive generated code. We test-drive code that we type with our own fallible fingers. - I want the value object to be immutable, so I defined a constructor that takes String arguments for city and state.
- I added a declaration for
lookup(String zipCode)to theCityStateLookupinterface. - I moved the code in
JobApplicant.setZipCodethat performs the lookup into the newCityStateLookupImpl.lookupmethod and replaced it with a call to that method. - At that point,
JobApplicant.zipCodewas no longer referenced, so I deleted it. - In
CityStateLookupImpl.lookup, changed reference fromthis.zipCodetozipCode(the argument passed in). - In
CityStateLookupImpl.lookup, tweaked the code to place the result values in theCityStatevalue object and return that object. - Used Eclipse to add throws declarations where needed. This can be further improved later.
- Changed getters for city and state in
JobApplicantto pass through to the returnedCityStatevalue object. - Ran all automated checks.
- Smoke tested the application manually.
If that sounds like a lot of work, it is...and it isn't. When we are dealing with legacy code, there will be many times when we have to take many small steps to accomplish a relatively minor clean-up. But as long as we work in small steps and check frequently as we go along to be sure we haven't overlooked something or broken something, then it really isn't that difficult. The idea that it's "impossible" to clean up legacy code is wrong, but on the other hand it isn't always quick and easy.