[ Team LiB ] |
9.1 Abstracting Business LogicThe last few chapters explored ways to keep the presentation tier and the business tier separate. We accomplished this by wrapping the domain logic for the system in both regular and Enterprise JavaBeans and introducing various controller patterns, including the MVC and Service to Worker patterns, in order to mediate the interaction between the tiers. This decoupling makes the overall system easier to understand and maintain. But without a good way to communicate between the tiers, the advantages can quickly be lost. The Business Delegate and Business Delegate Factory patterns provide client-side objects that flexibly connect the presentation and business tiers. 9.1.1 Business DelegateIn an MVC design, the controller still has to interact directly with the business tier (the model). The view will do so as well, although in a read-only fashion and mediated through the controller. This exposes the underlying implementation details of the business tier to the presentation tier. The application needs to know enough about the business tier to perform the operations requested by the client, and enough about the networking infrastructure between the presentation tier and the business tier to locate the business objects in the first place. The result is a tightly coupled application. The Business Delegate pattern defines a client-side object that handles communications with the business objects. It fulfills the model role in an MVC framework. The business delegate doesn't necessarily contain any business logic itself, but encapsulates knowledge about how to locate, connect to, and interact with the business objects that make up the application. These objects might be in the same JVM, or they might be remote Enterprise JavaBeans, CORBA components, web services, or other resources. The business delegate is also responsible for returning enough data to the controller to allow the controller to instantiate an appropriate view. The business delegate wraps access to a series of business objects and business services into a single, easy-to-implement application call. It can also incorporate all of the networking code required to access those resources, including any retries, caching, or exception handling that might be required. Networking or database access exceptions can be converted into application exceptions for consistent and user-friendly handling. The UML diagram in Figure 9-1 shows a basic set of object relationships for an application using business delegates. The client interacts with the business delegate object, which in turn interacts with a set of business services. Some of these services are instantiated directly, and others are retrieved via a service locator class. The service locator provides a directory interface to locate the various business services used by the business delegate. In EJB environments, this is often a JNDI server. We cover service locators later in this chapter. Figure 9-1. Business delegateThe major difference between our definition of Business Delegate and others is that we don't consider them to be pure EJB front ends. Rather, we consider them the client-side equivalent of an EJB session bean: an object that contains a front end to the process logic underlying an application. It doesn't matter whether the business delegate object implements that logic itself, parcels it out to other components, or does a little of both. The presentation tier delegates business process responsibilities to the business delegate, and that's more or less the end of that. Our version varies from the version presented in Sun's J2EE Patterns collection.[1] For one thing, we make the Service Locator component optional. While Service Locators are often a good idea, we don't consider them vital to a business delegate implementation. 9.1.1.1 Organizing business logic into business delegatesIf none of the external resources available to the business delegate are capable of incorporating all of the business logic, then the business delegate's own implementation should provide the missing elements. For example, consider a use case that involves retrieving stock information from a web service, computing a shipping charge, and submitting stock updates and shipping information to two additional web services. Because business delegates can be shared, we don't consider it bad practice to include the code for computing shipping charges in the business delegate implementation. Of course, it does make sense to consider ways to move that functionality into other areas that can be reused more easily, but that's almost always true. Incorporating the code directly into the presentation tier (having a servlet compute the charges before submitting the order), on the other hand, would easily prove problematic. The quantity of business logic included in the business delegate depends on the application's persistence model. If the business tier consists exclusively of EJB entity beans that represent the data model, the business delegate will, of necessity, have to incorporate a substantial amount of domain knowledge. It must know which entity beans to create, remove, or update to perform a particular activity. A delegate for a legacy interface, on the other hand, may simply relay calls to an existing API that handles object management. In both cases, we consider the business delegate to be part of the business logic tier, even if the object itself is instantiated at the client tier. We'll talk more about the division of business logic between business delegates and EJBs later in this chapter, when we discuss the Session Façade pattern. 9.1.1.2 Business delegate implementationIn an iterative or distributed environment, the business delegate can be stubbed out first, allowing development to proceed on the presentation tier while the ultimate business logic implementation is worked out. For applications that include complex relationships with external systems, this approach substantially accelerates development. Example 9-1 shows a very simple business delegate. It performs a data validity check and then uses a DAO object to update the domain objects. In a real application, the business delegate doesn't need to be much more complex. It should be easy to see how we would convert this class to an EJB session bean. Example 9-1. A business delegatepublic class PatientManagementDelegate { public static PatientDTO createNewPatient(PatientDTO patient) throws InvalidPatientException{ if(patient == null|| patient.getFirstName( ) == null || patient.getLastName( ) == null) throw new InvalidPatientException( "Patient Records Require Full Name"); PatientDAO pDAO = PatientDAOFactory.getPatientDAO( ); PatientDTO newPatientRecord = pDAO.createPatient(patient); return newPatientRecord; } } The implementation of the business delegate can include other patterns as well, further hiding the business logic's complexity from the client. For example, a business delegate can use the Gang of Four Strategy pattern to retrieve data from different sources depending on runtime requirements or changing infrastructure (we touched on the Strategy pattern in Chapter 2). In an EJB environment, business delegates usually work as a front end to a session bean (specifically, to a session façade, which we'll discuss later in this chapter). Implementing the presentation tier-to-EJB tier connection this way insulates the presentation tier programmers from the EJB environment, and simplifies error handling, since the various EJB exceptions can be caught, handled, and either resolved within the delegate or reported to the presentation layer in a consistent, simplified manner. A business delegate can throw exceptions related to the business process rather than exceptions related to the EJB environment. 9.1.1.3 Nesting business delegatesSince a business delegate is an access point to business logic and the domain model, there's no reason it can't use the resources of other business delegates. Business delegates can be nested arbitrarily deep, particularly when you're not in an EJB environment. This ability allows you to create separate business delegates for each use case and combine them as needed to implement the master use cases for your application. For example, Figure 9-2 shows how a servlet, accessing a business delegate object to submit an order to a purchasing system, might actually invoke three different delegates, even though the servlet only sees one of them. Figure 9-2. Nested business delegatesNesting business delegates means that we have to keep track of the resources each delegate uses. For example, if each call to a business delegate retrieves a database connection and doesn't return it until all processing is completed (whether by instantiating DAO objects or retrieving the connection directly), a call stack containing five business delegate instances might hold onto five separate database connections. This means that a pool of 25 database connections can only support 5 concurrent users. Using nested business delegates in an EJB application could result in inappropriate connections between different EJBs. If a session bean uses a business delegate to access another session bean, the business delegate may have to make a remote connection to the second bean, even if it's located in the same EJB server. If the session bean connected directly, it could easily use local interfaces instead. We'll address this issue later in this chapter with the Session Façade pattern. The other major issue raised by nested business delegates is transaction control. A presentation tier programmer will, absent indications to the contrary, expect that each call to a business delegate method takes part in one transaction. If each business delegate creates its own transaction, the nested delegates will handle their transactions separately from the parent delegate. At best, this process results in incomplete rollbacks when the parent delegate fails; at worst, it will result in database resource locks and the application slowing or grinding to a halt. Since this isn't a simple issue, we'll look at transaction issues in enterprise applications in much more detail in the next chapter, including a pattern, Transactional Context, which addresses the problem specifically. 9.1.1.4 Stateful business delegatesBusiness delegates can be stateful or stateless. To use a stateful business delegate, generate an instance of the business delegate class. To create a stateless business delegate, declare the business delegate methods as static, and call them accordingly. You can also make the business delegate class a singleton. Either way, all classes running in the same Java VM and class loader will share a stateless business delegate. For web applications, this means that all the code will share the delegate. The tradeoffs between stateful and stateless business delegates are almost identical to the tradeoffs between stateful and stateless session beans in EJB. Stateful delegates require more resources and can be marginally slower, due to the overhead of creating the additional objects. On the other hand, they allow you to configure a particular instance to your needs, making the delegates themselves easier to reuse. Stateless delegates require less memory and present a simpler interface to the programmer. Here's a rule of thumb: if a business delegate requires a resource that is both expensive to create and that must be created specifically for the current user (such as a JAAS security principal), and if that resource will be used multiple times within the same delegate by the same thread, use a stateful delegate. You should also use a stateful delegate if the delegate needs complex configuration specifically for the current invocation, rather than trying to pass in all configuration information with each method call. Otherwise, look for opportunities to use a stateless delegate. 9.1.2 Business Delegate Factory PatternSince the simplest business delegates are just objects with static methods, we don't need to worry about creating or configuring them. But once we start using stateful business delegates, things get more complicated—even with stateless delegates we can eventually accumulate so many of them that it becomes hard to keep track. Architectures using business delegates face the following problems:
We can address these points with the Business Delegate Factory pattern. The Business Delegate Factory pattern is specialization of the Gang of Four Factory pattern, much as we saw in Chapter 8 with the DAO Factory pattern. A business delegate factory is a single object responsible for creating instances or one or more kinds of business delegates. Depending on your application, the factory can produce a particular kind of business delegate (resulting in one factory per delegate class), or a set of delegates related by their configuration requirements or general functionality. Having a broad-spectrum factory makes it easier to find the various business delegates within an application, but also introduces additional compilation dependencies. Like business delegates themselves, business delegate factories can be stateful or stateless. The same rules apply: if the factory needs to be configured for the current thread/user/block of code, or if creating a new delegate instance requires an expensive resource that cannot be shared between all users of the application, consider a stateful factory. Otherwise, consider a stateless factory. Figure 9-3 shows the sequence diagram for a stateful factory creating a stateful business delegate. It's a simple process: the client application creates a new instance of the factory by calling a static newInstance( ) method. It then calls a setAttribute(...) method to configure the factory, and finally requests an instance of a particular business delegate type. The factory creates the delegate instance and returns it to the client. At the end of their useful lives, both the factory and delegate objects go out of scope and are garbage-collected. Figure 9-3. Stateful business delegate factoryA stateless business delegate factory would look similar but could include all of the code executed in the newInstance( ) and setAttribute( ) methods in static initialization blocks or in the getBusinessDelegate( ) method, which would now be a static method on the delegate factory class. If we implement the delegate as a singleton, the initialization can still take place at startup. 9.1.2.1 Modifying business delegate behaviorThe Business Delegate Factory pattern lets you add functionality to your business delegates at runtime. As a simple example, consider logging. An application might need to support logging calls to business delegate methods but require different levels of detail, or different outputs, depending on the current status of the application. The simplest way of implementing this functionality (and the road most often taken) is to implement the logging functions in each business delegate object, perhaps tagging them with a "Loggable" interface, and toggling logging on or off via a centralized control class or at creation-time within the factory. Other solutions take better advantage of the object-oriented nature of Java and create wrapper classes for each business delegate. The factory can wrap each delegate in the appropriate logging wrapper when logging is required, and skip the step when it isn't. The problem is that we need to maintain a separate logging wrapper for each business delegate class, and update them at the same time we update the delegate. It's a lot of work, and a lot of class profusion. Obviously, we're leading up to something! The Java reflection API (which we've already seen in Chapter 8) includes a class called Proxy, which allows you to dynamically create an object that implements a given set of interfaces. An InvocationHandler object handles calls to methods on those interfaces, and the entire assemblage is referred to as a proxy object. Most uses of this class involve taking an existing object, extracting its interfaces, and creating a new object that implements those same interfaces while providing some additional functionality. The new object can delegate method calls to the original object or handle method invocations itself. Figure 9-4 shows how this can work in practice. Figure 9-4. Wrapping an object with an InvocationHandlerThe custom InvocationHandler includes a reference to the wrapped object. When the application calls methods on the proxy object, the invocation handler forwards the method on to the original object and returns the results to the application. Example 9-2 shows how we can apply this technique to the logging problem. The DelegateLoggingWrapper class includes a single method, decorate( ), and an inner class defining a LoggingWrapperHandler, which implements the InvocationHandler interface. The invoke( ) method prints out the name of the method and the arguments passed into it, invokes the method on the original object, prints another message confirming success, and returns the result of the method call. In the next chapter, we'll use this approach to handle transaction management within and across business delegates. Example 9-2. DelegateLoggingWrapper.javaimport java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; final class DelegateLoggingWrapper { /** * Decorates a business delegate object with a Logging Wrapper. * The object returned by this method will implement all of the * interfaces originally implemented by the target * and loaded by the same class loader as that of the target. * @param delegate The Business Delegate to wrap * @return The business delegate wrapped in this wrapper */ static Object decorate(Object delegate) { return Proxy.newProxyInstance( delegate.getClass().getClassLoader( ), delegate.getClass().getInterfaces( ), new LoggingWrapperHandler(delegate)); } static final class LoggingWrapperHandler implements InvocationHandler { private final Object delegate; LoggingWrapperHandler(Object delegate) { this.delegate = delegate; } /** Invoke the target method, but display logging information first. */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Invoked Business Delegate Method: " + method.getName( )); if (args != null) { for (int i = 0; i < args.length; i++) { System.out.print(args[i]); System.out.print( (i < (args.length - 1)) ? "," : "\n"); } } Object result = method.invoke(delegate, args); System.out.println("Completed Without Exception"); return result; } } } Example 9-3 shows how to use invocation proxies in the business delegate factory itself. We've created a static factory object that produces runtime instances of each delegate object (to save space, we've included only one delegate type). The user can call static methods on the class in order to start or stop logging. If logging is enabled, the DelegateLoggingWrapper is used to add logging capability. Note that we've shifted the logging/no-logging decision all the way down to the wrap( ) method, rather than placing it in the delegate creation methods. While this adjustment is marginally slower, it reduces the amount of code we need to write in each delegate creation method, and makes it easier to add support for additional wrappers later. Example 9-3. BusinessDelegateFactory with loggingpublic class BusinessDelegateFactory { private static boolean useLogging = false; public static PatientManagementDelegate getPatientManagementDelegate( ) { PatientManagementDelegate delegate = new PatientManagementDelegateImpl( ); return (PatientManagementDelegate)wrap(delegate); } private static Object wrap(Object o) { if(useLogging) return DelegateLoggingWrapper.decorate(o); return o; } public static void startLogging( ) { useLogging = true; } public static void stopLogging( ) { useLogging = false; } public static boolean getLogStatus( ) { return useLogging; } } You probably noticed that the proxy object requires interfaces, rather than concrete classes. Rather than maintain a single class for each business delegate we want to wrap, we now need to maintain an implementation class and an interface. This is simple enough to do (and still less work than maintaining separate wrapper classes for each delegate, since it's easy to update the interface at the same time a delegate changes: no additional code needs to be written). We assume you already know how to create an interface, but for completeness, Example 9-4 shows the interface for the patient management delegate. Example 9-4. PatientManagementDelegate.java (interface)public interface PatientManagementDelegate { public PatientDTO createNewPatient(PatientDTO patient) throws InvalidPatientException; } Example 9-4 expects a PatientManagementDelegateImpl object that implements this interface. The code for this class is exactly the same as in Example 9-1, but renamed and with an implements clause. This approach to modifying behavior on the fly does have performance implications. Each request for a business delegate now carries the overhead of creating the proxy object, which increases the time required to create a new business delegate instance. Each method call also incurs additional overhead, both for the reflection activity itself and whatever activities the wrapper performs. In absolute terms, the difference due to reflection is measured in microseconds, and we don't consider it significant, particularly given that the business delegates and the wrapper objects are likely to be participating in much more resource intensive activities. Since the Factory pattern itself is fundamental to good object-oriented programming (out of everything in the original Gang of Four book, it may well be the pattern most frequently encountered),[2] it's worth keeping the proxy approach in mind when implementing other areas of your system. We have already seen a number of different kinds of proxies, including the Decorators from Chapter 3.
|
[ Team LiB ] |