6.1. Designing the Service

6.1.1. Separating Service Interface and Implementation

The service should be designed with the interface and implementation separated. The characteristics and benefits of this practice are

  • The interface between the caller and the callee is well defined

  • The interface is the only coupling point between the caller and the callee

  • Any change made to the implementation portion of the callee does not impact the caller. We know that software always evolves. This minimizes the impact by bug fixes or feature enhancements, and significantly contributes to the stability of the entire software system.

  • We can apply useful abstractions. For example, a file system service can offer its callers simple abstractions of reading, writing, and listing files, but one can be implemented on top of local hard disks while another is implemented over networked file systems such as Network File System.

  • One implementation can be devoted to a very specific approach to maximize efficiency and performance without worrying about sacrificing generalization, which has already been captured in the interface. We are not excluding any alternatives because we can always supply another implementation. For example, a video service can provide a getFeed API. One implementation can concentrate on error correction and compensation with sophisticated image-processing techniques if the feed comes from a slow parallel port; another implementation can be spared this complexity altogether with a high bandwidth FireWire connection.

As a result, separation of interface and implementation encourages software reuse, improves flexibility, and reduces maintenance costs.

The OSGi framework provides further incentives for separating interface and implementation, because bundles are developed by different service providers (thus their implementation should be isolated), and they are expected to cooperate in a dynamic environment in which each may undergo life cycle transitions (hence their interfaces should be well defined and stable).

With standard interfaces defined, many bundles can be developed, but they needn't all be deployed. Some may serve as “spare parts” and may be taken off the shelf when their specific applications are called for. Having a software component market is not a new idea, but its practice is far from being widespread. That is perhaps one of the reasons why we begin by writing software but end up rewriting it, and software development remains to be a labor-intensive effort. But we digress.

It may be worth reviewing that the interface and implementation classes are separated in different packages, and only the packages containing service interface classes are exported. The implementation classes are kept private to the service-providing bundle. In the Java Embedded Server product, such behavior is realized through class loaders. Each installed bundle has its own class loader. By default, it can only load classes from within the same bundle. However, with the Export-Package manifest header, a bundle can instruct its class loader to export the specified classes to the framework. When another bundle declares Import-Package in its manifest, its class loader is “linked” with that of the exporter. That is, the importer delegates the loading and resolution of any class in the imported packages to the class loader of the exporting bundle. For example, assume bundle A exports the package foo, which bundle B imports. After the two bundles are started, when bundle B attempts to load class foo.ServiceA, system classes and CLASSPATH are searched. Failing that, B tries the exported packages, and succeeds in loading the class from A.

6.1.2. Challenges in Designing a Service Interface

Although the separation of interface from implementation offers many benefits, it also poses a challenge to the developer: The interface must be useful and stable. If the interface is changed often, the callers as well as the implementations are affected, and the benefits we have outlined are defeated.

Designing a good interface is hard. One must grasp the intrinsic functionality offered by the service, and foresee how it can be used. If the interface is too narrowly defined, many applications that could have used it can find it lacking. They are left with two awkward options: creating a new service interface that duplicates what most of the existing interface does, with the needed, additional API; or pushing for revision of the existing interface. The former may lead to a proliferation of similar interfaces; the latter results in interface changes that may break backward compatibility. Both cause a lost chance for algorithmic integration: Had all use scenarios of the interface been taken into consideration, similar behaviors or data structures could have been integrated, resulting in a more succinct and functional interface. When common factors reveal themselves after the interface has been set loose, you can only add new and possibly redundant APIs and constructs to your interface.

These challenges are not readily obvious to developers new to the component-based model. We are used to designing as we code on the fly, and few habits are more detrimental when it comes to interface design. Aiming to define interfaces that last and having disciplines in practice is the first step to designing services properly.

6.1.3. Approaching an Interface Design

What would be a good approach to designing interfaces? We have explained that it can be disastrous to design interfaces haphazardly or carelessly. However, it is also obvious that it takes an eternity to design the perfect interface. Things always evolve, and humans are not all knowing. The dilemma is not unlike the one faced by the trio in the following story:

Once three men were asked to walk through a field and pick the fullest wheat by the time they reach the far end. They could only make one choice and were not allowed to retrace their steps. The first man took the first large wheat he laid his eyes on, and regretted his pick for the rest of his trip because he saw many fuller ones. The second man resisted temptation and put off a decision, expecting a better catch down the road, only to settle for a small one near the end of the field. The third man made mental notes as he walked the field, and at about the middle, he selected the fullest one up to that point. He ended up with the fullest wheat among all three men, although his probably wasn't the biggest in the entire field.

The last approach appears to be a good rule of thumb for interface design as well. Although it is impossible to produce the perfect interface, it is possible to produce a good interface and to evolve the interface in large stages. This is preferred over designing the interface piecemeal and releasing it incrementally. Here are a few guidelines on designing service interfaces:

First, address the fundamental question, What exactly does the service do? It must be considered independently of how it is done.

For instance, determining the height of a building is what needs to be achieved. Measuring the atmospheric pressure with a barometer, timing its free fall from the top of the building, and trading it for the data from the building administrator are three ways of how to do it.

Let's look at a more tangible example. Suppose we want to create a file system service and the following interface is one cut of the design:

public interface FilesystemService {

   /** Gets the disk offset of the file. */
   public long getDiskOffset(String fileName);

   /**
    * Creates a file in the file system;
    * returns the disk offset for the new file.
    */
   public long createFile(String fileName);

   /**
    * Writes data read from the input stream to a file
    * at the specified disk offset.
    */
   public void writeFile(long offset, InputStream in);

   /** Reads a file at the specified disk offset. */
   public InputStream readFile(long offset);

   /**
    * Inspects the physical media at the specified offset range.
    * Fix problems automatically.
    */
   void verifyMedia(long startOffset, long endOffset);
}

The problem with this design is that it exposes the implementation aspect of the service through the service interface. The disk offset should not be a concern to the clients of this service. Having it as a parameter in the API and a conceptual entity makes the job of this service's caller more difficult. Removing the getDiskOffset and createFile methods and referring to files with their names in the writeFile and readFile methods would be a better solution. Exactly how the filename as a string is mapped to the physical address on the disk is up to the implementation of this service.

The inclusion of the verifyMedia API in this design is another problem. The applications that call this service to read or write files as part of their normal operation are quite different from utilities that perform maintenance duties. The former may be a tax calculator; the latter may be a disk “doctor.” Therefore, it is worth separating the verifyMedia API into a dedicated FilesystemDiagnostic service, where more focused and powerful features such as defragmentation and compression APIs can be added.

The service interface API should cover a category of use and should be made “as simple as possible, but not simpler,” as Albert Einstein put it.

Second, balance the generality of the service by considering the scope of use by potential clients.

Recall the print service example. It has the following service interface:

public interface PrintService {
   /** Gets status of the print queue. */
   public String getStatus(String queue) throws IOException;

   /** Prints the contents from the source to the print queue. */
   public void print(String queue, URL source) throws IOException;
}

The print API reads the source contents from a URL. This is an example of incorporating generality in consideration of potential uses of the client. A first attempt was to use java.io.File as the parameter. However, this design restricts the print service to print local files. What if the gateway does not have a local file system? What if the application would like to print directly from a link on the Web? Clearly the URL argument can cover more ground and is more flexible.

Third, consider all known forms of incarnations of a service and generalize the interface to make it possible to implement them.

Imagine a synchronization service that is responsible for communicating with a back-end server. The following is a flawed interface:

public interface SyncService {
   /** Communicates with the back-end server. */
   public void download(Socket s);
}

By mandating that a socket be used as an end point of a communication channel, we are excluding other potential implementations such as datagrams via UDP in cases that require efficiency and tolerate data loss (for example, streaming media), or transactions using HTTP in situations in which the data must tunnel through firewalls. Using the socket also reveals the intended implementation of the service. One possible solution is to use URL to identify the remote server.

Fourth, factor out common functionality into an independent service. The necessity of this usually reveals itself when you set out to implement a service. If a functional module turns out to be needed by multiple services, it is probably logical to create a separate service. For example, if we were to write a suite of network server services such as FTP, IMAP, and HTTP, it may be valuable to create auxiliary services such as a connection manager service that manges a pool of threads for handling each incoming TCP/IP connection, instead of duplicating the same logic in each service.

Fifth, exclude the configuration aspect of a service. Generally, configuration is more closely tied to the implementation of a service. Consider the following proposed LogService interface:

public interface LogService {
   /** Logs the message. */
   public void log(String msg);
   /** Sets the remote URL where log entries are sent and stored. */
   public void setRemoteURL(URL u);
   /** Gets the remote URL where the log entries are stored. */
   public URL getRemoteURL();
   /** Sets the name for the local log file. */
   public void setLogFilename(String name);
   /** Sets the maximum size in bytes of the local log file. */
   public void setMaxLogSize(long size);
   ...
}

LogService intends to allow for two different implementations: One is to save the log entries locally to a file; another is to deliver the log entries remotely to a URL. Depending on which implementation is used, different configuration parameters are called for. In a local log file implementation, it makes sense to configure filename and size, which does not make sense in the remote log implementation. This is why the configuration aspects of a service generally should not appear in the service interface.

Sometimes it is okay to put configuration methods into the interface because they do not vary based on implementations. However, including a large number of get/set methods in the service interface obscures the essential functionality of the service and clutters the interface. For example, consider the following version of HttpService:

public interface HttpService {
   void registerServlet(String alias, Servlet servlet, ...);
   void setPort(int port);
   int getPort();
   void setMimeTypes(String[] mimeTypes);
   ...
}

In any event, you may very well have a dedicated configuration service responsible for configuring parameters of various services.

Sixth, do not reinvent APIs. In Chapter 4 we discussed a mailer example that does not register any service of its own and sends e-mail messages directly using the JavaMail API. However, it is always tempting to create your own service interface. For example,

public interface MailService {

   /** Sends e-mail. */
   public void sendMail(String recipient, String sender,
      String subject, String messageBody);

   /** Opens a folder on a mail host to retrieve messages. */
   public Folder open(String mailHost, int port);
   // ...
}

Before you proceed, it is important to assess what value is added by this service interface over the published JavaMail API. Most such undertakings go forward under the pretext of simplifying the API on which they are built, but we must consider that yet another API usually increases the amount of learning a developer needs to do, duplicates the design effort that has already been made, and may very well miss important requirements or features. For example, the proposed interface would not be able to handle multipart multipurpose Internet mail extension (MIME) messages.

Programmers have a tendency to distrust code from their peers and wouldn't hesitate to put in their own creations. This may raise a hurdle to effective development work within our programming framework.

Seventh, document the interface thoroughly. An interface is often called a “contract,” because it stipulates the semantics of the functionality it provides, and usually it is the only thing exposed to clients. For this reason it is important to accompany the interface definition with ample comments. The following parts of the interface should always be documented using the documentation comments recognizable by the javadoc tool:

  • The interface itself. The functionality of the service should be stated as well as its limitations, assumptions, and other general information the client may need to know.

  • Each method. Essential aspects that need documentation are what the method does, what parameters it takes, what return value it produces, any exceptions it throws under what kind of situations, and so forth. It may also be important to document whether the method is thread safe.

Eighth, and last, apply binary compatible changes to interfaces to minimize impact. The Java programming language allows certain changes to be made to an interface while preserving binary compatibility. For example, consider again our PrintService interface. After compilation, we end up with the PrintService.class file, which is loaded by a client bundle that needs to use it. Now suppose an enhancement is needed, and a new API

/** Cancels the specified print job. */
public void cancel(int jobNumber) throws IOException;

is added to the interface. We can recompile the print service (including the implementation classes), and obtain a new version of the binary class file PrintService.class. If we replace the previous version with this class, the client bundle does not have trouble accessing it.

Now, as an afterthought, we decide that it would be convenient to have the original print method return the print job number as follows:

/**
 * Prints the contents from the source to the print queue.
 * Return the print job number.
 */
public int print(String queue, URL source) throws IOException;

Thus we can easily cancel an ongoing job with our new API. Unfortunately, a client bundle unaware of this change breaks and raises java.lang.NoSuchMethodError.

We have seen that adding a new method to an existing interface does not break binary compatibility, but changing the return type of an existing method does. The complete specification of binary compatibility can be found in Chapter 13 of The Java Language Specification [17].

Version specifications in the Export-Package and Import-Package manifest headers do not help us with incompatible interface evolution. A higher version is expected to be backward compatible with a lower one for the same package. This is because the version number specified in the Import-Package header is the minimum, not the exact one required of the package.

Although certain changes to the interface will not break existing clients, these clients will not be able to take advantage of the new features. Therefore, service interface designers should not lessen their rigor by using these as a rectification mechanism if they want their design to be enduring and effective.

In summary, a good service interface should capture the essential functionality of what it ought to do. It should be general enough to allow room for multiple implementations and various demands from its clients, it should be minimal, and its function should be unambiguously documented. If changes to an interface are unavoidable, they should be applied in a binary-compatible fashion.

6.1.4. The Social Aspect

The process of producing a good interface is usually filled with debate, what-if brainstorming, and trade-offs among conflicting feature demands. Many of us find this distasteful because we are accustomed to making coding decisions on our own and seeing things happen right away. The planning is frustrating because we don't consider any progress has been made until the coding phase has begun.

But agreeing on an interface is most likely a collective effort, because Alice and Bob cannot proceed with their service implementations unless Charlie's service interface, on which both Alice and Bob rely, has become stable. The component-based model dictates that the team members must cooperate more explicitly on the interface design. In fact, this process is a mini standardization effort, not at the level of companies, industries, or international bodies, but within the scope of your project team, and possibly your customers. It is not surprising that designing interfaces has the similar pain and benefits associated with standardization.

Therefore, it is only normal that considerable time may be spent on arguing the merits of an interface. The initial deliberation is well worth the effort if the interface turns out to be well conceived and can stand the test of changes and diverse use cases. It saves time in the long run. As a matter of fact, we think that an interface without much scrutiny is highly suspicious of defects and needs to be revisited.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.77.158