Layering Applications

Introduction

Note that this is a quick, initial pass at this article, in response to a newsgroup post. I'll beef it up with more examples and diagrams when I get the chance...

Maintainable applications can be quite tricky to develop. One of the keys to creating such a beast, is to properly layer your application.

Separation is the key to maintainability. The more separate the parts of your application are, the lower the coupling between them, and changes to the guts of one part have less (or optimally no) effect on the rest of the application.

Your biggest enemy in the quest for a maintainable application is direct communication.

Let's get one thing out of the way right up front. Never talk directly to an EJB. I don't care what Sun or IBM says.

Reason? Simple. EJB is a technology, not a component in your application.

What are application components?

Think of your application as having components, similar to a stereo. We're not talking GUI Widgets here (AWT Components), we're talking about sizeable chunks of your application.

There are several benefits to a component stereo system:

Brilliant idea, eh?

We can apply this concept to software with some amazing results:

The key here is isolation. The more isolated your components, the more real the benefits become.

And what makes this possible?

Interfaces are the key

Think about what makes a component stereo work so well, or how we can use the same spark plugs in a Pacer (forgot about that, eh?) and a Rolls Royce.

The interface between the components is what makes it possible!

Components in a stereo use a common interface: RCA jacks and obfuscated setup manuals. Cars use common interfaces for their components as well, such as the threading and contacts for a spark plug.

In the same manner, interfaces are the key to separation in your application. If you have two components that need to talk with one another, define one or more interfaces to lock down that communication. Once you have the interfaces in place, it doesn't matter what the components actually are or do, as long as they respect that communication!

Think about this for a moment. Suppose we define:

public interface Customer {
  public String getName();
  public void setName(String name);
  ...
}

public interface CustomerManager {
  public Customer   load(int id);
  public Customer   store(Customer customer);
  public Customer[] findByName(String namePattern);
  public Customer   createCustomer(String name, float creditLimit);
  public void       deleteCustomer(Customer customer);
}

Notice how this does not say anything about how or where the customer is stored. All we're doing here is specifying the basic CRUD (Create, Read, Update, Delete) operations that you need for a piece of data.

Wow, I can reuse things!

Sorry, bud, you missed the point...

I've often heard phrases like "software is like cars; you can reuse spark plugs, pistons, even the whole engine."

Can you say "apples and oranges"?

Sure, if all I wrote were personal information manager applications, I could reuse the notepad, address book and email components constantly. But do I really write the same type of application more than once??? Of course not!

(Note: Reusing things like MS Excel inside Word isn't a new type of app... they are both "office apps")

So why are components good, if not for reuse?

Think about stereos, refrigerators, and cars.

If you examine them at a microscopic level, they all reuse molecules. Go up a bit farther, and they all share transistors, but as your components grow bigger, they become more domain specific. Domain-specific components can really only be used in one type of application. You can't take your car's engine and use it to build a fridge.

Small enough components, like GUI Widgets, and data structures like Java's Vector and Hashtable, are incredibly reusable. But that's really low level.

Thinking of application components, the real benefit is in the upgrade and repair department, not the cross-application reuse department.

If you had separated your application into components, and one of those components is a CustomerManager (defined as the interface above), you're in great shape. Suppose your initial CustomerManager implementation uses JDBC calls to store and retrieve data. If you want to upgrade to EJB, all you'd need to do is change the implementation of the CustomerManager. (There's a few gotchas we'll hit later, such as generic exception handling, though. The simple definition of CustomerManager above isn't quite enough, but it's presented here to get the basic idea across.)

Application layering

Components fall into layers in your application. Each layer has a specific set of responsibilities, and defined communication with other layers.

Figure 1. Application Layers
Application Layers

The current ideal way to design your application is to provide nice, clean interfaces between layers:

[Note that I said "current ideal". Architecture evolves over time, and this is a current best practice. Newer and better techniques will come along, but they should grow from this approach.]

Each of the three layers has specific responsibilities, and can be composed of one or more components in your application. The dotted lines between the layers in Figure 1 represent the separation between the layers, and are written using one or more interfaces.

Any number of presentation components (GUIs, JSPs, Servlets, Consoles) can present data to the user and accept user input. They communicate with the business logic of the application to request data to display and ask for modifications to the data. The business logic asks the data managers for data, and may modify that data before returning it to the presentations.

Skipping layers is a no-no

Communication should never skip layers! This is incredibly important, though it may not seem obvious at first.

One of the key sins committed by application developers is having their user interface talk directly to the data managers. The business logic is there for a reason, folks! If you skip past the business logic, the "smarts" of the application doesn't have the opportunity to do anything with the request. At first, you may not see a need for passing through, but you should.

For example, suppose you're implementing a simple online store application. You may at first think it's ok for the presentation to directly ask the data managers for the list of items to display. Hey, it's just a list, after all...

But what happens when you want to put those items on sale? Ok, so you go and modify the presentations to list a 20% off price. But there could be a bazillion pages that display the item, and all it takes is one missed page to give an inconsistent presentation to the user.

So, you could change the data manager. But now all business logic sees the change.

If you had the presentation call the business logic, instead of directly calling the data managers, all you would need to do is change the business logic and all pages that use the business logic see the change! You could even implement filters that decorate the business logic (see Design Patterns, by Gamma et al) to transform the data.

Much more here later...

(This is just a placeholder... I'll go on and on some more here later, when I get the time...)

Generic exception handling

The hardest part of making your application nice and generic is dealing with exceptions.

Let's just think about our CustomerManager:

public interface CustomerManager {
  public Customer   load(int id);
  public Customer   store(Customer customer);
  public Customer[] findByName(String namePattern);
  public Customer   createCustomer(String name, float creditLimit);
  public void       deleteCustomer(Customer customer);
}

This definition has some serious problems. If anything goes wrong, the caller needs to know about it. So we'll need to declare some thrown exceptions for error cases.

But what exceptions should we throw?

Let's start by thinking about and implementation using JDBC. JDBC throws SQLExceptions (btw: SQL is properly pronounced "squeal", no Ned Beatty references, please.)

So we could write a class that looks like:

public class CustomerManagerUsingJDBC implements CustomerManager {
  public Customer   load(int id) throws SQLException {
    ...
  }

  public Customer   store(Customer customer) throws SQLException {
    ...
  }

  ...
}

and then modify the interface to also throw those exceptions. Ok so far, and we set up the business logic to catch SQLExceptions as necessary.

Uhhhh, but what about EJB?

But things get nasty if we try to change the implementation to use Enterprise JavaBeans:

public class CustomerManagerUsingEJB implements CustomerManager {
  public Customer   load(int id) throws EJBException, RemoteException {
    ...
  }

  public Customer   store(Customer customer) throws EJBException, RemoteException {
    ...
  }

  ...
}

Accessing EJBs could throw EJBException or RemoteException. Ok, so we'll modify the interface to use EJB, then change the code in the business logic.

Wrongo!!!

The whole point of this article is easier maintenance. Easier maintenance relies on one commandment: thou shalt not change thy interface!

Think about what this does. Anytime you change the data manager, you'd need to change the business logic to "match" its exceptions.

So, uhhhh, how do we deal with exceptions?

Ever hear of application-defined exceptions?

What is the actual problem from the point of view of the business logic?

Those are a few simple cases. Ok, let's define custom exceptions to deal with this.

public class MyApplicationException extends Exception {
  private Throwable nestedException;

  public MyApplicationException (String message, Throwable nestedException) {
    super(message);
    this.nestedException = nestedException;
  }
  public Throwable getNestedException() {
    return nestedException;
  }
}

public class CustomerException extends MyApplicationException {
  public CustomerException(String message, Throwable nestedException) {
    super(message, nestedException);
  }
}

public class NotFoundException extends MyApplicationException {
  public NotFoundException(String message, Throwable nestedException) {
    super(message, nestedException);
  }
}

Now we define the interface as follows. Note that this is how we should have defined it in the first place, so it won't require changes.

public interface CustomerManager {
  public Customer   load(int id) throws CustomerException, NotFoundException {
    ...
  }

  public Customer   store(Customer customer) throws CustomerException {
    ...
  }

  ...
}

Now the interface is totally generic, with respect to how we store the data! A sample JDBC implementation might look like:

public class CustomerManagerUsingJDBC implements CustomerManager {
  public Customer   load(int id) throws CustomerException, NotFoundException {
    try {
      // JDBC to try to fetch data
      if (resultSetIsEmpty)
        throw new NotFoundException("Customer " + id + " not found", null);
    }
    catch(NotFoundException e) {
      throw e;
    }
    catch(SQLException e) {
      throw new CustomerException("oops!", e);
    }
    ...
  }

  ...
}

The idea is to catch the specific exception and wrap it in an application exception. This allows the business logic to worry about the concept of the problem, without knowing anything about the details of how the data manager was implemented.

(more here later)

Enterprise JavaBeans and layering

The "trick" to using Enterprise JavaBeans is hiding them in the data management layer and business logic layer.

Generally:

(I'll add some stuff about EJB 2.0 message beans later...)

Your application should never directly use the home or remote interfaces of EJBs. It should use your application interfaces!

Entity Beans

Entity beans are really just another way to store data.

Start with our interfaces again:

public interface Customer {
  public String getName()          throws CustomerException;
  public void setName(String name) throws CustomerException;
  ...
}

public interface CustomerManager {
  public Customer   load(int id)
                      throws CustomerException, NotFoundException;
  public Customer   store(Customer customer) 
                      throws CustomerException;
  public Customer[] findByName(String namePattern) 
                      throws CustomerException, NotFoundException;
  public Customer   createCustomer(String name, float creditLimit) 
                      throws CustomerException;
  public void       deleteCustomer(Customer customer) 
                      throws CustomerException;
}

The key here is the cooperation between the implementations of Customer and CustomerManager. First, our Customer implementation might look like:

public class CustomerAsEntityBean implements Customer {
  private EntityCustomer entityCustomer;
  public CustomerAsEntityBean(EntityCustomer entityCustomer) {
    this.entityCustomer = entityCustomer;
  }

  public String getName() throws CustomerException {
    try {
      return entityCustomer.getName();
    }
    catch(Exception e) { // EJBException, RemoteException
      throw new CustomerException(e);
    }
  }
  ...
}

This customer is a proxy for our Entity Bean. Note that all requests are merely passed through to the entity bean.

There are several approaches to using entity beans like this:

Whatever implementation you choose makes no difference to the rest of your application (other than the data manager). All the rest of your application cares about is that it can ask a Customer for pieces of information and deal with problems accessing that data.

The final piece necessary to make this work is a data manager:

public class CustomerManagerUsingEJB interface CustomerManager {
  public Customer   load(int id)
                      throws CustomerException, NotFoundException {
    
    try {
      // set up naming context
      // grab home interface for entity
      // find entity (store as entityCustomer)
      return new CustomerAsEntityBean(entityCustomer);
    }
    catch(FinderException e) { 
      throw new NotFoundException("Customer " + id + " not found", e);
    }
    catch(Exception e) { //EJBException, RemoteException
    }
  }
  ...
}

Now all your business logic cares about is that it can ask some CustomerManager for some Customer. It doesn't need to even know that EJB was involved!

Session Beans

(Similar to entity beans, but replaces a business logic component -- remember -- HIDE the EJB access)

Tying it all together

We need something to tie all of the pieces together. That something is a Factory class. We'll use the Factory Method pattern (see Design Patterns by Gamma et al) to implement this.

A simple factory might look as follows:

public class MyApplicationFactory {
  private static CustomerManager customerManager =
                   new CustomerManagerUsingEJB();
  private static ApplicationLogic applicationLogic =
                   new ApplicationLogicAsSessionBean();

  public static CustomerManager getCustomerManager() {
    return customerManager;
  }
  
  public static ApplicationLogic getLogic() {
    return applicationLogic;
  }
}

You could even make things more flexible, keeping the names of the actual classes to use in a property file (I'll eventually provoide much more detail on how this works, and better error handling)

public class MyApplicationFactory {
  private static CustomerManager customerManager;
  private static ApplicationLogic applicationLogic;

  // note: needs **much** better error handling...
  //       this is just to give the idea... 
  static {
    try {
      InputStream in = 
        MyApplicationFactory.getResourceAsStream(
                               "application.properties");
      Properties p = new Properties();
      p.load(in);
      in.close();
      customerManager = 
        Class.forName(p.getProperty("manager.customer")).
              newInstance();
      applicationLogic = 
        Class.forName(p.getProperty("logic.application")).
              newInstance();
    }
    catch(Exception e) {
      // report error
    }
  }
 
  public static CustomerManager getCustomerManager() {
    return customerManager;
  }
  
  public static ApplicationLogic getLogic() {
    return applicationLogic;
  }
}

 

(more detail to come, including some simple examples)