I recently simplified the PMP algorithms API. This was a somewhat embarrassing but also very rewarding experience. Embarrassing because the old code was just absurd in some cases. Rewarding because it made me question some deeply ingrained habits. In fact, this exercise changed my approach to API design.
Want the short version? Here it is: Stop writing classes, use functions.
Now, that’s of course too simple. Read on for the full story.
Complex Class Interfaces
Up until the 2.x releases, we were using classes as the primary interface for algorithms. The main reason was convenience and consistency. This led to some absurd usage patterns like this:
auto mesh = SurfaceFactory::icosahedron();
simplifier.simplify(0.1 * mesh.n_vertices());
This type of interface looks highly familiar to anyone used to OOP style APIs. In fact, I always had the filter interfaces of VTK in mind when designing the original API. Even though this interface might be familiar, I wouldn’t call it simple.
It turns out, however, that none of our algorithms actually needs a class-based interface. Plain and simple functions are fully sufficient, and the resulting API is much cleaner, leaner, and easier to use:
auto mesh = icosahedron();
decimate(mesh, 0.1 * mesh.n_vertices());
Of course the above example is somewhat artificial, but I think you get the spirit. Straightforward code, less typing, uniform usage.
Another benefit is that a simple function-based interface exposes far fewer implementation details in the headers files. And all that without resorting to artificial constructions like the Pimpl idiom.
Reducing API Surface
The next step was to reduce the functionality exposed in the public API. Some folks call this the API surface, and I think it’s a useful analogy. It is important to realize that every externally observable behavior of your API will eventually be used and relied upon. The more dependencies you have the harder it gets to change and maintain your code over time. Therefore, one of your goals should be to keep your API surface small.
In case of PMP, the implementations of complex algorithms like remeshing or decimation make use of several helper classes such as a
TriangleKdTree or a
Quadric class. In the past, we used to make those helpers public, just in case someone might have a use for it. YAGNI at work. There’s absolutely no need to expose those helpers in the public API. Now they are all neatly tucked away in the implementation files of their respective algorithms.
Focus on Singular Use Cases
Another common pitfall is to provide too many variants of algorithms. When you have an implementation of a well-known algorithm floating around, it’s always tempting to just add it to the library, just in case someone might need it. YAGNI again. However, this can lead to a large collection of algorithms with only half of them working correctly. Plus, your maintenance cost increases constantly.
What I prefer instead is to have one algorithm implementation for a specific task. Example: We now have
loop_subdivision() for triangle meshes,
catmull_clark_subdivision() for quad meshes, and
quad_tri_subdivision() for mixed meshes. And that’s it. No need for three different triangle mesh subdivision schemes.
Don’t Misuse Inheritance
Even though I advocate for using functions above, classes and OOP still have their place. Inheritance is a key principle of OOP but it can be easily misused. This can be the case when using inheritance for purely technical reasons, code re-use, or out of convenience and laziness. If your inheritance does not represent an is-a relationship, you should be skeptical.
In our library, the
SurfaceMeshGL class responsible for rendering meshes was inheriting from the core
SurafaceMesh data structure. The main reason was just code reuse and convenience. It was convenient to have direct access to the
SurfaceMesh internals. It was convenient to have a single
SurfaceMeshGL member in all viewer classes and to modify the mesh through the
I am now using an independent
Renderer class that only has a single responsibility, and I use composition in the viewer classes instead of relying on inheritance. Basic stuff, I know.
There’s more work to come. I’m not yet happy with all aspects of the API, and there are other areas of doubtful complexity such as the inheritance hierarchy of the viewer classes.
References and Further Reading
Related articles I wrote:
- Keep it Simple, Over Time
- Your Code Doesn’t Have to Be a Mess
- C++ Member Functions vs. Free Functions