A Case Study on the Philosophy of Software Design

A photo of a person writing in a notebook with the title of the article overlayed.

Recently, I was thinking about how there are so many ways to approach software design. While some of these approaches have fancy names, I’m not sure if anyone has really thought about them philosophically. While I don’t think I’ll make any major steps towards that today, I did want to share something I’ve been thinking about.

To give you a taste of what I mean, I often think of programming disciplines in the same way you might think of martial arts. There are many different schools, but folks generally only practice one particular style. Are there different schools of programming arts? It makes me wonder!

Table of Contents

Comparing Two Disciplines

Like most articles I write, this one starts with a conversation I was having with some of my students. Specifically, in the course I teach, we use our own software design discipline. Because we teach it so early in the curriculum, students often don’t know what real world software looks like, so we leave a little room at the end of the course to give them a taste.

At this point, it’s my fourth time teaching the course, but only now have I been reflecting on the differences between our discipline and the approach Java takes in its standard libraries. The exact moment that came to mind for me was when I introduced Input/Output (IO) in Java. Just to illustrate what I mean, here’s some sample code for how to write to a file in standard Java:

public static void main(String[] args) throws IOException {
  PrintWriter output = new PrintWriter(new BufferedWriter(new FileWriter("data/test.txt")));
  output.print("Hello, World!");
  ...
  output.close();
}

Meanwhile, here’s the code for writing to a file using our own API and discipline:

public static void main(String[] args) {
  SimpleWriter output = new SimpleWriter1L("data/test.txt");
  output.print("Hello, World!");
  ...
  output.close();
}

Hopefully, it’s clear that the primary difference is on line 2, where we have an object that handles writing that doesn’t throw an exception. At a glance, you might find our approach nicer, but I wanted to really unpack the differences in this article. So, let’s do that!

Introducing the OSU Discipline

In order for you to really appreciate the difference between our discipline and the way Java handles things, I want to share both my rough description of how our discipline works and what it looks like in practice. I should note that I did not develop this discipline, and I don’t fully subscribe to it personally. However, I like teaching it as a means of showing students that there is more than one way to approach a problem.

To start, our discipline is deeply rooted in Design by Contract. Naturally, this is a very academic (and perhaps math-based) approach to software development where formal contracts are written for each method. These contracts specify any preconditions (i.e., things that must be true before the method is called) and postconditions (i.e., things that must be true when the method returns). The broader idea is that as long as the precondition is true, the client can expect the postcondition to be true. We denote the precondition using the @requires javadoc tag, and we denote the postcondition using the @ensures javadoc tag.

At the class level, we also define the representation invariant, which can roughly be described as the rules we want to impose on our instance variables. We denote the representation invariant using the @convention javadoc tag. In addition, we define the abstraction function, which can roughly be described as the interpretation of the instance variables as some abstract value (i.e., how can the instance variables be interpreted as the object we’re implementing?). We denote the abstraction function using the @correspondence javadoc tag.

Speaking of tags, we also explicitly state what will happen to any parameter of a method. We call these parameter modes (and I believe I’ve written about them before), but basically there are four kinds: restores, replaces, updates, and clears. By default, we assume that a parameter will be restored. If it is not, the method must include one of @replaces, @updates, or @clears in its javadoc contract.

In addition to these formal contracts, we also have a particular approach to designing software components. Specifically, we use a layering approach composed of interfaces and abstract classes. I could perhaps write an entire article about this design process, but the big picture is as follows:

  • Every component inherits a set of standard methods from a top-level interface called Standard
  • Each component is then defined by an interface containing its most minimal behavior, which we call the kernel interface
  • Each component then has more complex functionality added in a separate interface, which we call the enhanced interface
  • Each component then has only the enhanced methods implemented in an abstract class using the kernel methods
  • Finally, each component has the kernel (and Standard) methods implemented in a class

The purpose of this design is to allow for multiple implementations of the same component while reducing the amount of overall labor needed to implement all the methods. Specifically, only the kernel and Standard methods need to be implemented for each unique implementation.

In addition to all the rules above, several additional considerations make up the discipline. For example, components are designed in such a way to eliminate aliasing and null references. For example, rather than being able to access an element in a list directly, our API requires that you remove the element. If a method produces an alias, the method must be documented to include an @aliases javadoc tag. Likewise, null is often dealt with by allowing a component to exist in some “empty” state, such as an empty tree or an empty list.

As you can imagine, with a discipline this rigorous, it would be hard to teach it using existing Java components. Therefore, the creators of the course also created their own API with components that follow the discipline. Of course, this is a tough sell for students as they’ll never use these APIs once the course is over, but that’s a topic for a different time.

How Does the Java Standard Library Differ?

If you recall, the sample code I shared above showcased the standard Java approach to writing to a file. On the surface, the approach seems really painful. In order to write to a file, we need to construct a FileWriter object. Of course, we don’t want to use the FileWriter directly because writing directly to a file is costly. Instead, it’s better to write to the file in chunks using a buffer, so we pass the FileWriter to a BufferedWriter. Naturally, the BufferedWriter object leaves a bit to be desired in terms of methods, so we pass it to a PrintWriter. That way, the output stream is virtually indistinguishable to System.out.print().

While writing to a file in this way may seem painful, it actually surfaces a really clever software design approach: dependency injection. Rather than baking in all the dependencies—such as in the SimpleWriter example—the standard library will write to anything that fulfills the Writer interface. This shifts the responsibility to the client to construct an object that meets their needs: it’s plug and play.

Once I noticed this sort of philosophy of “deferring to the client,” I started to see it everywhere. Another place where this is common is the Java Collections Framework, which specifies all of the usual data structures like List and Set. If you take a peekOpens in a new tab., you’ll see a broad interface that includes all of the methods that a data structure is expected to implement. The power of this kind of flexible interface is that objects can sort of be swapped out as needed, such as in the following snippet:

List<Integer> x = new ArrayList<>();
x = new LinkedList<>();

You’ll also find that the Java standard library includes ideas like “optional” methods. Interestingly, this runs counter to Java itself in that you cannot partially implement an interface. Yet, the standard library allows for it by letting you throw an UnsupportedOperationException for any of the optional methods you don’t need.

If this were to be replicated in our discipline, we would not allow any of the methods to be optional. As a result, the API would either have to include unique implementations for all of its data structures (as it does now with descriptive method names like pop for stacks and enqueue for queues), or it would have to include an intricate hierarchy of increasingly specific interfaces (e.g., consider the taxonomy of animals).

Of course, it’s not all sunshine and rainbows on the Java front. Aliasing is still a very real concern in the Java standard library, and it’s almost never explicitly specified (often just implied). Likewise, there are only informal contracts, which make it difficult to know the limitations of a particular method. I’m sure there is some broader message there about the philosophy of deferring to the client being a double-edged sword.

What Does This All Mean?

I’m not sure that I have a well-reasoned argument to make here. In general, I’ve just found myself reflecting on the software discipline that I have been critically teaching for over five years now. When I first approached the discipline, I was highly skeptical of it as it was so violently different than the way I approach software design. Yet, it has somewhat grown on me over the years. I see the intentionality in it, and I understand the history behind it.

That said, do I write formal contracts in my day-to-day work? Of course not. However, I do tease out the bits I like. For example, I am significantly more intentional in what I put in my documentation. I am explicit in what will happen to a parameter, and I try to draw bounds on what inputs I am willing to entertain. This is particularly helpful in a language like Python that has significantly more flexibility in what can be passed around.

Moving forward, I will probably be more intentional with how I handle my dependencies. Do I bake them into my design or do I take a dependency injection approach? I definitely like designs that allow for more plug and play (maybe because I like playing games with mods). It’s why I created the Template interface in my snakemd libraryOpens in a new tab.. Anyone can make their own templates as long as they inherit from the interface.

With that said, I have a lot more I’d like to write, so let’s call this one a day! In the meantime, you can read more like this below:

Likewise, you can support the site even more by heading over to my list of ways to grow the site. Otherwise, thanks for sticking around!

Jeremy Grifski

Jeremy grew up in a small town where he enjoyed playing soccer and video games, practicing taekwondo, and trading Pokémon cards. Once out of the nest, he pursued a Bachelors in Computer Engineering with a minor in Game Design. After college, he spent about two years writing software for a major engineering company. Then, he earned a master's in Computer Science and Engineering. Most recently, he earned a PhD in Engineering Education and now works as a Lecturer. In his spare time, Jeremy enjoys spending time with his wife and kid, playing Overwatch 2, Lethal Company, and Baldur's Gate 3, reading manga, watching Penguins hockey, and traveling the world.

Recent Code Posts