Working Effectively with Legacy Code: Chapter 3 Summary

In the last chapter, we covered the refactoring process.

We declared that the most important step in refactoring is testing.

So why don't people write more tests?

The simple answer is because it's hard.

The reason why it's hard is because of dependency management. This basically means that to test some class, you must first instantiate it, but what if this class depends on three other classes. Should I also instantiate them?

What if one of the dependent classes depends on other classes too.

You see where I'm going with this. Most of the time, you just must break dependencies.

This is the subject of today's chapter.

Sensing and Separation

In general, there are two reasons why you would want to break dependencies:

  1. Sensing—We break dependencies to sense when we can’t access values our code computes.
  2. Separation—We break dependencies to separate when we can’t even get a piece of code into a test harness to run.

To illustrate these two concepts, let's take a look at the class named NetworkBridge.

public class NetworkBridge
{
    public NetworkBridge(EndPoint [] endpoints) {
        ...
}
    public void formRouting(String sourceID, String destID) {
        ...
}
... }

NetworkBridge accepts an array of Endpoint objects and manages their configuration. The main use of this class is to allow users to route traffic from one endpoint to another. It does that by changing the settings on the Endpoint object.

Now that you know what this class does, how will we test it?

Well, you might say let's just create a bunch of Endpoint objects and instantiate an NetworkBridge object.

It would look something like this.

class TestNetworkBridge {

  public void testNetworkBridge(){
    
    endpoints = []
    
    # generate some endpoints...
    
    networkBridge = new NetworkBridge(endpoints)
    
    # do some assertions...
    
    }

}

This might be an okay approach if not for one thing. Each instance of the Endpoint class opens a socket and communicates across the network to a particular device.

So our code here will make some real calls to some hardware.

So how bout we don't make new instances of Endpoint class?

Nope, we can't do that because our NetworkBridge class requires an array of them.

This example illustrates both the sensing and separation problems. We can’t sense the effect of our calls to methods on this class, and we can’t run it separately from the rest of the application.

Which problem is tougher? Sensing or separation? There is no clear answer. Typically, we need them both, and they are both reasons why we break dependencies. One thing is clear, though: There are many ways to separate software. In fact, there is an entire catalogue of those techniques in the back of this book on that topic, but there is one dominant technique for sensing.

Faking Collaborators

As stated above the biggest problem we confront in legacy code are dependencies. If we want to execute a class independently and see what it does we usually have to break dependencies in other code.

But it's not that simple. Often that other code is the only place we can easily sense the effects of our actions. If we can put some other code in its place and test through it, we can write our tests. In object orientation, these other pieces of code are often called fake objects.

Fake Objects

A fake object is an object that impersonates some collaborator of your class when it is beingtested. For example, going back to our NetworkBridge example. Our Endpoint class has a method called connect that creates a network connection to the some hardware.

The problem with testing NetworkBridge is that it will uses concrete Endpoint objects that will call the connect method and create real connections to hardware.

The solution is that instead of NetworkBridge relying on concrete implementations of Endpoint, we can force it to rely on abstractions.

The structure will look something like this:

Right now, instead of using real endpoints for our tests, we use fake endpoints.

In our FakeEndpoint class, instead of creating real connections we can do anything else, like logging that we have established connections.  

Conclusion

In summary:

  • Tests are hard to write due to very coupled dependencies.
  • There are two reasons to break dependencies that are sensing and separation.
  • Sensing is used to sense when we can’t access values our code computes.
  • Separation is used to separate when we can’t even get a piece of code into a test harness to run.
  • The best way to sense our dependencies is to use fake objects.

Thanks for reading.