Principles of Writing OSID Implementations
Tom Coppeto
OnTapSolutions
1 December 2004

The purpose of this document is to capture the rules of thumb we used in implementing a suite of OSIDs for MIT.

Let me first begin by addressing why you are here.

  1. your manager told you to use OKI
  2. you wanted to see if you could save some time
  3. you like to collect powered by logos

You won't save any time and never take your management seriously. Now that we have that out of the way, the OKI OSIDs are structured around a factory design pattern. If you have a need for factory patterns in your software, then by all means. If you don't, then don't force it (it's easier to just put the OKI compliant gif on your web page, I don't think anyone will mind).

Of course, if you have never used factories or have no idea why they are useful, then you may be missing out on some coolness. And, computer people can always use more of that.

Although this isn't a software engineering tutorial I do want to address the reaction some people get when they look at these interfaces for the first time (What am I supposed to do with that?).

It's all about interchangeability. Sure, many programmers have figured out how to stuff things in classes and libraries and claim code reusability. The OSIDs are about interchangeability. Like writing a device driver for a USB device that can be used across manufacturers' products, whenever the user plugs or unplugs the device, the application talks to the same interface. Underneath that interface there may be very different code and protocols (the implementation). A good interface specification can't permit anything that may prevent interchangeability. As such, the OSID Specification looks a little vague.

The OSIDs (interfaces) define the meeting point between Application and Implementation. They don't say when happens on either side of this boundary. You can do whatever you like. Whenever you get the feeling that there is some method you need that isn't specified in the Interface, remind yourself that you can control what happens both sides of the Interface.

What about reusability? Yes, implementations can be reused, but many people think this means having only one set of code per function across a product suite or an entire enterprise. In fact, I write more code per function than if I did it the old fashioned way. The value comes from interchangeability, not porting application code or wasting your time in long meetings bickering over feature sets. Implement them all. More on this later.

I also like to avoid the use of the word interoperability. You can achieve interoperability through a common interface. This concept often confuses network people (another one of my hats) who think in terms of protocols and RFCs. Interfaces ride above implementation specifics. It might be helpful to consider the interface specification to be layer 8 in the OSI Reference Model.

It would be helpful at this point if you were familiar enough with the OSIDs that you have actually tried to use them. Maybe you are looking to compare notes with others. Maybe you are at the point where you feel it doesn't work in your situation. That's ok. You might still find my notes helpful.

Interfaces and Implementations

Every religion needs some commandments. These were given to me on Mt. Rendezvous.

Push It Down

There's a tendency for programmers to want to pull every item up into the application. You've seen this code (and I've written a lot of it). It starts out by parsing command line arguments, reading config files, doing some user interfacing, calling functions that call functions, etc. and the programmer comes up with some clever (or maybe not) means of sending down the input parameters and pulling up the results and error messages. And yes, these programs often begin with main, but as they say you can program in Fortran in any language.

Now throw OSIDs in the way. How do I know what the error was? Where do I pass in my optional parameters? What do you mean this library is going to perform UI functions?

The interface defines the meeting point. If you can't do what you want through the interface, then listen to Lassie. She's telling you to push it down underneath the interface. Interchangeability.

Is there any value in propagating detailed error messages all the way up the stack until you get to the beginning of the program? If an application can parse a configuration file is there any reason the implementation can't parse one for itself? Does the specification prevent an implementation from opening a dialog box? It's all in how you look at it.

Push all that stuff down below the interface. It will make your life easier and leave your application to focus on the reason for its creation in the first place.

Implementation Configuration

When an implementation is required to have a single behavior within an application, it can rely on a modifiable fixed configuration to supply parameters and tweak its behavior. This is often performed through a properties file or some configuration database.

How does this differ from the Simplification directive above? The difference is between an implementation that modifies its behavior based on fixed parameters via an out-of-band configuration, and one that does so based on dynamic run-time parameters not already defined in the OSID. The latter supplants the interface definition introducing incompatibilities with other implementations of the same interface.

[help]

The rule of thumb is that when an application requires multiple instances of an implementation to be invoked with different configurations, this requires separate OSID implementations (which could be derived from the similar code) to exist. When an application requires one or more instances of an implementation to be invoked with the same configuration, the same implementation can be used. Not intuitive, however the interchangeability derived from strictly obeying the interface provides much greater value than a few extra jar files lying around.

An example is Logging. The Logging interface includes a formatType which presumably someone had the idea that the application should be able to influence the logging format (those of us who use syslog have no such fantasies). And, a log file with variable formats isn't very useful. What happens if you define format Types in your implementation? The application tries to use these custom types in the next implementation where, of course, they won't be understood.

Then how do I control the format of the log at all? The answer is through a configuration supplied directly to the Logging implementation. I ignore the formatType in the Implementations I've written.

Exceptions, Error Handling and Debugging

The OSIDs tightly control what information is passed from a service implementation into an application. Rarely do the defined Exceptions provide enough context for an application to do anything beyond, ignore, retry, or abort. Although generally this suffices, most applications tend to define their own error handling with respect to messaging to the user.

Exception chaining, however useful in providing a complete picture of the exception event, is not an option as inter-dependent OSIDs are required to pass a single pre-defined text-based error message to the caller. The OKI philosophy is such that the inner workings of an implementation are cloaked. The reality is that errors resulting from configuration or operational problems should be available in a manner conducive to efficient troubleshooting.

In a sense, the challenge is to walk both sides of the fence without getting hurt. Implementation biases are as follows:

Types, Types and More Types

OKI uses Types to decide how a particular object should be managed within the implementation. In general, objects that are to be managed in the same manner should use the same Type. For example, an Agent as defined at MIT may reference a student or an employee. At the surface, it may appear obvious to create both an MITStudent and an MITEmployee type. However they both may be accessed through identical means as far as the OSID implementation is concerned. We use MITPerson while the properties for each may differ inside the MITPerson agent itself (one may have a class year and course number). This avoids another common pitfall in data management: what to do if an employee is also a registered student.

Another pitfall is using Types to control behavior. You might authenticate people via one of two means: SSL and Kerberos. You might be tempted to develop a single Authentication Implementation that handles both mechanisms. After all, the Authentication Interface defines a Type that sure looks handy in instructing the implementation on which mechanism to use. The problem is that your application needs to invoke the interface with these Types. Suppose someone else implements a different authentication mechanism and you try to use it. It doesn't understand the Kerberos Type. All that energy spent and no interchangeability to show for it.

The answer is to develop two Implementations for Authentication: one for Kerberos and one for SSL. Ignore the Type in your implementation (more on these evil-doers later). When you get a third-party implementation that ignore Type, your software will just be able to use it.

Forgive, and forget

Be strict in what you send and liberal in what you receive. The same applies to implementations.

You might be tempted to implement a LoggingManager that simply logs to stdout. This could be useful in debugging an application that normally uses a different Logging implementation that sends its logs elsewhere. You swap in the stdout version of Logging because, after all, we can do that now we're all interface hip. Your implementation throws an Exception because it doesn't understand the logName given to it, since, after all, there is no file. So much for interchangeability.

The OSIDs don't prevent things from being written in such a way that result in non-interchangeable code. In a world of Types and method arguments, there will always be some way to snap things together so they don't work in the end. However, much of this can be finessed by following our liberal principle.

When writing implementations, there's no rule that every argument and Type be checked. In fact, do the opposite. Ignore them until they are really necessary.

If the stdout Implementation ignored all logNames, Types, create() and delete() methods by returning quietly instead of throwing exceptions, we'd be able to slide it in with much greater success.

Use the UNIMPLEMENTED exception only if a method is truly not implemented. If it's simply not applicable then let it slide. If your implementation has only one Type defined (you are not using Types), let it go. If you are not using a method argument, don't throw an exception if it is null. Move on.

And there is nothing wrong with implementing a default behavior here and there.

Use the Manager, Luke

The core of each OSID is the OsidManager. The OsidLoader is the factory that delivers the OsidManager corresponding to the chosen implementation. With some OSIDs the story ends there. However with others, like Agent, the AgentManager in turn generates Agents. The details of the constructor is buried within the Manager and are not defined in the Interface. This means you can do whatever you like.

One of the things I like to do in my implementations is to stash the OsidManager into whatever objects it creates. In the Agent example the AgentManager will invoke the constructor in its lookup and create methods:

Agent agent = new Agent ("displayName", this);

Now the Agent knows how to get back to the Manager. This makes it possible to implement commit and rollback functions (which are only defined in the Manager). It also creates a place to centralize common methods and Types (where you use them sparingly, of course).

For example, Logging may define a number of PriorityTypes. These Types need to be known within the LoggingManager, WritableLog and ReadableLog implementations. Instead I only define them in the LoggingManager and create package-only accessible methods to access them from ReadableLog and WritableLog. I might have something like this:


public void appendLogWithTypes(Serializable entryItem,
                               Type formatType,   // evil
                               Type priorityType)
		throws LoggingException {
		
    if (priorityType.equals(manager.CriticalLoggingType)) {
        screamAndShout(entryItem);
    } else {
        manager.SendLogDataToDatabase(entryItem);
    }
}


Again, WritableLog.appendLogWithTypes must obey the interface specification. Once you get to the other side you can do whatever you want, including invoking protected methods in the Manager to centralize your logic.

Stop complaining, and write

It's very tempting to argue every which way with the interface before writing your first implementation. This can be quite time consuming and often lead you in circles.

Get some experience and write working code. The advice I received was just go with it. I'm passing that along.

Then complain

Now that you are an expert with a few dozen implementations under your belt, and frankly, some credibility, then you probably have a lot of useful comments and suggestions to offer. Take some time out to submit them and make it a better Specification.