Problems with Non-Modularized Crosscutting Concerns

Why AOP is good

By definition, a crosscutting concern is a functionality that spans multiple modules of an application. This kind of concern is often hard to modularize with the traditional objectoriented approach. To understand crosscutting concerns, let’s start with a simple calculator example. First, you create two interfaces, ArithmeticCalculator and UnitCalculator, for arithmetic calculation and measurement unit conversion.

package com.calculator;
public interface ArithmeticCalculator {
public double add(double a, double b);
public double sub(double a, double b);
public double mul(double a, double b);
public double div(double a, double b);
}
package com.calculator;
public interface UnitCalculator {
public double kilogramToPound(double kilogram);
public double kilometerToMile(double kilometer);
}

Then you provide a simple implementation for each calculator interface. The println statements notify you of when these methods are executed.

package com.calculator;
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
public double add(double a, double b) {
double result = a + b;
System.out.println(a + " + " + b + " = " + result);
return result;
}
public double sub(double a, double b) {
double result = a – b;
System.out.println(a + " – " + b + " = " + result);
return result;
}
public double mul(double a, double b) {
double result = a * b;
System.out.println(a + " * " + b + " = " + result);
return result;
}
public double div(double a, double b) {
double result = a / b;
System.out.println(a + " / " + b + " = " + result);
return result;
}
}
package com.calculator;
public class UnitCalculatorImpl implements UnitCalculator {
public double kilogramToPound(double kilogram) {
double pound = kilogram * 2.2;
System.out.println(kilogram + " kilogram = " + pound + " pound");
return pound;
}
public double kilometerToMile(double kilometer) {
double mile = kilometer * 0.62;
System.out.println(kilometer + " kilometer = " + mile + " mile");
return mile;
}
}

Tracing the Methods

A common requirement of most applications is to trace the activities that take place during program execution. For the Java platform, there are several logging implementations available for you to choose. However, if you would like your application to be independent of the logging implementation, you can make use of the Apache Commons Logging library. It provides abstract APIs that are implementation independent and allows you to switch between different implementations without modifying your code.

Note To use the Apache Commons Logging library, you have to include commons-logging.jar (locatedin the lib/jakarta-commons directory of the Spring installation) in your classpath.

For your calculators, you can log the beginning and ending of each method, as well as the method arguments and return values.

package com.calculator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
private Log log = LogFactory.getLog(this.getClass());
public double add(double a, double b) {
log.info("The method add() begins with " + a + ", " + b);
double result = a + b;
System.out.println(a + " + " + b + " = " + result);
log.info("The method add() ends with " + result);
return result;
}
public double sub(double a, double b) {
log.info("The method sub() begins with " + a + ", " + b);
double result = a – b;
System.out.println(a + " – " + b + " = " + result);
log.info("The method sub() ends with " + result);
return result;
}
public double mul(double a, double b) {
log.info("The method mul() begins with " + a + ", " + b);
double result = a * b;
System.out.println(a + " * " + b + " = " + result);
log.info("The method mul() ends with " + result);
return result;
}
public double div(double a, double b) {
log.info("The method div() begins with " + a + ", " + b);
double result = a / b;
System.out.println(a + " / " + b + " = " + result);
log.info("The method div() ends with " + result);
return result;
}
}
package com.calculator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class UnitCalculatorImpl implements UnitCalculator {
private Log log = LogFactory.getLog(this.getClass());
public double kilogramToPound(double kilogram) {
log.info("The method kilogramToPound() begins with " + kilogram);
double pound = kilogram * 2.2;
System.out.println(kilogram + " kilogram = " + pound + " pound");
log.info("The method kilogramToPound() ends with " + pound);
return pound;
}
public double kilometerToMile(double kilometer) {
log.info("The method kilometerToMile() begins with " + kilometer);
double mile = kilometer * 0.62;
System.out.println(kilometer + " kilometer = " + mile + " mile");
log.info("The method kilometerToMile() ends with " + mile);
return mile;
}
}

Now you are free to pick up a logging implementation supported by the Commons Logging library. Currently it mainly supports the Log4J library from Apache and the JDK Logging API (available for JDK 1.4 and higher versions). Of these, Log4J is the better choice, as it is more powerful and easier to configure.

Note To use the Log4J library, you have to include log4j-1.2.14.jar (located in the lib/log4j directory of the Spring installation) in your classpath. Once the Log4J library is detected in the classpath, Commons Logging will use it as the underlying logging implementation. You can configure the Log4J library through a properties file named log4j.properties in the root of the classpath. The following Log4J configuration file defines a log appender called stdout that will output log messages to the console in a format controlled by the specified pattern. For more information about logging patterns of Log4J, you can refer to the Log4J documentation.

### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %5p %c{1}:%L – %m%n
### set root logger level ###
log4j.rootLogger=error, stdout
### set application logger level ###
log4j.logger.com.calculator=info

Log4J supports six logging levels for you to set the urgency of your logging messages. Listed from highest to lowest, they are fatal, error, warn, info, debug, and trace. As specified in the preceding Log4J configuration file, the root (default) logging level for this application is error, which means that only logs for the error and fatal levels will be output by default. But for the com.calculator package and its subpackages, the logs for levels higher than info will also be output.

To test the basic functionalities and the logging configuration of these two calculators, you can write the Main class as follows:

package com.calculator;
public class Main {
public static void main(String[] args) {
ArithmeticCalculator arithmeticCalculator = new ArithmeticCalculatorImpl();
arithmeticCalculator.add(1, 2);
arithmeticCalculator.sub(4, 3);
arithmeticCalculator.mul(2, 3);
arithmeticCalculator.div(4, 2);
UnitCalculator unitCalculator = new UnitCalculatorImpl();
unitCalculator.kilogramToPound(10);
unitCalculator.kilometerToMile(5);
}
}

Validating the Arguments

Now let’s consider adding a restriction to your calculators. Suppose you would like your calculators to support positive numbers only. At the beginning of each method, you make calls to the validate() method to check if all the arguments are positive numbers. For any negative numbers, you throw an IllegalArgumentException.

package com.calculator;
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
public double add(double a, double b) {
validate(a);
validate(b);
}
public double sub(double a, double b) {
validate(a);
validate(b);
}
public double mul(double a, double b) {
validate(a);
validate(b);
}
public double div(double a, double b) {
validate(a);
validate(b);
}
private void validate(double a) {
if (a < 0) {
throw new IllegalArgumentException("Positive numbers only");
}
}
}
package com.calculator;
public class UnitCalculatorImpl implements UnitCalculator {
public double kilogramToPound(double kilogram) {
validate(kilogram);
}
public double kilometerToMile(double kilometer) {
validate(kilometer);
}
private void validate(double a) {
if (a < 0) {
throw new IllegalArgumentException("Positive numbers only");
}
}
}

Identifying the Problems

As you can see, the original calculator methods expand as you add more and more nonbusiness requirements, such as logging and validation. These systemwide requirements usually have to crosscut multiple modules, so they are called crosscutting concerns to distinguish them from the core business requirements, which are called the core concerns of a system. Typical crosscutting concerns within an enterprise application include logging, validation, pooling, caching, authentication, and transaction. Figure 5-1 shows the crosscutting concerns in your calculator application.

Figure 5-1. Crosscutting concerns in the calculator application

However, with only classes and interfaces as programming elements, the traditional object-oriented approach cannot modularize crosscutting concerns well. Developers often have to mix them with core concerns in the same modules. As a result, these crosscutting concerns are spread out in different modules of an application and are thus not modularized.

There are two main problems caused by non-modularized crosscutting concerns. The first is code tangling. Like the preceding calculator methods, each of them has to handle multiple concerns as well as the core calculation logic at the same time. This will lead to poor code maintainability and reusability. For instance, the preceding calculator implementations would be hard to reuse in another application that has no logging requirement and can accept negative numbers as operands.

Another problem caused by non-modularized crosscutting concerns is code scattering. For the logging requirement, you have to repeat the logging statements multiple times in multiple modules to fulfill a single requirement. Later, if the logging criteria change, you would have to modify all of the modules. Moreover, it is also hard to ensure that the logging requirement will be implemented consistently. If you have missed a logging statement somewhere, the overall system logging will be inconsistent.

For all these reasons, the calculators should concentrate on the core calculation logic only. Let’s separate the logging and validation concerns from them.

package com.calculator;
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
public double add(double a, double b) {
double result = a + b;
System.out.println(a + " + " + b + " = " + result);
return result;
}
public double sub(double a, double b) {
double result = a – b;
System.out.println(a + " – " + b + " = " + result);
return result;
}
public double mul(double a, double b) {
double result = a * b;
System.out.println(a + " * " + b + " = " + result);
return result;
}
public double div(double a, double b) {
double result = a / b;
System.out.println(a + " / " + b + " = " + result);
return result;
}
}
package com.calculator;
public class UnitCalculatorImpl implements UnitCalculator {
public double kilogramToPound(double kilogram) {
double pound = kilogram * 2.2;
System.out.println(kilogram + " kilogram = " + pound + " pound");
return pound;
}
public double kilometerToMile(double kilometer) {
double mile = kilometer * 0.62;
System.out.println(kilometer + " kilometer = " + mile + " mile");
return mile;
}
}