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:

Figure 1: Customer definition as an interface

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.

Application Layers

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

  • Presentation Layer (aka User Interface Layer)
    Displays data to user and accepts i/p -- no business logic at all!
     
  • Domain Layer (aka Business Logic Layer)
    The "smarts" of your application
     
  • Storage Layer (aka Data Management Layer)
    The code that fetches and saves data for you

[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:

Figure 2: A manager -- needs some exceptions...

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:

Figure 3: A JDBC-based customer manager

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:

Figure 4: An EJB-based customer manager

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?

  • "Can't get the data"
  • "Can't store the data"
  • "Data not found"

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

Figure 5: A custom exception for our application

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.

Figure 6: Using logical exceptions in the customer manager

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:

Figure 7: Using logical exceptions in the JDBC-based customer manager

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:

  • Entity beans represent data
  • Session beans represent business logic

(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:

Figure 8: Exceptions in the customer

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:

Figure 9: Entity bean implementation of customer

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:

  • Direct proxy - just forward all calls to the entity bean. Incredibly easy to implement, always up to date with remote bean, but can provide poor performance. There is no separate "store" implementation; all "set" calls are performed immediately in the remote server. This is the implementation shown above.
     
  • Copy bean - take a snapshot of the entity bean's remote data and store it locally. More difficult to properly implement, faster access overall, but not always up to date with remote bean. Care must be taken when storing.
     
  • Cache as we go - set up all calls to ask the remote bean for data, then cache that piece of data for later use. If some pieces of data aren't used, they won't affect performance on obtaining them as in a copy bean. Requires much more care, as if part of the data in the entity changes, we could get out of sync!

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:

Figure 10: New EJB-based customer 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:

Figure 11: Sample application factory

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)

Figure 12: Property-file-based application factory

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)