2.6. How to Cope with Design Changes

Over time, all designs change. The system that you spent countless hours in design meetings for will change. Users will add requirements that do not fit into your original vision of how the application would work and the initial architecture that was designed. Other times you realize that there is a much simpler way to implement a section of code than you had originally thought, and you change the code to reflect this.

This is the process of refactoring. Refactoring is the process of improving code, without changing the result the code produces. Improvements include readability, testability, performance, and the general maintainability of your code base.

In Martin Fowler's book Refactoring: Improving the Design of Existing Code, he describes the various refactorings that can perform on code bases, when they should be performed, and the advantages they bring. In the refactoring book, Flower introduces the refactoring patterns and gives each type of refactoring a name to allow the concept to be described and shared with other people.

Examples of common refactoring include Extract method, move to class, and pull-up methods. In the next few examples you will apply common refactorings:

private string SetSalesManagers(string phoneNumber)
{
    int areaCode = int.Parse(phoneNumber.Substring(0, 3));
    string region = string.Empty;

    switch (areaCode)

{
        case 248:
            region = "Central";
            break;
        case 818:
            region = "West";
            break;
        case 201:
            region = "East";
            break;
        default:
            region = "Unknown";
            break;
    }

    return GetSalesManager(region);
}

2.6.1. Extract Method

Extract method is a type of refactoring where duplicated code is extracted into a shared method to be reused:

private string SetSalesManagers(string phoneNumber)
{
    int areaCode = int.Parse(phoneNumber.Substring(0, 3));
    string region = GetRegion(areaCode);
    return GetSalesManager(region);
}

private string GetRegion(int areaCode)
{
    string region;
    switch (areaCode)
    {
        case 248:
            region = "Central";
            break;
        case 818:
            region = "West";
            break;
        case 201:
            region = "East";
            break;
        default:
            region = "Unknown";
            break;
    }

    return region;
}

2.6.2. Move to Class

Move to class refactoring involves moving a method into a different or its own class:

public static class SalesHelper
{
    public static string GetRegion(int areaCode)
    {
        string region;
        switch (areaCode)
        {
            case 248:
                region = "Central";
                break;
            case 818:
                region = "West";
                break;
            case 201:
                region = "East";
                break;
            default:
                region = "Unknown";
                break;
        }

        return region;
    }
}

public class MySales
{

    private string SetSalesManagers(string phoneNumber)
    {
        int areaCode = int.Parse(phoneNumber.Substring(0, 3));
        string region = SalesHelper.GetRegion(areaCode);
        return GetSalesManager(region);
    }

    private string  GetSalesManager(string region)
    {
        return string.Empty;
    }
}

2.6.3. Pull-Up Methods

Pull-up method refactoring takes a method and moves the logic into a super class. This type of refactoring is useful when subclasses share functionality but each implements it separately:

public class Employee
{
    public string FirstName { get; set; }

public string LastName { get; set; }
}

public class ProjectManger:Employee
{
    public static string GetRegion(int areaCode)
    {
        string region;
        switch (areaCode)
        {
            case 248:
                region = "Central";
                break;
            case 818:
                region = "West";
                break;
            case 201:
                region = "East";
                break;
            default:
                region = "Unknown";
                break;
        }

        return region;
    }
}

public class Developer:Employee
{
    public static string GetRegion(int areaCode)
    {
        string region;
        switch (areaCode)
        {
            case 248:
                region = "Central";
                break;
            case 818:
                region = "West";
                break;
            case 201:
                region = "East";
                break;
            default:
                region = "Unknown";
                break;
        }

        return region;
    }
}

The code in the previous example may look familiar to you. You have two different classes (manager and developer) each that inherit from the Employee super class. Both sub classes (manager and developer) have a function included named GetRegion, which provides functionality to get the region where that employee is employed. When this development pattern is encountered, the pull-up methods refactoring should be applied:

public class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public static string GetRegion(int areaCode)
    {
        string region;
        switch (areaCode)
        {
            case 248:
                region = "Central";
                break;
            case 818:
                region = "West";
                break;
            case 201:
                region = "East";
                break;
            default:
                region = "Unknown";
                break;
        }

        return region;
    }
}

public class ProjectManger:Employee
{

}

public class Developer:Employee
{

}

These kinds of examples can have different impacts on systems depending on at which stage you perform the refactoring. Some refactorings will only affect the internal structure and as such have very little effect on your tests and the rest of the system. Other times, you might have to change a number of different public interfaces that will cause knock-on effects echoing around the rest of the system. Nevertheless, if your refactorings are going to affect your system and your tests, how should you cope with the changes?

Following a similar approach to TDD, your refactoring should also be test-driven. Your refactoring changes should flow from your tests. For example, if you are renaming a method, then you should first rename the method call in the tests to reflect your desired end result in the same way the original test was coded.

You should then perform the Red-Green-Refactor approach where you start with a failing test and refactor your old code into a new passing test. Because you are making your changes in a test-driven style you will know when you are done and when you have finished as the bar will be green. This will keep you focused and provide you with feedback on your changes.

In terms of changes which you will perform, if you're performing TDD and the Red-Green-Refactor, then the refactoring stage is a small focused step and performed very quickly after you have your test passing. As such, the impact on the tests should be minimal.

In terms of large changes, you should still follow TestDriven Refactoring. Similar to the method name changes just described, you should change the tests to reflect your end goal of the system. If the tests are focusing on behavior, then this shouldn't have changed and as such you simply need to change which objects and methods you're interacting with. Any change, to the behavior of the system is not a refactoring step and new tests should be created for them. If you have removed the behavior then you should also remove the associated tests.

To ease the pain and reduce the amount of changes that will be required for your codebase, you should follow the SOLID principles which have been discussed thoroughly previously in this chapter. By isolating your dependencies and each object having its own set of responsibilities, you will find that when you need to refactor the code, both the system and your tests will require fewer changes.

However, while driving your refactoring from unit tests, you also need to consider the implications they have on testers together with your integration and acceptance tests. After making the changes, your testers will need to update their test suites to reflect the updated code. The more testable your code is in terms of isolation and responsibilities, the easier this stage will be because the changes are less invasive.

However, the problem of refactoring changes echoing throughout the code base has been an issue in terms of tests, other methods, and your integrationacceptance. As such, various tools vendors have integrated automated refactoring steps into their development packages that allow the different scenarios to be performed more effectively. For example, when you rename a method, any other methods which are dependent are also updated to reflect this name. As a result, the number of breakages are reduced. By keeping your tests in the same solution as your project you can use the tool to aid in your steps.

In Chapter 3 we will discuss in depth how the various refactoring steps were performed on your sample application and how this affected the tests and the rest of the system.

2.6.4. Wax on, Wax Off

With that in mind, it's useful to take the concepts and quickly write some code to ensure you have a "solid" understanding.

In this exercise, you should create a testable object which calls out to an external service. The service will return a list of items; we need to identify a particular item and return that back to the calling code. The part we want to test is the service obtaining the results and obtaining the particular record.

  1. Create a test for a method called GetFirstNode on ServiceResultFilter class and mock a call to a method called GetFeed on a FeedPull object. The call should return "<nodes><node>One</node><node>Two</node><node>Three</node></nodes>."

  2. Verify the result of GetFirstNode is the string "One."

  3. Ensure the code compiles and execute the test to see it fail.

  4. Implement the passing code.

  5. Execute the test to see if your code works.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.107.210