Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Compile- and Run-Time Dependency

DZone's Guide to

Compile- and Run-Time Dependency

This lesson in refactoring explores using the Strategy Pattern to break a compile-time dependency into a run-time dependency for more flexible code.

· Java Zone ·
Free Resource

How do you break a Monolith into Microservices at Scale? This ebook shows strategies and techniques for building scalable and resilient microservices.

Here is the scenario we have:

You can think of all the code below as being created by you, meaning you are the owner and you already locked the files in the Configuration Management System so that others cannot alter it without you knowing. But you have grown tired of random change requests asking to use different sorting algorithms for the sort function.

abstract class Account {
    // ...
    // Many lines for generic bank account needs
    // ...

    // Default sorting method for transactions etc.
    public void sort() {
        System.out.println("Insertion Sort");
    }
}

class Saving extends Account {
    // ...
    // Many lines for saving account needs
    // ...

    // Saving accounts likes to use Bubble sort for sorting needs.
    @Override
    public void sort() {
        System.out.println("Bubble Sort");
    }
}
class Debit extends Account {
    // ...
    // Many lines for debit account needs
    // ...

    //Inherits the base class sorting functions
}

class Dividend extends Account {
    // ...
    // Many lines for dividend account needs
    // ...
    @Override
    public void sort() {
        System.out.println("Bubble Sort");
    }
}

// For demonstration purposes, we have only the main method here
public class CompileRunTimeDependency {

    public static void main(String[] args) {
        Account accSaving = new Saving();
        accSaving.sort(); //Prints: Bubble Sort
        Account accDebit = new Debit();
        accDebit.sort(); //Prints: Insertion Sort
        Account accDividend = new Dividend();
        accDividend.sort(); //Prints: Bubble Sort
        /* CONSOLE OUTPUT:
         * Bubble Sort
         * Insertion Sort
         * Bubble Sort
         * 
         * */
    }
}


Current Picture

If a client creates a Saving account instance, we know it will always use Bubble Sort. It is already set in stone. Or, if a client creates a Debit object, it will always call an Insertion sort. It is not possible to change sorting behavior without changing code — this is a compile-time dependency.

Change Request Pops

But a change request has just arrived wanting to use the Insertion sort algorithm for Dividend. This is possible, but we need to change the code — again, a compile-time dependency. As said before, you don't want anyone to change your existing code, including yourself!

Let's Critique the Current Design

We've violated the Single Responsibility Principle. Sorting is not the job of a bank account — a bank account should do only bank account things.

Second: We have code duplication. We copy/pasted the Bubble sort algorithm in Saving and Dividend accounts.

How to Accommodate the Change Request

We have a compile-time dependency due to inheritance. Therefore, we have to modify the existing code to accommodate the change request above. But you are tired of modifying the code for a never-ending request for using a different sorting algorithm. And in the future, there would be a request for even additional sorting algorithms, such as Selection, Merge Sort, etc. Life will be harder.

Refactoring Time

First step: Take out each sorting algorithm to its own class. But wait a minute. Remember the design principle Program to Interface. This way, we can break the compile-time dependency and reach a run-time dependency. This will enable our clients to change sorting algorithms as they want, without asking you to change your code.

interface SortingServices {
    void sort();
}

class BubbleSort implements SortingServices {
    public void sort() {
        System.out.println("Bubble Sort");
    }
}
class InsertionSort implements SortingServices {
    public void sort() {
        System.out.println("Insertion Sort");
    }
}


We have just pulled out the sorting algorithms, added an interface to make our code have standard method names, and taken advantage of programming to interface to use the benefit of a run-time dependency.

Now we need to update Bank Account's code to use the moved out sorting algorithms. For this, we create an instance of the SortingServices interface and initialize it in a sort() function with a default function of InsertionSort.

abstract class Account {
    //We added reference to interface
    SortingServices sortService;
    // ...
    // Many lines for generic bank account needs
    // ...

    // Default sorting method for transactions etc.
    public void sort() {
        // Default sorting still Insertion
        sortService = new InsertionSort();
        sortService.sort();
        //System.out.println("Insertion Sort");
    }
}


We need to change Saving and Dividend, too — so let's do it. In the constructor, we initialized sortService to BubbleSort since Saving uses BubbleSort. We have just added setSortingAlgorithm to enable our client to change the sorting algorithm at runtime. This is the advantage of the Program to Interface principle.

class Saving extends Account {
    // ...
    // Many lines for saving account needs
    // ...

    // Saving accounts like to use Bubble sort for sorting needs.

    public Saving() {
        //Default sorting for Saving Account
        sortService = new BubbleSort();
    }
    // We can change the sorting algorithm at run-time 
    // We do not need to change our code anymore for sorting algorithms
    public void setSortingAlgorithm(SortingServices sort) {
        this.sortService = sort;
    }

    @Override
    public void sort() {
        //Just call a sort method
        sortService.sort();
        //System.out.println("Bubble Sort");
    }
}


When we do the same update for Dividend, I noticed we have duplicated our code — the setSortingAlgorithm method. We can pull that method to the Account class to get rid of the code duplication.

class Dividend extends Account {
    // ...
    // Many lines for dividend account needs
    // ...

    public Dividend() {
        //Default sorting for Saving Account
        sortService = new BubbleSort();
    }
    // We can change the sorting algorithm at run-time 
    // We do not need to change our code anymore for sorting algorithm
    public void setSortingAlgorithm(SortingServices sort) {
        this.sortService = sort;
    }
    @Override
    public void sort() {
        //Just call a sort method
        sortService.sort();
        //System.out.println("Bubble Sort");
    }
}


Updated Account Class

We have just moved the setSortingAlgorithm method to the Account class and removed it from the derived classes (from the Dividend and Saving classes):

abstract class Account {
    //We added reference to interface
    SortingServices sortService = new InsertionSort();
    // ...
    // Many lines for generic bank account needs
    // ...

    // We can change sorting algorithm at run-time 
    // We do not need to change our code anymore for sorting algorithm
    public void setSortingAlgorithm(SortingServices sort) {
        this.sortService = sort;
    }

    // Default sorting method for transactions etc.
    public void sort() {
        // Default sorting still Insertion
        // sortService = new InsertionSort();
        sortService.sort();
        //System.out.println("Insertion Sort");
    }
}


Tested and it works as before. We just finished our refactoring.

Summary: What Have We Achieved?

We have just used the Strategy Pattern to break a compile-time dependency, and our code does not need to be changed anymore to use a different sorting algorithm. Our code can be enhanced independently; a new sorting algorithm can be added without disturbing your code. Our clients are happier; they do not need to beg you to change your code. By programming to interface, we break the compile-time dependency and create a run-time dependency.

Compile-time dependencies require code to be changed, but run-time dependencies do not.

Everyone is happy. Breaking dependency, reducing complexity, more flexible code.

Thanks for reading.

How do you break a Monolith into Microservices at Scale? This ebook shows strategies and techniques for building scalable and resilient microservices.

Topics:
java ,compile-time dependency ,run-time dependency ,refactoring ,strategy pattern ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}