devxlogo

Aspect- vs. Object-Oriented Programming: Which Technique, When?

Aspect- vs. Object-Oriented Programming: Which Technique, When?

ike most developers, I have been developing software systems using object-oriented programming (OOP) techniques for many years. So when I read that aspect-oriented programming (AOP) addresses many problems that traditional OOP doesn’t solve completely or directly, I wanted to better understand its benefits in real world application development. I thought comparing both techniques would provide some practical insight. So I decided to design a case study: an OOP application in which I identified aspects where AOP might be a good fit.

This article presents a similar case study. It begins by introducing a problem domain and then demonstrates two solutions: one that uses OOP and one using AOP. The AOP solution uses a JDK 1.5, JUnit, and AspectWerkz implementation. (The complete source code for both approaches is available in the “Download the Code” section in the left-hand column.) Finally, it demonstrates how to code a few aspects.

By the end, you will know all you need to know about using AOP in the real world, including what it tries to solve in software design.

 
Figure 1. Domain Model for the Solution

Problem Domain Description

A software company employs a programmer, assigns him to a business unit, and has him report to a manager. The manager gives his team members credit points when they meet their objectives. The company’s required solution must add a new employee and give credit points to existing employees. To keep things simple, the persistence can be done in a CSV file.

Figure 1 illustrates the domain model for the solution to this problem.

The Manager class is derived from the Employee class, and it contains an additional attribute, Managing Project. A set of employees would be part of a business unit. The business units form the company. Ignore the Company class, since it is out of this problem domain’s scope.

 
Figure 2. Sequence Diagram of Object Interactions

The Solution Application’s Design

The design for the solution can best be explained with a sequence diagram. Figure 2 represents the interaction between the objects for a solution that adds a new employee to the organization and assigns him or her to a business unit and a manager. This article explores only the required details of the application to keep things simple. However, you can explore the code directly for additional information.

EmployeeServiceTestCase, a JUnit test case class, simulates an end user, creates a new employee record, and assigns him to a business unit and manager. It fetches all the available business units and managers to populate the GUI. The repository classes load data from storage. To instantiate the domain objects BusinessUnit and Manager, the retrieved record is passed to the factory classes. Later, an Employee object is created by passing these references to the EmployeeService object. The service class uses EmployeeFactory to create the object and passes it to EmployeeRepository for persisting in storage.

Aspects of the Application that Call for AOP

So far, the discussion of the model and the design has been at a high level. Now, it turns to various other aspects of this application that are very important for understanding the value of AOP.

Resources for an Operation

Consider the highlighted lines in the following code snippet:

    public static Set findAllBusinessUnits() throws RepositoryException {        Set businessUnits = new HashSet();        try {            FileReader businessUnitFile = null;            BufferedReader bufferedBusinessUnitFile = null;            try {                businessUnitFile = new FileReader(FILE_NAME);                bufferedBusinessUnitFile = new BufferedReader(businessUnitFile);                String businessUnitRecord;                while((businessUnitRecord = bufferedBusinessUnitFile.readLine()) != null) {                    BusinessUnit businessUnit = BusinessUnitFactory.createBusinessUnit(businessUnitRecord);                    businessUnits.add(businessUnit);                }            } finally {                if(bufferedBusinessUnitFile != null) {                    bufferedBusinessUnitFile.close();                }                if(businessUnitFile != null) {                    businessUnitFile.close();                }            }        } catch(IOException ioe) {            String message = "IOError. Unable to find Business Unit records";            logger.log(SEVERE, message, ioe);            throw new RepositoryException(message, ioe);        }        logger.log(INFO, "Manager Records returned:" + businessUnits.size());        return businessUnits;    }

The code tries to read business unit records from a CSV file using FileReader and BufferedReader.

Applications repeatedly acquire a resource for an operation and then release it once the operation is complete. Removing this aspect of an application would improve the code readability and lead to a better design since once removed the remaining code does only what it is supposed to do. In this example, the method’s objective is to read business unit records. So it doesn’t need to worry about acquiring and releasing the necessary resources. Similarly, you also can handle the exceptions differently with AOP. (More on that later.)

Persistence

Traditional OOP uses repository classes to take care of an application’s persistence. The following example illustrates this concept:

public class EmployeeRepository {    public static void createEmployee(Employee employee) throws RepositoryException {      // Uses print writer to persist to the csv file.    }    public static String findEmployeeRecordById(String id) throws RepositoryException {      // Uses file reader to retrieve the employee record for the given id.    }    public static Employee findEmployeeById(String id) throws RepositoryException {      // Uses findEmployeeRecordById to fetch the employee record and the Employee      // object is created using the factory class.    }    public static void updateEmployee(Employee employee) {      // Updates the employee record.    }}

The EmployeeService class uses a repository class to provide services related to employees in the application. In an enterprise application, removing persistence from the domain model improves the design. Modelers and programmers would be able to concentrate on the domain logic and handle persistence separately. You will see how this is achieved using AOP later.

Logging

Removing logging?be it for debugging or auditing?from the classes would drastically improve the code readability. Consider the following code snippet:

    public Employee createEmployee(String name,                                                            String contactNumber,                                                           BusinessUnit businessUnit,                                                            Manager manager)                                                            throws EmployeeServiceException {        String id = createNewEmployeeId();        Employee employee = EmployeeFactory.createEmployee(id, name, contactNumber, businessUnit, manager);        try {            EmployeeRepository.createEmployee(employee);        } catch(RepositoryException re) {            String message = "Created employee successfully:" + employee;            logger.log(SEVERE, message);            throw new EmployeeServiceException(message, re);        }        logger.log(INFO, "Created employee successfully:" + employee);        return employee;    }

The above example includes a couple of audit logs, a fatal error, and a success message. The logging aspect can be moved out of the domain model and implemented separately.

Error Handling

The sample application leaves the error handling aspect of the application to you, but this section does discuss the underlying concept using above code. When you call the createEmployee method in the EmployeeRepository object, you could get RepositoryException. In the traditional approach, you handle this error in the same class. Alternatively, the createEmployee method of the service class can return null if RepositoryException is thrown, and other logic in the catch block can be handled outside this class.

Error handling varies case by case. Hence, applying AOP might differ for each of these scenarios.

AOP Approach

The sequence diagram in Figure 3 illustrates the design of the AOP approach and the interaction between the classes at a higher level. You might want to compare it with Figure 1 to understand the concept better.

 
Figure 3. The Design of the AOP Approach

The objective of the application is to fill the map in the BusinessUnitService class with BusinessUnit instances by reading the records from the CSV file. Using AOP to populate this map is like a backdoor approach, where the control is delegated to a repository class for reading BusinessUnit records from storage.

AOP is all about defining point cuts and advices. A point cut is a place of execution in the source code. The above example defined a point cut for the method findBusinessUnits in the class BusinessUnitService. An advice is a piece of code that executes when a point cut is reached. The BusinessUnitPersistentAspect class contains the advice method findAllBusinessUnits, which loads data from storage and uses the factory class to create the BusinessUnit object. This object is then added to the map. The map reference is retrieved from the BusinessUnitService instance. The point cuts and advices form an Aspect.

The OOP approach delegates the call to a DAO class in order to read from storage. With AOP, you define a point cut in the domain class and define an advice for reading from storage. The AOP framework would inject the code written as an advice, either at the time of execution (online weaving) or during compilation (offline weaving).

To summarize, when the call to findAllBusinessUnits in the BusinessUnitService class is made, the AOP framework injects code that is the advice method and pre-populates the map instance with the BusinessUnit instances by reading values from storage. This way, the persistence aspect of the application can be moved out of the domain model.

The Aspects in the New Approach

This section discusses how each of the aspects in the application modeled using OOP can be done using the AOP approach.

Resources for an Operation

A buffered reader resource is set to the persistence methods in the BusinessUnitPersistenceAspect class. You can define aspects even for aspects, but to keep things simple, this discussion concentrates on only the find methods in the class. Focus on the highlighted lines of the following code:

@Aspect("perJVM")public class BufferedFileReaderAspect {    @Expression("execution(* org.javatechnocrats.aop.withaop.aspects.BusinessUnitPersistenceAspect.find*(..))")    Pointcut businessUnitPersistenceAspect;    // Other point cut definitions    @Expression("businessUnitPersistenceAspect ||                             employeePersistenceAspect ||                              managerPersistenceAspect")    Pointcut allPersistencePointcuts;    private Map fileNames;    public BufferedFileReaderAspect() {        System.out.println("BufferedFileReaderAspect created");        fileNames = new HashMap();        fillFileNames();    }    @Before("allPersistencePointcuts")    public void assignReader(JoinPoint joinPoint) throws Throwable {        System.out.println("assignReader advice called");        Object callee = joinPoint.getCallee();        IBufferedFileReaderConsumable bufReaderConsumable = (IBufferedFileReaderConsumable)callee;        Class persistenceClass = callee.getClass();        String fileName = fileNames.get(persistenceClass);        FileReader fileReader = new FileReader(fileName);        BufferedReader bufferedReader = new BufferedReader(fileReader);        bufReaderConsumable.setBufferedReader(bufferedReader);     }    @AfterFinally("allPersistencePointcuts")    public void releaseReader(JoinPoint joinPoint) throws Throwable {        // We would release the buffered reader resource    }     // Other methods}

The code tries to create a point cut for all the method names, starting with find in the BusinessUnitPersistenceAspect class. Whenever these methods are called, the aspect class method assignReader is called before the find method executes. Here, it retrieves the callee class instance and sets the newly created buffered reader.

Similarly, in the releaseReader method, the code closes the buffered reader set previously. This section explains only the @before and @AfterFinally point cuts, which are defined using annotations in J2SE 5.0. Alternatively, you can declare them in the aspect definition XML file. You can examine the aop.xml file in the etc folder in the example code. (To get to know various types of point cuts, refer to the links in the Related Resources section in the left-hand column.)

Persistence

As previously discussed, the OOP approach for application persistence fills the Map with BusinessUnit instances. In the highlighted code below, the defined advice method findAllBusinessUnits would be invoked when the findAllBusinessUnits method in the BusinessUnitService class is called:

@Aspect("perJVM")public class BusinessUnitPersistenceAspect implements IBufferedFileReaderConsumable {    private BufferedReader buffFileReader;    @Before("execution(Collection org.javatechnocrats.aop.withaop.BusinessUnitService.findAllBusinessUnits())")    public void findAllBusinessUnits(JoinPoint joinPoint) throws Throwable {        System.out.println("findAllBusinessUnits advice called");        Map businessUnits =                                      ((BusinessUnitService)joinPoint.getThis()).getBusinessUnits();        String businessUnitRecord;        while((businessUnitRecord = buffFileReader.readLine()) != null) {            BusinessUnit businessUnit = BusinessUnitFactory.createBusinessUnit(businessUnitRecord);            businessUnits.put(businessUnit.getId(), businessUnit);        }    }    public void setBufferedReader(BufferedReader buffFileReader) {        System.out.println("BusinessUnitPersistenceAspect.setBufferedReader called");        this.buffFileReader = buffFileReader;    }    public BufferedReader getBufferedReader() {        System.out.println("BusinessUnitPersistenceAspect.getBufferedReader called");        return this.buffFileReader;    }}

The advice method reads the records from storage and uses the factory class to create BusinessUnit class instances. The created instances are then added to the Map, which handles the persistence aspect of the application.

Logging

The example in this article doesn’t contain the complete AOP solution for logging. However, it has defined a point cut for the java.lang.Object class’s toString method in order to get the debug information about domain class objects. Hence, the domain classes don’t need to implement the toString method to form a debug string using a StringBuffer, which you normally do for all the classes in an application:

@Aspect("perJVM")public class LoggingAspect {    @Around("execution(String org.javatechnocrats.aop.withaop..*.toString())")    public Object toStringAdvice(JoinPoint joinPoint) throws Throwable {        System.out.println("toStringAdvice called");        String toString = (String)joinPoint.proceed();        Object target = joinPoint.getThis();        Field fields[] = target.getClass().getDeclaredFields();        List members = new ArrayList(fields.length + 1);        members.add(toString);        for(Field field : fields) {            field.setAccessible(true);            Object member = field.get(target);            members.add(field.getName() + "=" + member);        }        return members.toString();    }

You can use this sample code to complete the error handling aspect as well.

Dig into the Code

To gain an understanding of the OOP design for the example requirement, refer to the source code download and do the following:

  • Start analyzing the EmployeeServiceTestCase code in the oldway package.
  • Examine the testEmployeeCredit method.
  • Look through the domain classes Employee and BusinessUnit.
  • Learn the concepts behind service, repository, and factory. These are the artifacts used in a domain-driven design.
  • Look at each of the service, repository, and factory classes at a higher level in the oldway package.

For AOP, follow these steps:

  • Analyze the EmployeeServiceTestCase code in the newway package.
  • Examine the service, repository, and factory classes. These classes will be more or less the same those of the previous one, except that you would let advices intercept the flow of the application.
  • Study the aspect classes to learn the point cut definitions.

The following instructions walk you through executing the program:

  • Download AspectWerkz 2.0.
  • Set the following environment variables:
    set JAVA_HOME=c:Program FilesJavajdk1.5.0set ASPECTWERKZ_HOME=C:aw_2_0_2set PATH=%PATH%;%ASPECTWERKZ_HOME%inset CLASSPATH=C:aw_2_0_2libaspectwerkz-2.0.RC2.jar;C:aw_2_0_2libaspectwerkz-jdk5-2.0.RC2.jar;
    classes;C: junit3.8.1 esourceslibjunit.jar
  • Extract the source and other files to a folder.
  • Compile the Java files, except the test case files. Otherwise, you will see an error during the weaving process.
  • Perform offline weaving. Assuming you extracted the files to c:aop and the class files are in c:aopclasses, navigate to c:aop and execute the following command:
    %ASPECTWERKZ_HOME%inaspectwerkz -offline etc/aop.xml -cp classes classes
  • The AOP framework will modify the classes to inject the necessary byte codes.
  • Compile the test case files and run them using JUnit text or GUI runner.

The ‘Takeaway’

Once you have completed the preceding instructions, you should have a strong understanding of the following:

  • Cross cutting concerns in an application
  • The meaning of an aspect with respect to AOP
  • How to use AOP to move the cross cutting concerns out of the domain layer in an application by using point cuts and advices
  • The differences between the flow of program control within the classes when using OOP and AOP in a given scenario

Hopefully, you also gained a new perspective on real world programming from this article. You should feel confident in using AOP to improve the design, modularity, and reusability of the code in your projects. At the very least, you can start using AOP for the logging, error-handling, and persistence aspects of applications.

Personally, I find the learning curve for AOP steep. Specifically, understanding the syntax for defining point cuts is a chore. Ideally, you would use OOP to design domain models and use AOP to move the concerns that cut across the domain classes out, hence making them neat and readable.

Another downside of AOP is debugging can be difficult, because?unlike OOP?the program flow is not straightforward and the interactions are determined only at compile time or at runtime. I’m counting on some intelligent tools to address this issue in the near future.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist