Newsletter sign-up

Sign up for our technology specific newsletters.

Enterprise Java
View all newsletters

Email Address:

I want my AOP!, Part 1

Separate software concerns with aspect-oriented programming

Page 2 of 5

Currently, OOP serves as the methodology of choice for most new software development projects. Indeed, OOP has shown its strength when it comes to modeling common behavior. However, as we will see shortly, and as you may have already experienced, OOP does not adequately address behaviors that span over many -- often unrelated -- modules. In contrast, AOP methodology fills this void. AOP quite possibly represents the next big step in the evolution of programming methodologies.

View the system as a set of concerns

We can view a complex software system as a combined implementation of multiple concerns. A typical system may consist of several kinds of concerns, including business logic, performance, data persistence, logging and debugging, authentication, security, multithread safety, error checking, and so on. You'll also encounter development-process concerns, such as comprehensibility, maintainability, traceability, and evolution ease . Figure 1 illustrates a system as a set of concerns implemented by various modules.

Figure 1. Implementation modules as a set of concerns

Figure 2 presents a set of requirements as a light beam passing through a prism. We pass a requirements light beam through a concern-identifier prism, which separates each concern. The same view also extends towards development-process concerns.

Figure 2. Concern decomposition: The prism analogy

Crosscutting concerns in a system

A developer creates a system as a response to multiple requirements. We can broadly classify these requirements as core module-level requirements and system-level requirements. Many system-level requirements tend to be orthogonal (mutually independent) to each other and to the module-level requirements. System-level requirements also tend to crosscut many core modules. For example, a typical enterprise application comprises crosscutting concerns such as authentication, logging, resource pooling, administration, performance, and storage management. Each crosscuts several subsystems. For example, a storage-management concern affects every stateful business object.

Let's consider a simple, but more concrete, example. Consider a skeleton implementation of a class encapsulating some business logic:

public class SomeBusinessClass extends OtherBusinessClass {
    // Core data members
    // Other data members: Log stream, data-consistency flag
    // Override methods in the base class
    public void performSomeOperation(OperationInformation info) {
        // Ensure authentication
        // Ensure info satisfies contracts
        // Lock the object to ensure data-consistency in case other 
        // threads access it
        // Ensure the cache is up to date
        // Log the start of operation
        // ==== Perform the core operation ====
        // Log the completion of operation
        // Unlock the object
    }
    // More operations similar to above
    public void save(PersitanceStorage ps) {
    }
    public void load(PersitanceStorage ps) {
    }
}


In the code above, we must consider at least three issues. First, other data members do not belong to this class's core concern. Second, implementation of performSomeOperation() seems to do more than perform the core operation; it seems to handle the peripheral logging, authentication, multithread safety, contract validation, and cache management concerns. In addition, many of these peripheral concerns would likewise apply to other classes. Third, it is not clear if save() and load() performing persistence management should form the core part of the class.

Crosscutting concern problems

Although crosscutting concerns span over many modules, current implementation techniques tend to implement these requirements using one-dimensional methodologies, forcing implementation mapping for the requirements along a single dimension. That single dimension tends to be the core module-level implementation. The remaining requirements are tagged along this dominant dimension. In other words, the requirement space is an n-dimensional space, whereas the implementation space is one-dimensional. Such a mismatch results in an awkward requirements-to-implementation map.

Symptoms

A few symptoms can indicate a problematic implementation of crosscutting concerns using current methodologies. We can broadly classify those symptoms into two categories:

  • Code tangling: Modules in a software system may simultaneously interact with several requirements. For example, oftentimes developers simultaneously think about business logic, performance, synchronization, logging, and security. Such a multitude of requirements results in the simultaneous presence of elements from each concern's implementation, resulting in code tangling.
  • Code scattering: Since crosscutting concerns, by definition, spread over many modules, related implementations also spread over all those modules. For example, in a system using a database, performance concerns may affect all the modules accessing the database.


Implications

Combined, code tangling and code scattering affect software design and developments in many ways:

  • Poor traceability: Simultaneously implementing several concerns obscures the correspondence between a concern and its implementation, resulting in a poor mapping between the two.
  • Lower productivity: Simultaneous implementation of multiple concerns shifts the developer's focus from the main concern to the peripheral concerns, leading to lower productivity.
  • Less code reuse: Since, under these circumstances, a module implements multiple concerns, other systems requiring similar functionality may not be able to readily use the module, further lowering productivity.
  • Poor code quality: Code tangling produces code with hidden problems. Moreover, by targeting too many concerns at once, one or more of those concerns will not receive enough attention.
  • More difficult evolution: A limited view and constrained resources often produce a design that addresses only current concerns. Addressing future requirements often requires reworking the implementation. Since the implementation is not modularized, that means touching many modules. Modifying each subsystem for such changes can lead to inconsistencies. It also requires considerable testing effort to ensure that such implementation changes have not caused bugs.


The current response

Since most systems include crosscutting concerns, it's no surprise that a few techniques have emerged to modularize their implementation. Such techniques include mix-in classes, design patterns, and domain-specific solutions.

With mix-in classes, for example, you can defer a concern's final implementation. The primary class contains a mix-in class instance and allows the system's other parts to set that instance. For example, in a credit card processing example, the class implementing business logic composes a logger mix-in. Another part of the system could set this logger to get the appropriate logging type. For example, the logger could be set to log using a filesystem or messaging middleware. Although the nature of logging is now deferred, the composer nevertheless contains code to invoke logging operations at all log points and controls the logging information.

Behavioral design patterns, like Visitor and Template Method, let you defer implementation. However, just as in case with mix-ins, the control of the operation -- invoking visiting logic or invoking template methods -- stays with the main classes.

Domain-specific solutions, such as frameworks and application servers, let developers address some crosscutting concerns in a modularized way. The Enterprise JavaBeans (EJB) architecture, for example, addresses crosscutting concerns such as security, administration, performance, and container-managed persistence. Bean developers focus on the business logic, while the deployment developers focus on deployment issues, such as bean-data mapping to a database. The bean developer remains, for the most part, oblivious to the storage issues. In this case, you implement the crosscutting concern of persistence using an XML-based mapping descriptor.

The domain-specific solution offers a specialized mechanism for solving the specific problem. As a downside to domain-specific solutions, developers must learn new techniques for each such solution. Further, because these solutions are domain specific, the crosscutting concerns not directly addressed require an ad hoc response.

The architect's dilemma

Good system architecture considers present and future requirements to avoid a patchy-looking implementation. Therein lies a problem, however. Predicting the future is a difficult task. If you miss future crosscutting requirements, you'll need to change, or possibly reimplement, many parts of the system. On the other hand, focusing too much on low-probability requirements can lead to an overdesigned, confusing, bloated system. Thus a dilemma for system architects: How much design is too much? Should I lean towards underdesign or overdesign?

For example, should an architect include a logging mechanism in a system that does not initially need it? If so, where should the logging points be, and what information should be logged? A similar dilemma occurs for optimization-related requirements -- with performance, we seldom know the bottlenecks in advance. The usual approach is to build the system, profile it, and retrofit it with optimization to improve the performance. This approach requires potentially changing many system parts indicated by profiling. Further, over time, new bottlenecks may appear due to changed usage patterns. The reusable library architect's task is even more difficult because he finds it harder to imagine all the usage scenarios for the library.

Resources