Novell Home

Building Service-Oriented Business-Logic Components for J2EE and .NET

Novell Cool Solutions: Feature
By J. Jeffrey Hanson

Digg This - Slashdot This

Posted: 11 Oct 2004
 

J. Jeffrey Hanson
Chief Architect
eReinsure.com, Inc.
jeff@jeffhanson.com

This is the first of a series of articles that will discuss building service-oriented business-logic components that can be consumed by both J2EE and .NET applications. This article outlines techniques and best practices for building these business-logic components in J2EE.

Motivation for Building Heterogeneous Service Components

Typical platform-dependent development leaves object and component developers to worry about the internal details of the particular platform, such as object activation, type systems, protocols, platform standards, etc. A distributed, platform-dependent programming model must be able to exchange information between system demarcations in order to fulfill business needs. Platform-dependent information is structured in a near-sighted format which can only be interpreted with specific platform tools and technologies.

Over the past decade or so, developers have constructed software components using JavaBeans, EJB, CORBA, COM, ActiveX, and other component technologies. These technologies execute on various operating systems and platforms and rely on disparate protocols for remote method invocations including RMI, IIOP, DCOM, etc. Many efforts to make these component frameworks interoperable with each other have been attempted; however, inconsistencies continue to plague developers and interoperability remains largely untamed.

A service-oriented architecture (SOA) urges common techniques and best practices for building applications and services despite the given platform. A service-oriented architecture advises loosely-coupled, protocol-independent components which promote adaptability to a changing environment with evolving requirements.

Finding Common Ground for a Service Framework

Before we can attempt to tackle the task of designing a framework which will expose a usable abstraction over J2EE and .NET, we need to find the commonalities between the two and exploit these with what is sometimes referred to as a least-common-denominator (LCD) approach.

To begin to uncover commonalities between business-logic components in J2EE and .NET, let's look at invocation protocols and associations used with both technologies. The diagram in figure 1 illustrates the typical interactions between clients and business-logic components in J2EE and .NET.


Figure 1: Business Logic Interactions with J2EE and .NET

As we can see in figure 1, the prevailing similarity across all interactions is SOAP-over-HTTP. With this in mind and knowing that J2EE and .NET both support SOAP-over-HTTP; let's make SOAP/HTTP the protocol on which to base our framework.

SOAP and HTTP are technologies which are a natural fit for Web services and general service-oriented frameworks. This is the reason we will choose a service-oriented architecture for the design of our framework. I will show in the next sections how service-oriented principles can be used to build J2EE services.

Building on the Principles of a Service-Oriented Architecture

A service-oriented architecture has a number of principles to follow in order to enjoy the benefits of heterogeneous message handling, service reuse, etc. The following sections discuss some of these principles and how they can be applied in J2EE.

Stateless Interactions

Service-oriented architecture (SOA) recommends that services should be independent, self-contained modules, which do not store state from one request to another. In addition, services should not depend on the context or state of other services. Any state-dependencies should be defined using business processes, and data models, rather than context objects or session keys.

Stateless services depend on each input message to supply all the information that the service needs to perform its given task. Similarly, output messages from stateless services should supply all the information the caller needs in order to restore a given state and complete a conversation in a single call rather than an sequence of multiple, interrelated calls.

Coarse-Grained Interfaces

One of the primary benefits of service-oriented architecture (SOA) is the ability to compose applications, processes, or more complex services from other less complex services. This activity, sometimes called service composition, allows developers to compose applications and processes using services from heterogeneous environments without regard to the details and differences of those environments. This SOA feature depends greatly on the services being constructed and exposed with coarse-grained interfaces.

Using coarse-grained interfaces, a system of services controls access to the objects referenced by each service. While each service may be implemented as an abstraction on a group of finer-grained objects, the objects themselves can be hidden from public access. Each service can be implemented by grouping objects, components, and fine-grained services, and exposing them as a single unit using facades or interfaces.

To illustrate the concept of coarse-grained interfaces, let's take a couple of simple objects and compose a more useful business service out of them by applying a new facade. Assume that we have one class called UserInfo and another class called UserAccount. We can make a useful business process, getUserAccounts, out of these classes by abstracting a new interface to represent the process. We'll use Java as our target development language.

The code in listing 1 defines a simple, stateless, coarse-grained class encapsulating user information:
package com.jeffhanson.services;

public class UserInfo
{
   private static ArrayList instances = new ArrayList();

   private String name;
   private String address;
   private String city;
   private String state;
   private String postalCode;

   public static UserInfo getInstance(String name,
                                      String address,
                                      String city,
                                      String state,
                                      String postalCode)
   {
      Iterator iter = instances.iterator();
      while (iter.hasMore())
      {
         UserInfo userInfo = (UserInfo)iter.next();
         if (userInfo.compare(name, address, city, state, postalCode) == true)
         {
            return userInfo;
         }
      }

      UserInfo userInfo = new UserInfo(name, address, city, state, postalCode);
      instances.add(userInfo);
      return userInfo;
 }

   private UserInfo(String name,
                    String address,
                    String city,
                    String state,
                    String postalCode)
   {
      this.name = name;
      this.address = address;
      this.city = city;
      this.state = state;
      this.postalCode = postalCode;
   }

   public String getName()
   {
      return name;
   }

   public String getAddress()
   {
      return address;
   }

   public String getCity()
   {
      return city;
   }

   public String getState()
   {
      return state;
   }

   public String getPostalCode()
   {
      return postalCode;
   }

   public boolean compare(String name,
                          String address,
                          String city,
                          String state,
                          String postalCode)
   {
      if (this.name.equalsIgnoreCase(name) &&
          this.address.equalsIgnoreCase(address) &&
          this.city.equalsIgnoreCase(city) &&
          this.state.equalsIgnoreCase(state) &&
          this.postalCode.equalsIgnoreCase(postalCode))
      {
         return true;
      }

      return false;
   }

   public boolean compare(UserInfo obj)
   {
      if (name.equalsIgnoreCase(obj.getName()) &&
         address.equalsIgnoreCase(obj.getAddress()) &&
         city.equalsIgnoreCase(obj.getCity()) &&
         state.equalsIgnoreCase(obj.getState()) &&
         postalCode.equalsIgnoreCase(obj.getPostalCode()))
      {
         return true;
      }

      return false;
   }
}
The code in Listing 2 defines a simple, stateless, coarse-grained class encapsulating account information for a user:
package com.jeffhanson.services;

public class UserAccount
{
   private static ArrayList instances = new ArrayList();

   private UserInfo userInfo;
   private String name;
   private String description;
   private double balance;

   public static UserAccount getInstance(UserInfo userInfo,
                                         String name,
                                         String description,
                                         double balance)
   {
      Iterator iter = instances.iterator();
      while (iter.hasMore())
      {
         UserAccount userAccount = (UserAccount)iter.next();
         if (userAccount.compare(userInfo, name, description, balance) == true)
         {
            return userInfo;
         }
      }

      UserAccount userAccount =
         new UserAccount(userInfo, name, description, balance);
      instances.add(userAccount);
      return userAccount;
   }

   private UserAccount(UserInfo userInfo,
                       String name,
                       String description,
                       double balance)
   {
      this.userInfo = userInfo;
      this.name = name;
      this.description = description;
      this.balance = balance;
   }

   public UserInfo getUserInfo()
   {
      return userInfo;
   }

   public String getName()
   {
   return name;
   }

   public String getDescription()
   {
      return description;
   }

   public double getBalance()
   {
      return balance;
   }

   public boolean compare(UserInfo userInfo,
                          String name,
                          String description,
                          double balance)
   {
      if (this.userInfo.compare(userInfo) &&
          this.name.equalsIgnoreCase(name) &&
          this.description.equalsIgnoreCase(description) &&
          this.balance == balance)
      {
         return true;
      }

      return false;
   }

   public boolean compare(UserAccount obj)
   {
      if (this.userInfo.compare(obj.getUserInfo()) &&
          this.name.equalsIgnoreCase(obj.getName()) &&
          this.description.equalsIgnoreCase(obj.getDescription()) &&
          this.balance == obj.getBalance())
      {
         return true;
      }

      return false;
   }
}

Loose Couplings

Coupling generally refers to the act of joining two things together, such as the links in a chain. In software engineering, coupling typically refers to the degree to which software components/modules depend upon each other. The degree to which components are linked defines whether they operate in a tightly coupled relationship or in a loosely coupled relationship. With tightly coupled components, each component and any auxiliary dependent objects must be present at execution time as well as compile time. On the other hand, loosely coupled components can operate independently from one another.

Couplings are creating in software development when a developer uses normal programming techniques to relate one or more components to each other. Coupling also occurs when two interacting components need to exist in the same processing space at runtime. This type of coupling stipulates that deployment of one component depends on the presence of the other.

Not all coupling is bad. In fact, all nontrivial programs require some coupling. However, coupling becomes problematic when it limits the ability of the application. These limitations might show up in development, testing, and deployment of the application. For most applications, minimizing the coupling between components of the application provides far-reaching dividends.

Loosely coupled components locate and communicate with each other dynamically at runtime as opposed to a static compile-time binding. This is often referred to as late binding. It allows a loosely coupled application to be deployed as desired, without the limitations that tight coupling imposes. The right deployment decisions can be made at deployment time, rather than having to design in how the application will be deployed at the outset of development.

Service-oriented architecture (SOA) recommends that services should be constructed with loose-coupling in mind. An example of how SOA can enjoy loosely coupled benefits is illustrated by the Web services find-bind-invoke model.

Web services represent an increasingly popular SOA methodology for deploying services, enabling location transparency by utilizing registries such as UDDI for runtime discovery. Clients can locate the desired service dynamically by requesting the service from the registry. The Web services architecture provides benefits of loose coupling by providing a mechanism to find, bind, and invoke the service dynamically.

Another technique for helping to ensure loose-coupling is to require that services accept only data, and not objects, as parameters to method calls. When a method call accepts an object as a parameter, there is an implication of behavior, since the object combines data with code. Once objects and behavior cross component, object, or service boundaries; tight couplings are very easily formed. Message passing is one familiar technique to pass data between services. Since no behavior is shared when message-data is passed, tight couplings are difficult to form.

The code in listing 3 illustrates a service interface defining a loosely-coupled, coarse-grained, and stateless, business-logic service:

package com.jeffhanson.services;
public interface FinanceServices
{
   public UserAccount[] getUserAccounts(UserInfo userInfo);
}

With our interface in place, we can now compose instances of our two classes into a loosely-coupled, coarse-grained, stateless, business-logic service, as shown in listing 4.

package com.jeffhanson.services;

import java.util.Vector;

public class FinanceServicesImpl
   implements FinanceServices
{
   private UserAccount[] allUserAccounts;

   public static FinanceServices getInstance(UserAccount[] allUserAccounts)
   {
      FinanceServices financeServices =
         new FinanceServicesImpl(allUserAccounts);
      return financeServices;
   }

   private FinanceServicesImpl(UserAccount[] allUserAccounts)
   {
      this.allUserAccounts = allUserAccounts;
   }

   public UserAccount[] getUserAccounts(UserInfo userInfo)
   {
      Vector resultList = new Vector();

      for (int i = 0; i < allUserAccounts.length; i++)
      {
         if (allUserAccounts[i].getUserInfo().compare(userInfo) == true)
         {
            resultList.add(allUserAccounts[i]);
         }
      }

      UserAccount[] results = new UserAccount[resultList.size()];
      resultList.copyInto(results);

      return results;
   }
}

Conclusion

Applying SOAP/HTTP and SOA principles, we can construct services which are stateless, coarse-grained, and loosely-coupled. This is a sound foundation for which to base a J2EE service framework. In our next article, we will see how to apply these same principles to a .NET implementation of the same type of services.

For Additional Information

For more information about the technologies discussed in this Cool Solution for Developers, refer to the following resources:


Novell Cool Solutions (corporate web communities) are produced by WebWise Solutions. www.webwiseone.com

© 2014 Novell