When we talked about decoupling in the previous section, the idea of flexibility and maintainability probably came to mind by association. Loosely coupled systems are more flexible and easier to maintain.
For example, in Chapter 6, Interfaces, Classes, and Object Construction, we demonstrated a flexible solution when implementing an object factory:
public static Calculator createInstance(){
WhichImpl whichImpl =
Utils.getWhichImplValueFromConfig(Utils.class,
Calculator.CONF_NAME, Calculator.CONF_WHICH_IMPL);
switch (whichImpl){
case multiplies:
return new CalculatorImpl();
case adds:
return new AnotherCalculatorImpl();
default:
throw new RuntimeException("Houston, we have another problem."+
" We do not have implementation for the key " +
Calculator.CONF_WHICH_IMPL + " value " + whichImpl);
}
}
It is tightly coupled with its Calculator interface (its API) only, but that is inevitable because it is the contract the implementation must honor. As for the implementation inside the factory, it is much freer from any restrictions as long as it adheres to the contract.
We could create an instance of each of the implementations only once and return only that one instance (making each class a singleton). Here is an example of CalculatorImpl as a singleton:
private static Calculator calculator = null;
public static Calculator createInstance(){
WhichImpl whichImpl =
Utils.getWhichImplValueFromConfig(Utils.class,
Calculator.CONF_NAME, Calculator.CONF_WHICH_IMPL);
switch (whichImpl){
case multiplies:
if(calculator == null){
calculator = new CalculatorImpl();
}
return calculator;
case adds:
return new AnotherCalculatorImpl();
default:
throw new RuntimeException("Houston, we have another problem."+
" We do not have implementation for the key " +
Calculator.CONF_WHICH_IMPL + " value " + whichImpl);
}
}
Or we could add another Calculator implementation to the factory as a nested class and use it instead of CalculatorImpl:
public static Calculator createInstance(){
String whichImpl = Utils.getStringValueFromConfig(CalculatorFactory.class,
"calculator.conf", "which.impl");
if(whichImpl.equals("multiplies")){
return new Whatever();
} else if (whichImpl.equals("adds")){
return new AnotherCalculatorImpl();
} else {
throw new RuntimeException("Houston, we have a problem. " +
"Unknown key which.impl value " + whichImpl +
" is in config.");
}
}
static class Whatever implements Calculator {
public static String addOneAndConvertToString(double d){
System.out.println(Whatever.class.getName());
return Double.toString(d + 1);
}
public int multiplyByTwo(int i){
System.out.println(Whatever.class.getName());
return i * 2;
}
}
And the client code of this factory would never know the difference unless it prints out information about the class using the getClass() method on the object returned from the factory. But that is another story. Functionally, our new implementation of Whatever would work as an old one.
And that is actually a common practice—to change internal implementation from one release to another. There are bug fixes, of course, and new functionality added. And as the code of the implementation is evolving, its programmers are constantly watching for the possibility of refactoring. In computer science, factoring is a synonym of decomposition, which is breaking a complex code into simpler parts with the purpose of making the code more readable and maintainable. For example, let's assume we were asked to write a method that accepts two parameters of the String type (each represents an integer) and returns their sum as an integer too. After thinking for a moment, we decided to do it this way:
public long sum(String s1, String s2){
int i1 = Integer.parseInt(s1);
int i2 = Integer.parseInt(s1);
return i1 + i2;
}
But then we have asked for a sample of possible input values, so we can test our code in the condition close to production. It turned out that some of the values can be up to 10,000,000,000, which exceeds 2,147,483,647 (the maximum Integer.MAX_VALUE int value Java allows). So we have changed our code to the following:
public long sum(String s1, String s2){
long l1 = Long.parseLong(s1);
long l2 = Long.parseLong(s2);
return l1 + l2;
}
Now our code can handle values up to 9,223,372,036,854,775,807 (which is Long.MAX_VALUE). We deployed the code to production and it worked just fine for several months, used by a big software system that processes statistics. Then the system switched to a new source of data and the code started breaking. We investigated and found out that a new source of data yields values that can include letters and some other characters. We have tested our code for such cases and discovered that the following line throws NumberFormatException:
long l1 = Long.parseLong(s1);
We discussed the situation with the domain experts and they suggested we record the values that are not integer, skip them, and continue the sum calculations. So, we have fixed our code, as follows:
public long sum(String s1, String s2){
long l1 = 0;
try{
l1 = Long.parseLong(s1);
} catch (NumberFormatException ex){
//make a record to a log
}
long l2 = 0;
try{
l2 = Long.parseLong(s2);
} catch (NumberFormatException ex){
//make a record to a log
}
return l1 + l2;
}
We have quickly released the code to production, but for the next release got new requirements: the input String values can contain decimal numbers. So, we have changed the way we process the input String values by assuming they carry decimal values (which cover integer values too) and refactored the code, as follows:
private long getLong(String s){
double d = 0;
try{
d = Double.parseDouble(s);
} catch (NumberFormatException ex){
//make a record to a log
}
return Math.round(d);
}
public long sum(String s1, String s2){
return getLong(s1) + getLong(s2);
}
That is what refactoring does. It restructures the code without changing its API. As new requirements keep coming in, we can change the getLong() method without even touching the sum() method. We also can reuse the getLong() method in other places, and that is going to be the topic of the next section.