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.
Join the DZone community and get the full member experience.
Join For FreeHere 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.
Opinions expressed by DZone contributors are their own.
Comments