Robb is a software engineer at LION bioscience AG. He can be contacted at [email protected].
Reuse and decoupling are concepts fundamental to the object-oriented paradigm. In practice, however, achieving their promise is easier said than done. In this article, I'll present step-by-step instructions for making applications both reusable and independent using a technique called "design by interface."
In a nutshell, design by interface means you clearly specify the services an object offers, separate from its implementation. Anyone who uses the object can only call on the services in this specification. The advantages of design by interface include:
- Libraries and subsystems can be reused without fear of dependencies.
- Faster, simplified, parallel development. Quick-and-dirty implementations can be used (and improved later on) if necessary. Also, independent teams can work on different sides of a horizontal interface without continually having meetings.
- A guideline for development is supported that lets developers of all skill levels contribute.
- The interface can be brought in gradually to a software project.
- Clearer expression and documentation of designs are possible.
Here's the scenario I was faced with: Sending e-mail to users is an easy way for programs to give feedback and send warnings. In the UNIX world, this is common and is usually done by starting a sendmail process, then passing it the necessary header and content information. As I began writing intranet programs at LION bioscience AG (where I work), however, I wanted the same functionality in Java that I had with our UNIX scripts.
Initially, I turned to the ORO NetComponents class library (http://www.oroinc.com/), which was a nearly perfect solution. NetComponents is a Java package that enables access to the most commonly used Internet protocols -- FTP, NNTP, SMTP, POP3, Telnet, TFTP, finger, whois, and the like, as well as rexec, rcmd/rshell, and rlogin. Because all client classes are derived from the NetComponents SocketClient and DatagramSocketClient classes, NetComponents presents a consistent API. Still, there were several needs that the ORO package didn't address. Consequently, I ended up writing additional classes, which are presented here.
The Problem
As a designer/developer, I feel responsible for the effects any libraries that I install into my company's development environment could have. It's easy to imagine that many applications could make use of e-mail services. Dependencies will arise between applications written by other people and the classes that I install. These kinds of dependencies are hard to detect and keep track of. Figure 1 shows the typical use of the ORO SMTPClient and an application. With many different applications and programmers using these packages, dealing with change becomes important. Change is the rule and not the exception in software and network development. For example, Javasoft is currently working on a JavaMail API. It will probably become a standard and an integrated part of Java. Some third-party library developers (including ORO) have already promised to conform to it or base tools on it. How will large changes in the mail system affect the various dependent applications?
Another problem with these dependencies is that class libraries almost always have bugs and rough edges that the producers want to smooth out. Library upgrades are common, and sometimes the API changes along with them. And then of course, there are the cases where a bug won't be fixed soon enough and a module has to be reimplemented in-house. These changes can be difficult to make if other systems are tied to a particular class library.
Table 1 is a partial list of the ORO e-mail interfaces that have to be learned. For the classes to work correctly, you must call certain methods and check certain results in a specific order. These ordering constraints are difficult to remember. Also, they expose implementation details -- users are forced, for instance, to know how SMTP works. Maybe future e-mail systems (or an e-mail system at a different site) won't use SMTP to send e-mail.
That the interface is low-level means that you will have to repeatedly make the same sequences of low-level calls. Sure, some applications might need access to some low-level features, but I've found that nine times out of ten, e-mail (from applications) is used in one basic way -- a letter has a sender, subject, some recipients, and a short message.
Another way to look at this issue is that application programmers should be spending their time developing applications, not making infrastructure components useable. In most cases, sending e-mail is just one small part of a program's function, and shouldn't require the developer to master the details of a particular package, or SMTP.
Most organizations have tight schedules and several projects simultaneously underway. This was true for this case as well. We could not wait for weeks while I looked for the ultimate e-mail solution. I needed to find a reasonable solution fast to let other projects keep on going.
The Solution
The first step is to sit back and imagine how you'd like to send e-mail from Java if you could. What kind of object or objects would you have? What methods would they have? How would they be named? There are good guidelines available for making these kinds of decisions. In Object Oriented Software Construction, Second Edition (Prentice Hall, 1997), Bertrand Meyer suggest a common, clear way to work with objects:
- Create the required objects.
- Set up any of their properties that differ from defaults.
- Apply the operations you need.
For the sake of example, I'll assume that I need just one object -- an e-mail message. First, I instantiate it. Then I send some simple messages to it like "set sender," and "set subject." The message for specifying the recipients would be like "add recipient," which makes it clearer that multiple recipients are allowed. Finally, I tell the e-mail message to send itself.
The next step is to convert this natural-language description into terms computers can understand, by writing the equivalent Java interface. The EmailMessage.java interface (see Figure 2 and Listing One), for instance, is both a description of how to use the e-mail subsystem, as well as a contract that specifies what services it will provide.
There are a couple of things to note here. First, the interface is public -- it can be seen and used by anyone. This is enforced by Java, and will be handy later. Second, exceptions are specified in the interface. This is important because a full interface specifies not only the inputs and outputs of a class, but also the error conditions. Finally, it is fully documented -- the interface will become the user's view into the e-mail subsystem, and is why documentation is so important here.
The third step is write a package-visible adapter for your current e-mail implementation. OROEmailAdapter.java (Listing Two), for instance, is an "adapter" -- something that works between our nice e-mail interface and the actual e-mail subsystem. Simply put, an adapter converts one interface to another. Here's where I've done some programming work -- the set and add methods collect information about the e-mail message. The send() method calls all of the ORO methods in the correct order, feeding them the data that's been previously collected. This is the first type of reuse that you'll see: simple code reuse. Every programmer normally would have had to do this kind of coding, somewhere in their applications. The code I present here will eventually be accessible to everyone. The class definition class OROEmailAdapter implements EmailMessage is marked as implementing EmailMessage. This has two advantages: One, the compiler checks to make sure that the adapter really implements the interface -- that it fulfills the specified contract. The second advantage will be clear as you read on.
Also, notice how the class has the default package visibility. It's not public like the interface. This means that application developers, or the programs they write, won't have access to it. They won't be able to instantiate it. The Javadoc program by default won't document it. Figure 3 shows the class structure to this point.
The final step is to tie all this together by creating a public factory that hides the implementation class.
The interface and the adapter are finished. All that's needed is a way to somehow give the outside world access to the adapter, yet not expose unnecessary details. A "factory" -- an object that instantiates other objects -- is the solution. Mail.java (Listing Three) is a class that's public. Its one method just returns a reference to an OROEmailAdapter. It can do that, because the factory is in the same package as the adapter. Note, though, that the reference is returned as type EmailMessage. Here is the second benefit of tagging the adapter as implementing the interface: The ORO adapter "is a" EmailMessage and can be returned as one from the factory. The actual adapter implementation class will be hidden from users, who will only have an EmailMessage to work with. Figure 4 shows the factory structure.
Factories are used extensively in frameworks that are intended to be adaptable and flexible. By hiding the instantiation of an object, clients are not dependent on implementation names. The San Francisco project, for example, uses this idea extensively. (The SanFrancisco project is a reusable framework for building distributed business applications in Java. It lets you write applications that are independent of data storage and other subsystems; see http://www.ibm.com/java/.)
The client of these classes (that is, the customer) is the application programmer. Keeping the customer in mind will affect everything from method naming to documentation. Everything done so far contributes to making the programmer's job as easy as possible. For sending e-mail, a programmer has the EmailMessage interface and the Mail class to rely on. This is all you need to do your work. Figure 5 shows this visually. MailTest.java (Listing Four) shows an example of how these classes are used. This is where the small extra effort starts to pay off. Note how nothing about "ORO" appears in the code. Also, note how the code is clear and short, without any unnecessary details. Even if you are the only developer, you'll appreciate how your application-layer code becomes simpler with this design style.
Finally, notice that although as a designer, you must consider adapters, factories, and patterns, an application programmer doesn't have to understand these underlying mechanisms. They just use the factory class in what becomes a natural way.
Conclusion
What the design by interface process does is create a horizontal interface between two or more independent subsystems. This means that the new interface can apply to a whole category of (e-mail) subsystems, as opposed to only one implementation. It's now much easier to make changes on either side of the interface, as well as move to different implementations.
Real reuse and decoupling have been achieved -- e-mail facilities can be used from many different applications without fear of becoming dependent on a single implementation or manufacturer. The actual e-mail system being used could be swapped out at any time.
I'm still investigating the payoffs of design by interface, and exploring the different ways it can be used. In conclusion, here are some ideas for taking this approach even further.
- Use Java's reflection and dynamic binding to allow adapters to be specified at run time. Its class name can be read from a properties file or command-line option. The factory class can make sure it implements the proper interface before instantiating it. This can make your system configuration even more flexible if necessary. For example, someone who purchased your system could configure it to use a new adapter without needing access to your source code.
- Apply design by interface whenever there are multiple subsystems that must use each other's services. Some examples could be a GUI, a datastore, or an authentication system. Objectspace (http://www.objectspace.com/) does this with its Voyager Db interface.
- Use this design style for all of your Java packages. In his Java development guidelines (http://www.chimu.com/publications/javaStandards/index.html), Mark Fussell suggests that you make the interfaces the only public items, and write at least one factory class per package that gives access to the implementations.
- Apply interface-based design as a software-development process. This can lead to a plan where every developer is productive, regardless of experience level. Those with more experience become system architects, defining interfaces and specifying services and behavior. Developers with less experience are assigned the task of creating implementations of predefined interfaces. With interface-based design, the emphasis shifts from the actual implementation towards the interface. This means that it's no longer important if the (novice) programmer's code is too slow, or uses too much memory. As long as it conforms to the interface, it can be used. In the future the implementations can be optimized or refined if they have to be.
DDJ
Listing One
// file: EmailMessage.java package net; import java.io.IOException; /** A a simple email message class. It allows email messages to be sent * easily from Java. Here's how it is used: * <pre> * EmailMessage message = Mail.newMessage(); * message.setSender("[email protected]"); * message.addRecipient("[email protected]"); * message.setSubject("Have you been naughty or nice?"); * message.setContent("Just checking..."); * message.send(); * </pre> * The order of the various set()'s and add()'s is not important. Just * make send() the last operation. Multiple recipients can be specified by * calling addRecipient() or addCC() multiple times. * @see Mail **/ public interface EmailMessage { /** Specify the <code>From:</code> header of the message. **/ public void setSender(String address); </p> /** Specify a <code>To:</code> header of the message. This can be * invoked more than once for messages with multiple recipients. **/ public void addRecipient(String address); </p> /** Specify a <code>Cc:</code> header of the message. This can be * invoked more than once for messages with multiple recipients. **/ public void addCC(String address); </p> /** Specify the <code>Subject:</code> header of the message. **/ public void setSubject(String subject); </p> /** Specify the actual text of the message. **/ public void setContent(String content); </p> /** Connect to the mail server and deliver the message. * @exception IOException can be thrown for many, many reasons. **/ public void send() throws IOException; }
Listing Two
// file: OROEmailAdapter.java package net; </p> import java.io.*; import java.util.*; import com.oroinc.net.smtp.*; </p> /** An EmailMessage implementation that uses the ORO tcp/ip toolkit. **/ class OROEmailAdapter implements EmailMessage { private static final boolean DEBUG = false; private Vector recipients = new Vector(); private Vector ccs = new Vector(); private String subject = ""; private String content = ""; private String server = ""; private String sender = null; </p> public OROEmailAdapter(String server) { this.server = server; } public void setSender(String address) { sender = address; } public void addRecipient(String address) { recipients.addElement(address); } public void addCC(String address) { ccs.addElement(address); } public void setSubject(String subject) { this.subject = subject; } public void setContent(String content) { this.content = content; } public void send() throws IOException { SMTPClient client = new SMTPClient(); client.connect(server); debug(client.getReplyString()); if (! SMTPReply.isPositiveCompletion(client.getReplyCode())) { throw new IOException("SMTP server refused connection"); } client.login(); debug(client.getReplyString()); SimpleSMTPHeader header = makeHeader(client); Writer writer = client.sendMessageData(); debug(client.getReplyString()); if (writer == null) { throw new IOException("Could not send message data"); } writer.write(header.toString()); writer.close(); if (! client.completePendingCommand()) { // failure throw new IOException("Could not complete pending command"); } client.logout(); debug(client.getReplyString()); client.disconnect(); } /** Create the header for the message. **/ private SimpleSMTPHeader makeHeader(SMTPClient client) throws IOException { // Prepare the 'From' header. String from = sender.toString(); if (from == null) { from = ""; }; client.setSender(from); </p> // Prepare the 'To' header. int toCount = 0; String to = ""; Enumeration addrs = recipients.elements(); while (addrs.hasMoreElements()) { toCount++; if (toCount > 1) { to += ", "; } String addr = (String)addrs.nextElement(); debug("adding recipient: "+addr); client.addRecipient(addr); debug(client.getReplyString()); to += addr; } // Now we can instantiate the header. SimpleSMTPHeader header = new SimpleSMTPHeader(from, to, subject); // Add in cc's, if any. Enumeration carbonCopies = ccs.elements(); while (carbonCopies.hasMoreElements()) { String addr = (String)carbonCopies.nextElement(); client.addRecipient(addr); debug(client.getReplyString()); header.addCC(addr); } return header; } /** Simple debuging output **/ private void debug(String s) { if (DEBUG) System.out.println("debug in OROEmailMessage: "+s); } }
Listing Three
// file: Mail.java package net; /** E-mail system. Use this class to construct new e-mail messages. * <pre> * EmailMessage mesg = Mail.newMessage(); * </pre> * See the documentation for EmailMessage for details on how to manipulate * and send it.<p> * @see EmailMessage **/ public class Mail { /** Create a new email message that uses the default LION SMTP server. **/ public static EmailMessage newMessage() { return new OROEmailAdapter("mail.lion-ag.de"); } }
Listing Four
// file: MailTest.java package net; /** A short program that sends test e-mail messages. **/ public class MailTest { public static void main(String[] argv) { /* Check command line arguments */ if (argv.length != 2) { System.out.println("Usage: MailTest <from> <to>"); System.exit(0); } /* Create and send a test message */ try { EmailMessage mesg = Mail.newMessage(); mesg.setSender( argv[0] ); mesg.addRecipient( argv[1] ); mesg.setSubject("Hello from Java!"); mesg.setContent("Hi! \n\n This is a test message."); mesg.send(); System.out.println("Message was successfully sent."); } catch (Exception e) { System.out.println("An exception occurred: " + e); System.exit(1); } } }
Copyright © 1999, Dr. Dobb's Journal