Object-Oriented Design: Insights

History

Computer programming was initially something done by electrical engineers. Back then, it was a matter of setting up logic gates.

Over time, it has remained a logic exercise. Regardless of the language, computer programmers were almost always people with a high aptitude for math and/or logic. All programming tasks have been viewed as merely an exercise in logic. Thus, any body of code that produces the desired result has been regarded as an acceptable product.

If you are a hobbyist, as most programmers of the 70s and 80s were, such an approach is perfectly fine.

Many such hobbyists became professional programmers. Having heard and read their whole lives that programming is a logic exercise, they simply threw together any set of code which accomplished the goal. The result was usually something so difficult to maintain, often directly proportional to its size, that people who leafed painstakingly through the code trying to debug or improve it were often called wizards.

Nowadays, the most common term in the IT world for such people is hackers. This has nothing to do with illegal intrusions; it simply refers to people who attempt to untangle the mess of an older program.

The problem is, reading one’s way through the code, pretending to be the computer and guessing what the computer would do at each line, is a painstaking and grievously slow process. It is easily the single biggest consumer of the average professional programmer’s time.

A Better Way

One of the goals of object‐oriented development is to eliminate this enormous drain on the programmer’s time. Over the decades, most have accepted this drain as a standard part of the job, but this is wrong, and backward: the programmer’s job and goal is to create an application, not deal with the problems of past code, particularly code written by others.

Imagine if nearly all of the programmer’s time were spent on creating the task given to him/her, instead of tediously pouring over someone else’s inscrutable, hurried spaghetti code.

At this time, if you are silently (or aloud) emphasizing that the idea is absurd, that programmers have always had to do this, and to suggest otherwise is whining, then you may stop reading now. You will never understand OO design and you were never meant to understand it.

The Transition

A result of the logic exercise approach is that computer science students have been taught for decades that programming concepts are universal, and can be applied to any language, though some languages make some concepts easier to implement.

That is true. However, it has had the tragic effect of leading most programmers to believe that once they are armed with the basic knowledge of logic concepts and programming concepts, they need learn no other skills. That is false.

It is impossible to become an object‐oriented programmer by applying the old set of logic skills which have been traditionally used in programming for the past few decades. It is necessary to learn some new skills.

This seems to terrify many programmers, but it should not. Most of them are completely capable of learning the new skills with no more difficulty than they learned their original programming skills.

But make no mistake, attempting to apply the old programming logic is programming logic philosophy will result in a miserable failure. Even so, it happens all the time; such programmers will often be heard saying that OO is worthless overhead and I don’t see what the big deal is. Often they will claim they have done OO when in fact all they have done is attempted to juggle the Java syntax, particularly extends and implements, in fanciful and elaborate ways. (Subclassing is a part of abstraction, but otherwise has almost nothing to do with OO design or development.)

Those who have implemented OO for real, by learning new skills and not trying to just stick with their old skills, know exactly what the big deal is.

The Contract is King

At the core of OO is the contract.

A contract is hereby defined as the description of a class or method or code module or object, which guarantees exactly what the documented class, method, etc., will do, and the circumstances under which it is guaranteed to do it.

Worship the contract. The contract is king. The contract is law. The contract supersedes all other things. When you program, you program to the contract.

(For brevity, from here on I shall use method to refer to anything with a contract, even though the thing in question may be a method, class, object, or module, and probably some other things as well.)

Examples

A Java interface is an ideal means of coding a contract. The ideal interface is roughly 90% contract and 10% method signatures and constants.

Logic exercise programmers see the interface as nothing but a means of forcing certain methods on a group of classes. This is because they see programming as a game of logic, and thus they see the use of syntax as the most important part of programming.

Such people believe that their class has successfully implemented a Java interface once it contains all of the interface’s methods. They are wrong.

A simple example is the ListModel interface (in the javax.swing package). It has two data methods, getSize and getElementAt. It also has two other methods which deal with adding listeners which can monitor changes to the data model.

Classes which use ListModel expect it to work as its contract specifies it must work. Many programmers, if they were writing a ListModel implementation from scratch, would leave the addListDataListener and removeListDataListener methods blank. In doing so, they are violating the contract of ListModel. Their class will not work as the users of ListModel expect it to work — specifically, as the contract promised users of ListModel it would work. So while the programmer has implemented the method signatures, he has not successfully implemented a ListModel. His class will not behave like a ListModel is required to behave by the contract. JLists, the primary users of ListModels, will break if given such a broken model.

Simpler examples are the hashCode and equals methods of the Object class. If you intend instances of your class to be members of a Set or keys in a Map, you must implement those methods. If you don’t, you will have something that compiles — but it is not guaranteed to work, and in fact may not work at all in Sets or Maps.

The point of the above examples is that it is no longer sufficient to solve the logic puzzle and create something that compiles. OO demands more.

If this seems like pointless extra work, then consider the savings. Ten minutes spent writing a contract (in the form of javadoc) will save oneself and all other programmers hours or days of code scrutiny. Which consumes more time, writing an explanation or trying to figure out months‐old code that lacks documentation and has numerous unclear side effects?

OO Sacred Principle #1: Robustness

The contract is a promise, to all users of a method, of exactly what it will do, what it will change, and what it will return. By definition, if the contract does not promise to do something, you cannot rely on the method to do that thing, even if the method sometimes seems to do that thing. Side effects are not guaranteed if not documented.

Many, many programmers will rely on side effects anyway. This comes back to the logic exercise train of thought: if it works, it must be acceptable.

The problem is, it’s not guaranteed to work. This is the essential difference between a hobbyist and a true professional developer: one simply seeks a logic path that will accomplish the goal, while the other seeks to make every part of the program such that it is guaranteed to work under all circumstances. Contracts make that possible.

This guarantee is what makes true OO code robust. It is robust if it is predictable. It is most predictable if it does precisely what its documentation — its contract — says it does.

OO Sacred Principle #2: Stability

Any programmer who has attempted to hit a moving target (changing requirements or changing APIs) can testify that it is an order of magnitude higher in difficulty than programming to a stable set of requirements or to a stable interface.

A contract should never change. If there is ever a need to change it, then it was written poorly to begin with, and should have been given more consideration when it was conceived. It is not code, after all; it is merely a promise of functionality which a method will deliver.

If a different set of functionality is needed, the best thing to do is create a new method (possibly with the same name but different parameters), and in some cases, deprecate the old one. Often it is possible to have one method call the other, which is much better than deprecation.

A contract is worth very little if users of the method have to worry about it changing. A contract needs to represent a stable interface if others are going to rely on it. If it changes, then programmers are back to the burden of having to check up on it to see what the latest version is. While that certainly is not as bad as having to check the code, it still detracts significantly from the task at hand: that which the programmer has actually been tasked to do.

Retaining old methods and either deprecating them or having them call or be called by newer methods is a form of graceful evolution. It preserves the interface as well as the contract, so other programmers don’t have to halt their work and rewrite their code to catch up to a changed interface.

This can also be done with classes, by changing their inheritance but keeping all existing methods. When Vector was altered to implement Collection, it kept all of its old methods. Java 1.5 changed Collection so that it extended Iterable, but no methods had to change in Collection.

A stable interface allows other programmers to use a method or class with confidence, and concentrate on their own task instead of inspecting others’ classes.

Adding to a contract does not affect its stability, except in the case of interfaces which are intended to be implemented by others. In that case, it makes more sense to create a subinterface, so the original interface remains as it was and programmers are not burdened with the upgrade. (Cases where all other programmers need to be forced to upgrade are very rare, and are usually a perception of someone who believes that leaving a slightly deficient interface in place commands a higher cost than forcing all other programmers to play catch‐up. That perception is wrong.)

OO Sacred Principle #3: Opacity

Implied by the notion of stable interfaces is a division of responsibility. It is the responsibility of whoever wrote the contract to make sure the method lives up to the contract.

However, other programmers who use the method must be absolved of that responsibility. It is not their task to inspect the method’s code in any way. They have the contract; it tells them what the method is for, and what it will do. They have all they need in order to use it. Thus, they can devote their time to the task they’ve been given, instead of the onerous inspection of someone else’s code.

This is the meaning of a core OO concept: opacity. A method with a contract is opaque — you cannot see (or at least, you are not meant to see) how it is implemented, because you as an OO programmer are trusting the contract.

Even recalcitrant programmers will admit that they’d rather spend their time doing their own task than deciphering the code of others. A programmer who prefers to inspect the code of everything he uses is a liability and should be discarded or isolated from important projects as early as possible. He will make more work for other programmers instead of furthering their progress, because he will expect them to painstakingly inspect his own code, and he will document it poorly or not at all for that reason.

OO Sacred Principle #4: Abstraction

The division of responsibility requires that programmers do not attempt to take responsibility for everything. They must learn to trust that other components can do their job and do it well. In the case where the other component does not do its job well, it is the responsibility of the component’s author to fix it so it fully lives up to its contract.

Inherent in this is separating function from implementation. Java interfaces support this concept well: when you are given an interface, you are only given a subset of an object’s functions. The actual implementation is irrelevant. If you really really want to inspect the object to see what its concrete implementation is, you may do so, but there is a reason such code is verbose and ugly. You are not meant to do it.

The simplest example is the List interface. If your code has been handed a List, it may be an ArrayList, it may be a Vector, or it may be an unnamed List implementation such as those returned by Arrays.asList and Collections.unmodifiableList.

The point is that it doesn’t matter. No matter how much a neurotic CPU‐cycles‐obsessed programmer may think he needs to know which type of List it is, in OO programming you absolutely do not need to know and you have no business attempting to find out. You have an object which fulfills the function described by the List contract; that is all you need to know. Trust the implementor to fulfill the contract.

The separation of function from implementation is the core of abstraction. Abstraction takes many forms besides interfaces and their implementations. Many basic design patterns are forms of abstraction, such as wrappers, façades, and bridges.

OO Sacred Principle #5: Isolation

Abstraction allows for isolation between parts of a system. If the implementation for one part changes, the other part will not be affected, because the abstraction prevents access to the actual implementation. The need for one person’s code to be updated whenever another part changes is called shear, due to the negative frictional effect it has. When a developer is forced to catch up by updating his own code to account for changes to the public interface of code written by others, it detracts heavily from the developer’s ability to tend to the original task he was given. Such catch up practices are not a necessity of software development.

It follows that isolation cannot be accomplished without a stable interface: a changing interface is just another form of shear.

Isolation is the basis for a multi‐tiered architecture. Multi‐tiered architectures usually create tiers based on functionality groups which are subject to change. For instance, a customer may decide to use a different storage tool; the goal of isolation of tiers is to eliminate any shear that would be caused by such a customer decision. The user interface should be not affected by the type of persistence used.

OO Sacred Principle #6: Independence

A principle which has actually been true of all code, object‐oriented or not, for a long time, is the need for independence. Dependencies are external references. In the case of code, dependencies are references to non‐system classes outside the class’s package. In the case of a code module (a jar file, war file or ear file), dependencies are required libraries beyond system classes (the Java core classes and the J2EE classes).

Each and every dependency weakens code by creating a point of fragility: the code cannot work without that external object.

A program which has no dependencies is ultimately portable: it is ready‐to‐go, and can be placed anywhere and run.

A program which needs other libraries requires a setup procedure. Each user has to obtain the necessary libraries and make sure they’re in the classpath. Obviously the likelihood that this won’t be done correctly is high. (It can be mitigated by using Java Web Start, and sometimes by having an intelligently written manifest file in a jar file.)

A large number of interdependencies usually indicates poorly organized code with little or no isolation of functionality. This is evident when the javadoc is generated: either it needs to include the javadoc of the libraries on which it depends, or the javadoc will contain references to types as plain text instead of hyperlinks. This degrades the use of the javadoc significantly, as the reader does not have on hand information on how to handle (or create) those types. And clear, useful javadoc is critical since it defines the contract.

In general, choose to use Java’s internal implementations whenever possible. For example, it is extremely unlikely that a third party’s implementation of collections is so superior that it merits the significant cost of having to depend on that external library. You need a function, not an implementation; if Java’s function performs adequately, go with it.

Implementing OO

In Java, contracts are defined in javadoc. Javadoc comments are not merely comments and cannot be dismissed in the way that comments have been traditionally dismissed by programmers for the past several decades.

A Java file without javadoc is not complete. If it has no contract, it is not usable, since one cannot tell what it does; one can only guess. The benefit of predictability, of functions which are guaranteed to have certain behaviors, is missing.

Javadoc is integrated with the Java compiler. In all versions prior to 1.5, it is the only way to designate a deprecated class, method or field. It is still required for serializable fields.

Because contracts are the foundation of OO, they need to be clear and concise. They must be well worded and unambiguous. This means that one of the skills that a programmer must have, or learn, in order to be an effective OO programmer is good communication skills. Sometimes it is necessary to stop and take a few minutes to decide how to best and most effectively word a contract. A programmer who is unwilling to take that time is not an OO programmer and is a liability. It is not acceptable to expect others to figure out what he meant.

Unfortunately, there are programmers who simply cannot learn to communicate effectively. They have reached a barrier: they can never go on to be OO programmers, and they must not be allowed to work on an OO project unless they are frequently paired with someone who can write out the contracts for them.

A practice which has long earned respect among logic exercise minded programmers is devising hacks — clever shortcuts or manipulations of quirks of the system to achieve a goal. This had a real benefit at one time: until the late nineties, computing power and memory were precious, highly limited resources. But they’re not anymore.

Software is more reliable, has a faster turnout, and has a much faster maintenance time when the classic K.I.S.S. rule is followed: Keep It Simple, Stupid.

No one needs tricks anymore. They don’t help. Saving a few CPU cycles or ten bytes of RAM with an inscrutable, spaghetti‐code web of un‐debuggable classes is not an accomplishment. It is an affront to the rest of the team, the programmers who will have to deal with it and maintain it in the coming months.

Do it the simple way. Your team will thank you, and you yourself will find things much easier to navigate when you have to return to work out bugs.

Limits

OO is not a magical panacea. It does not do the work for the programmers.

OO provides benefits many times the effort that is put into it. But that effort must still be put into it.

Cheating in order to defeat OO almost always results in the forfeiture of OO’s benefits. Many programmers do not want to be bothered with the last mile of OO. Failing to write documentation is the primary manifestation of this but there are many other examples.

For instance, if a method needs to return two strings, an inferior programmer will write the method to return an array of Strings. A superior programmer will go the extra mile and create a small class, perhaps even a static inner class, like this:

public static class Results
{
    public String name;
    public String location;
}

It seems simple enough, but the benefits that come from clearly labeled data are enormous. Merely returning an array of Strings is fraught with weak points: the caller needs to know there are always exactly two elements in the array; the caller needs to know which is the first element and which is the second.

A good criterion is how much work a programmer who has just happened upon the method for the first time must do, in order to use the method effectively. It should be immediately obvious what is expected and what is returned.

Short‐changing the contract, opacity, isolation or independence of an object surrenders the robustness and usability of it. It only takes a few such shortcuts to end up with spaghetti code.

Keeping a large code base organized rather than chaotic requires frequent diligence. Peer reviews are an excellent and effective way to accomplish this.