Managing dependencies between classes is not something a lot of developers like to think about when they write code. After all, when you are in the flow of writing code, if you need an object you simply create and use it. What could be easier?
However, the small decisions we make while in the flow of coding can have a significant impact on the long-term maintainability of the applications you build.
This became evident to me the other day as I was writing a piece of code and thinking about how I wanted to unit test the code. The code I was unit testing was rather simple, but I had one problem: I needed two classes (
CompareBundleLocations and
CompareLocations) that were the responsibility of another developer. This developer was behind schedule and and I knew I could write my code and unit test it around him.
Typically the class and method I would have written would have looked like this:
public MyClass {
public LocationResults compareTwoLocations(Location pLocationA,
Location pLocationB){
if ( pLocationA.checkBundling() ){
CompareBundleLocations cpl = new CompareBundleLocations();
return cpl.compare(pLocationA, pLocationB);
}
else{
CompareLocations cl = new CompareLocation();
return cl.compare(pLocationA, pLocationB);
}
}
}
Functionally, this class works, but I also made this class very difficult to unit test. When I write my unit test I have no way of stubbing out the behaviors of the
CompareBundleLocations and
CompareLocations class. The creation of the two objects are hard-coded inside of my
compare() method.
One thing I could do is define a "Stub" for each class and change my actual code to use the stub classes. I have two problems with this. First, I am not testing the actual code that would go into production. By hard-coding my stubs into the actual class I want to test, I am changing the base behavior of the class I am testing. Secondly, I always run the risk of forgetting to change my code back from the "Stub" to the actual class after it has been delivered by the developer.
The problem is that by the act of instantiating the objects within my method, I have locked myself into using that class. I not change easily "plug-in" new functionality.
Lets take a step back and start refactoring the class:
public MyClass {
private CompareBundleLocations mCBL = null;
private CompareLocations mCL = null;
public MyClass(){
private mCBL = new CompareBundleLocations();
private mCL = new CompareLocations();
}
public LocationResults compareTwoLocations(Location pLocationA,
Location pLocationB){
if ( pLocationA.getCheckBundling() ){
return getCompareBundleLocations().compare(pLocationA, pLocationB);
}
else{
return getCompareLocations.compare(pLocationA, pLocationB);
}
}
protected CompareBundleLocations getCompareBundleLocations(){
return mCBL;
}
protected void setCompareBundleLocations(CompareBundleLocations pCBL){
mCBL = pCBL;
}
protected CompareLocations getCompareLocations(){
return mCL;
}
protected void setCompareLocations(CompareLocations pCL){
mCL = pCL;
}
}
One of the major differences between this version of
MyClass and the earlier version is that I no longer directly instantiate the
CompareLocations and
CompareBundleLocations directly inside of my
compare() method. Instead I instantiate an instance of these classes in the constructor of
MyClass. When the
compare() method wants to use these two objects, it retrieves them by using the
get() methods.
Doing this extra work up can save a significant amount of effort in a multi-person development environment. Lets say the developer writing the
CompareLocations and
CompareBundleLocations classes are running behind. All they have done is provided you with a stub or interface for their classes.
All you want to do is test that
your code is behaving the way you expect it to. So what you could do is write a test class that behaves in the following manner:
public TestMyClass extends Test{
public TestMyClass(String pArg){
super(pArg);
}
public testCompare_NonBundled(){
Location locationA = new Location();
Location locationB = new Location();
locationA.setLocId("ABC");
locationA.setCheckBundling(false);
locationB.setLocId("CDE");
CompareLocations compareLocations = new NonBundledCompareSearch();
MyClass myClass = new MyClass();
myClass.setCompareLocations( compareLocations );
LocationResult result = myClass.compare(locationA, locationB);
assertTrue(result.getLocationMatch() );
assertTrue(result.getLocationConfidence()==100);
}
public testCompare_Bundled(){
Location locationA = new Location();
Location locationB = new Location();
locationA.setLocId("ABC");
locationA.setCheckBundling(true);
locationB.setLocId("CDE");
CompareLocations compareLocations = new BundledCompareSearch();
MyClass myClass = new MyClass();
myClass.setCompareLocations( compareLocations );
LocationResult result = myClass.compare(locationA, locationB);
assertFalse(result.getLocationMatch() );
assertTrue(result.getLocationConfidence()==0);
}
class NonBundledCompareSearch extends CompareLocations{
public LocationResults compare(Location pLocationA, Location pLocationB){
LocationResults results = new LocationResults();
results.setLocationMatch( true );
results.setLocationConfidence(100);
return results;
}
}
class BundledCompareSearch extends CompareBundleLocations{
public LocationResults compare(Location pLocationA, Location pLocationB){
LocationResults results = new LocationResults();
results.setLocationMatch( false );
results.setLocationConfidence(0);
return results;
}
}
}
So what just happened here? By removing the dependencies from the
compare() method, can inject new functionality into a
MyClass instance.
CompareLocations compareLocations = new NonBundledCompareSearch();
MyClass myClass = new MyClass();
myClass.setCompareLocations( compareLocations );
This is extremely useful because I need to unit test the code in
MyClass and not in the
CompareLocations and
CompareBundleLocations classes. By using inner classes to extend the
CompareLocations and the
CompareBundleLocations classes, I can prove my code works and that the appropriate behavior is being exercised.
One of the biggest traps developers fall into when writing their unit tests is that they start worrying about the behavior of other classes. By managing the dependencies between your classes you can focus on the behavior of a single class.
This technique is called Inversion of Control (IOC) or Dependency Injection (DI). By using IOC as a design principle, a developer can modify the behavior of their objects at run-time.
The behavior of the
compare() method has been changed by using inner classes that override the behavior of dependent objects used inside the
compare() method.
This same behavior could be duplicated by using Spring and having different Spring configurations for our unit tests. One Spring configuration could contain our "live" code configuration and another Spring configuration could be used Strictly for unit testing.
In the end by thinking about how to test our code and learning to managing object dependencies, you can end up with extremely flexible and maintainable code.