Isolating Java APIs
Haim Zamir
Java packages sorely need an isolated, executable API to show the forest despite the trees, to facilitate API design, implementation, consumption and maintenance.
The closest analogue to what we are proposing, for all its faults, is the C++ header file.
This article sets out to accomplish the following:
1. Analyze the good and the bad of our model: C++ header files (intended for API use)
2. Provide an overview of applicable Java facilities, to see where they can help
3. Propose a Java convention which implements our concept of pure API headers at their finest: acting as an aid in design, implementation, consumption and maintenance of Java packages.
C++ programmers rely on header files to provide the most basic documentation for library APIs. It is true that in practice, without the application of rigorous discipline, C++ header files may fall short as pure API documents. This is due primarily to two factors:
1. Limitations of C++ as an API specification tool
2. Limited application of applicable techiniques by C++ programmers
Careful design[1] can produce a short, elegant header file expressing the API of a library. Elegance is not an option whose purpose is to enhance the experience of the class user, it is a necessity for clear thinking about class design. Sophisticated design tools notwithstanding, the C++ header file is still a primary and necessary canvas for the art of class design.
Strictly speaking, Java does not have the concept of a header file, it does, however, support two very helpful mechanisms:
1. Javadoc: This tool mitigates some of the need for headers for documentation
2. Interfaces: Used judiciously, they are a valuable API design tool.
While javadoc can address some of the need for a header-file like construct, it falls short in important areas, namely:
· Javadoc output is HTML; it is not seamlessly integrated with the code itself, and requires a browser.
· A pure executable API document facilitates design. Running javadoc to browse the public API portions of the code you are developing is a very awkward way to refine one’s design.
Java Interfaces are promoted as either the answer to multiple inheritance, or as a compensating feature. However, even more noteworthy is their potential to serve as a replacement for what we are missing—header files!
While generous use of interfaces can go a long way to provide isolated Java APIs, additional conventions are necessary to address the following needs:
1. Physical isolation of API files. This makes it easy to distribute the APIs to facilitate their remote implementation, or inspection.
2. Support for class[2] variables and methods. Java does not allow interfaces to contain static methods.
The Java header solution is practical and available solution. It requires Java 1.1 or greater to implement.
1. All public APIs go into a separate package,—in fact, into a separate package tree. Given a package called com.domain.widgets, its APIs would be defined in a parallel package called com.domain.api.widgets.
2. Each implementation file imports the appropriate API package.
3. Absolutely no public methods are provided which are not defined in the API. Public classes only implement interfaces, and provide no new methods (with very limited exceptions noted below).
4. Use of static variables and methods are severely restricted to routines such as main(), and a declaration of a static variable to hold each class’s metaclass (defined below).
5. Each class requiring statics will have a single public static final[4] pointer to an instance of its metaclass called Class.
6. Each interface in the API follows strict naming conventions. The class Rectangle implements API_Rectangle. Its class methods are declared in a separate interface API_Class_Rectangle.
7. Static variables and methods are deprecated in favor of metaclasses. A metaclass is a class describing a class. In other words, what are effectively class variables and methods (which in C++ and Java are called static methods) are declared as instance variables and methods of a metaclass. There is one instance of a metaclass for each class, where such constructs are needed.
1. All public APIs go into a separate package,—in fact, into a separate package tree. Putting all APIs into a separate package tree facilitates delivery of the API for implementation or inspection to another party. The implementation is valuable, but often the API is all anyone wants to see, including the developer himself, when he wants to focus on the design.
2. Each implementation file imports the appropriate API package. A simple import statement removes any coding overhead associated with a separate API tree.
3. Absolutely no public methods are provided which are not defined in the API. If it is public, it must be in the API. If it is in the API, it must be declared in the API files, and not in the implementation. (This convention cannot be enforced without additional tools.)
4. Use of static variables and methods are severely restricted but not eliminated. Allowing this limited usage of statics is a pragmatic consideration, as will be shown in the example. It facilitates the global access of public class variables and methods without requiring an instance. It also facilitates the inclusion of a main() static variable which is commonly needed.
5. Each class requiring statics will have a single public static final[5] pointer to an instance of its metaclass called Class. As of Java 2, this is still the only way class methods and variables can be encoded within interfaces. All communications with the class (as opposed to its instances) is now done by accessing this static named Class (e.g. setting the roundess of any new instance of RoundRect is accomplished with an expression such as: RoundRect.Class.setRoundness(0.5)). There is no performance penalty for this if the metaclasses are themselves declared final.
6. Each interface in the API follows strict naming conventions. One must know the correlation between classes and the interfaces. When browsing the APIs, one can then know that by stripping off the prefix API_ he can address the actual classes.
7. Static variables and methods are deprecated in favor of metaclasses. Given the limitations of defining interfaces for statics (see #5, above) we look for another proven object oriented model. Metaclasses are a Smalltalk feature, and in our opinion, a better object orientated design, preventing random intermixing of static and instance variables and methods.
In the following conventional example, class RoundRect contains both static and non static members. There can be no Java interface which tells us how to use RoundRect, since it contains static methods. These which represent over half of the class protocol, and yet can not be represented directly in a Java interface. The result is the interface and the implementation are not separate.
package com.geboing.shapes;
public class RoundRect extends Shape
{
public final static Class_RoundRect Class =
new Class_RoundRect();
static protected float sRoundness = 0.5f;
static public void setRoundness(float roundness) {
sRoundness = roundness;
}
static public float getRoundness() {
return sRoundness;
}
public void draw()
{
// ...
}
public static void main(String[] args)
{
RoundRect r = new RoundRect();
RoundRect.Class.setRoundness(0.4f);
}
} // end class RoundRect
This is a Java “header” file, API_Shapes.java. It describes all the public features of a simple Java library. Since Java headers contain only interfaces, and no public classes, they can be factored and named in any desired fashion, (e.g. one header per package).
There is zero implementation in the file.
What would be otherwise be an embedded static set of methods, in class RoundRect, now becomes a separate interface to the RoundRect class itself (as opposed to its instances) called Class_RoundRect.
Instead of calling RoundRect.setRoundness() (as in the conventional example) we can call RoundRect.Class.setRoundness() with no difference in overhead.
package com.geboing.api.shapes;
//=====================================================================
public interface API_Class_Shape {
int defaultXOrigin = 0;
int defaulYOrigin = 0;
int defaultWidth = 1;
int defaultHeight = 1;
}
//=====================================================================
public interface API_Shape {
void draw();
void setSize(int width, int height);
void setOrigin(int x, int y);
}
//=====================================================================
public interface API_RoundRect {
void draw();
void setSize(int width, int height);
void setOrigin(int x, int y);
}
//=====================================================================
public interface API_Class_RoundRect {
void setRoundness(float roundness);
float getRoundness();
}
//=====================================================================
In this example, RoundRect class variables are implemented as a public final static inner class. While the outer class implements the API_RoundRect interface, the inner class implements the API_Class_RoundRect interface.
Note that one final static variable is defined to hold all references to the RoundRect class. There is a miniscule overhead to allocate this a Class_RoundRect when RoundRect is first accessed. Accessing members of Class_RoundRect do not have any additional overhead above that of accessing any static member.
package com.geboing.shapes;
import com.geboing.api.shapes.*;
public class RoundRect extends Shape implements API_RoundRect
{
public final static Class_RoundRect Class =new Class_RoundRect();
//---------------------------------------------------------------
public final static class Class_RoundRect
implements API_Class_RoundRect
{
protected float sRoundness = 0.5f;
public void setRoundness(float roundness)
{
sRoundness = roundness;
}
public float getRoundness()
{
return sRoundness;
}
}
//---------------------------------------------------------------
public void draw()
{
// . . . implementation here
}
//---------------------------------------------------------------
public static void main(String[] args)
{
RoundRect r = new RoundRect();
RoundRect.Class.setRoundness(0.4f);
}
} // end class RoundRect
The Java header technique described above offers a clean separation between interface and implementation, without introducing significant overhead.
In the absence of language support for necessary interface features, some reliance on convention is required to reap the benefits of this technique.
[1] Examples are separating all public APIs from implementation through: 1. judicious use of abstract classes, and 2. for cross platform work, segregating the design into distinct API and implementation classes, which allow only implementations to know the details, creating portable, implementation-free APIs.
[2] We favor the Smalltalk terms class variable and class method to static variable and static method. Smalltalk treats classes as true objects in their own right, for a much cleaner design. We propose the same solution partially for the same reason, and particularly to solve the limitation of Java interfaces.
[3] Suggestions for a better name are welcomed.
[4] That is to say that the pointer to the class (aka metaclass instance) is immutable, and not the class itself.
[5] That is to say that the pointer to the class (aka metaclass instance) is immutable, and not the class itself.