One of the most profound insights I have learned about OO is that class design—the shaping of classes & types—is best informed by what processing needs to do, rather than the ‘kind’ of entities it goes between.
What we are talking about here is behavior, rather than trying to categorize entities at rest. Program code only acts by being executed; classes & interfaces (types) are a mechanism to dispatch that execution to specific methods.
This shows that OO is really about active behavior, rather than any other arbitrary notions of classification.
But Shouldn’t Inheritance Just Follow Entity Properties?
At this point, we’ll address a common interjection—what about bean-style entities, and their properties? Surely inheritance should just follow these!
The answer here is that reading state is a behavior; and often a simpler one, amongst the more important (data processing & calculation) interactions an entity has to participate in. Class design must include all behaviors in modeling, and complex interactions are often the more important factor in guiding design.
An extra tip: Many young players assume writeability should go hand-in-hand with readable state; however writeable state is actually a much more specific & constrained behavior. For simple entities writeability normally implies read, but the reverse is not always true. If your v2 app needs to introduce calculated values or derived information, having designed all your entities to be writeable in v1 will come back as a design decision to bite you.
The Shape of the Machine
When considered on a basic level, the purpose of most software is to process & transform data. Even in data storage software—such as a database like Oracle—engine internals focus on processing index blocks, streaming join operations, and the like. Program code is always concerned with activity, never with rest.
Comparing algorithms—like quicksort, a hashmap, or a shortest-distance graph traversal algorithm—we find these have the same structure, regardless of what data they may be processing.
To sketch a picture: Software is a machine, and its design should be based on what it does. What the inputs look like when standing still may inform us, but it’s a secondary consideration.
Entities Have Multiple Interactions
OO is a relatively modern invention. Though it became popular in the ’90s with C++ and Java, it's powerful & complex enough that it was only in the following decade that how best to use OO has really started to be understood.
And one of the struggles developers have with learning OO is the plethora of trivial animal examples that fail to illustrate any significant behavior.
So we’ll break that paradigm here, with an Animal example that actually demonstrates multiple interactions & significant behavior—the kind of space real-world OO design needs to work in.
Let’s consider a Zoo. Following our naive instincts and classifying animals biologically, this presents what might look like a good initial design:
This design is a great example of an entity categorization “by their nature”—an assumption of entities at rest, without really considering what anything in this system needs to do.
If this were accepted and coding started, everything would work fine for the first interaction—say, feeding—as there would be no other conflicting inheritance requirements in the hierarchy. The limitations of the design would only begin to be noticed after the second or third collaboration needed to be “shoehorned” in.
The problem is that the multiple interactions a real entity model needs are not effectively supported in the above design.
Let’s look at an alternate approach. This focuses first of all on “doing” within the Zoo:
What we see here is a completely different emphasis. This design identifies key “interaction roles” for the animal—Eats, Lives, and Displays—and defines these as the focus of interaction with overall Zoo activities (feeding, housing/enclosures, and visitors).
This reflects the primary need of the Zoo to know how an Animal should be fed, enclosed, and promoted. These are the interactions required daily to actually manage & run the zoo. As we see, these behaviors are far more important than how the animals are classified.
While we don’t explicitly detail the Animal hierarchy in our model now, a hierarchy is possible—but we should avoid over-complicating. This will lead to an interesting topic later, the question of composition versus inheritance. But we're focusing on the primacy of behavior, for now.
What this modeling diagram shows, is that interactions should be identified based on behavior. These can be implemented either via method overriding or composition, but what is crucial, is to identify them before putting an incompatible hierarchy in place.
Once interactions are identified, OO design can better focus on dispatching behavior. When behaviors vary independently of the entity (or each other), this implies that behaviors could be factored out of the entity.
Factoring a behavior out makes the behavior a first-class object, and gives it its own type hierarchy. While the Animal entity now uses the behavior by composition, this gives us total freedom of OO techniques to implement and vary behavior; and it enables the reuse of it, by any animal entity.
Where we end up with this approach, is a new focus of OO classes and hierarchy on modeling behavior. The idea of entity being a monolithic element has been deprecated; instead, we are breaking down complexity into separate elements, which OO techniques can easily model.
This shows how applying a behavioral perspective can allow the core requirements of a system to be better revealed, and OO applied to implement them.
This allows a successful design where new Animals and even entire new interactions could easily be added.
'Doing' vs. 'Being' in the Java Libraries
The Java libraries provide a frequent demonstration of the principle of focusing on ‘doing’ rather than ‘being.’ Class and interface design in many areas of the Java API focuses on streams, strategies, readers, or providers; all of which are behavioral.
Even in the area of data storage—java.util Collections, which is mostly about 'being'—the most key element of commonality in the API remains behavioral.
Collections offers an important and useful library of data structures. While many of these APIs are tied to the data structure, iteration is the most fundamental operation of all collections, and this is abstracted as a behavior.
Having Iterator modeled as its own interface enables Java code to iterate over any type of collection regardless of the underlying implementation. This is so important, that it’s almost regarded as a given; but it illustrates the importance in OO of focusing commonality and reuse on behavior.
Perhaps, though, the best examples of behavioral OO design can be found in the java.io package.
Let’s consider java.io.File—surely a file that should read and write? Actually, as we know, files are read & written via FileInputStream and FileOutputStream.
Instead of placing behavior on the entity, the Java IO library places key behavior in two separate classes.
This opens the door to the crowning glory of java.io: the ability to abstract, delegate, and wrap streams. Summing up these 3 key achievements:
- Abstraction and commonality: the ability to abstract input/output streams across a variety of underlying mechanisms.
- Delegation: the ability to delegate (e.g. BufferedInputStream delegating to a real FileInputStream).
- Wrapping into higher-level objects: the ability to wrap low-level streams to higher-level objects (e.g. InputStreamReader wrapping InputStream, or ZipFileInputStream wrapping FileInputStream).
The last concept—wrapping—seems to me, most powerful. This illustrates the ability to build high-level concepts (of great use) from lower-level building blocks.
All taken, java.io gives a great deal of power & immense design flexibility. This shows the power of design for ‘doing’ rather than ‘being;’ none of this would be possible if read() and write() had been implemented directly on File.
Summing up, many of the beginner difficulties with OO inheritance come from an excessive focus on “what it is,” not “what it does.” This confusion can be most effectively untangled by modeling interaction & behavior first, before considering entity inheritance.
Principle 1: Design for processing & behavior first, then design data at rest.
This article is part 1 of the Advanced OO Design series from LiterateJava.com. Add your comments on OO design now.