Understanding Object Oriented Programming

这是一篇很早的文章,讲的关于面向对象,原文地址:Understanding Object Oriented Programming。这里有关于这篇文章的评论如此理解面向对象编程,很有有趣。我觉得这篇作为一篇入门讲面向对象的例子还是很不错的,通过不同的例子讲述了不同人的实现想法。最后用策略模式+工厂模式来实现,来达到消除if-else。

The code on this page grew out of a discussion on the Object Technology in Computer Science Education list server. The discussion had been going on for about 36 hours in late March and early April 2000 centered on the question of “What is OO really; is it a real paradigm, different from procedural programming or is it just a packaging mechanism for procedural programming?” Both of the authors believe that it is a real paradigm shift, requiring a change in mental model in the practitioners. Winder produced the first three of the following code fragments to show the difference in styles between hackers, procedural programmers, and (naive) object oriented programmers. Bergin then added the more sophisticated OO version that appears last.

TheProblem

The problem to be solved is to output a value judgment about operating systems. The assumptions being (of course) that UNIX is good and Windows is bad.

Hacker Solution

public class PrintOS {
public static void main(final String[] args) {
String osName = System.getProperty("os.name");
if (osName.equals("SunOS") || osName.equals("Linux")) {
System.out.println("This is a UNIX box and therefore good.");
} else if (osName.equals("Windows NT") || osName.equals("Windows 95")) {
System.out.println("This is a Windows box and therefore bad.");
} else {
System.out.println("This is not a box.");
}
}
}

Their claim:

It works doesn’t it what more do you want? Also I got mine implemented and working faster than any of the others, so there.

Evaluation:

While this solves the problem, it would not be easy to modify in the future if the problem changes. In particular, if we need to add new operating systems, we need to extend the if structure. If we want to add additional functionality for each operating system, we would likely see this expand to nested if statements. This would get unwieldy over time. Thus, the hacker has solved the immediate problem, but made little progress on future evolution of the program.

Procedural Solution

public class PrintOS {
private static String unixBox() {
return "This is a UNIX box and therefore good.";
}

private static String windowsBox() {
return "This is a Windows box and therefore bad.";
}

private static String defaultBox() {
return "This is not a box.";
}

private static String getTheString(final String osName) {
if (osName.equals("SunOS") || osName.equals("Linux")) {
return unixBox();
} else if (osName.equals("Windows NT") || osName.equals("Windows 95")) {
return windowsBox();
} else {
return defaultBox();
}
}

public static void main(final String[] args) {
System.out.println(getTheString(System.getProperty("os.name")));
}
}

Their claim:

Java is a wonderful procedural programming language; it naturally supports top-down decomposition which is clearly the only way of analyzing and designing quality solutions to problems – as exemplified by this example.

Evaluation:

The procedural programmer has made some progress on the larger problem. If an operating system needs to be added, we extend the if statement in the getTheString function and add a new function for that OS. However, if the functionality of each OS needs to be extended, what we are likely to see is that the if statement will most likely be replicated elsewhere in the program each time we need to make the distinction between operating systems. Once that happens, whenever we add a new OS or change or add functionality we will need to find ALL of these if statements and update them compatibly*. This is very error prone and results in entropy setting into such programs over time.

In effect the programmer is using ad-hoc polymorphism. We want different things to happen, but the programmer must specifically make the choice of what is to happen in each instance.

Naive Object Oriented Solution

This solution requires several classes in several files.

// PrintOS.java
public class PrintOS {
public static void main(final String[] args) {
System.out.println(OSDiscriminator.getBoxSpecifier().getStatement());
}
}

// ---------------

// OSDiscriminator.java
public class OSDiscriminator { // Factory Pattern
private static BoxSpecifier theBoxSpecifier = null;

public static BoxSpecifier getBoxSpecifier() {
if (theBoxSpecifier == null) {
String osName = System.getProperty("os.name");
if (osName.equals("SunOS") || osName.equals("Linux")) {
theBoxSpecifier = new UNIXBox();
} else if (osName.equals("Windows NT") || osName.equals("Windows 95")) {
theBoxSpecifier = new WindowsBox();
} else {
theBoxSpecifier = new DefaultBox();
}
}
return theBoxSpecifier;
}
}

// ---------------

// BoxSpecifier.java
public interface BoxSpecifier {
String getStatement();
}

// ---------------

// DefaultBox.java
public class DefaultBox implements BoxSpecifier {
@Override
public String getStatement() {
return "This is not a box.";
}
}

// ---------------

// UNIXBox.java
public class UNIXBox implements BoxSpecifier {
@Override
public String getStatement() {
return "This is a UNIX box and therefore good.";
}
}

// ---------------

// WindowsBox.java
public class WindowsBox implements BoxSpecifier {
@Override
public String getStatement() {
return "This is a Windows box and therefore bad.";
}
}

Their claim:

Well I managed to get both Singleton and Factory Method into the implementation so according to all the hype about object-oriented programming and patterns it must be good.

Note: The factory here is a kind of naive singleton.

Evaluation

This programmer has made quite a lot more progress toward the goal of writing a maintainable program. In particular, if we need to add an OS, we extend the if statement as before, and write a new class for that OS. This is similar to what the procedural programmer had to do. However, if we need to add functionality for each OS, we only need to change the classes that deal with that OS. The if statement in OSDiscriminator is still a “logic bottleneck” but it is the only one in the program. This means that the location of change is easy to find (the classes that implement the functionality). Also, if we add functionality by changing the interface BoxSpecifier, then the compiler will tell us if some class fails to implement the new required functionality. We won’t have to search the program for the locus of each change with no help from the tools.

However, this solution still does ad-hoc polymorphism in the if statement. Object oriented programming attempts to remove all such ad-hoc decision making. Every if and every switch should be viewed as a lost opportunity for dynamic polymorphism. If we can replace this with dynamic polymorphism then the program will be much easier to maintain.

Sophisticated Object Oriented + Patterns Solution

In the following, PrintOS.java and BoxSpecifier.java are unchanged from the above.

// PrintOS.java
public class PrintOS {
public static void main(final String[] args) {
System.out.println(OSDiscriminator.getBoxSpecifier().getStatement());
}
}

// ----------

// OSDiscriminator.java
public class OSDiscriminator {// Factory Pattern

private static java.util.HashMap storage = new java.util.HashMap();

public static BoxSpecifier getBoxSpecifier() {
BoxSpecifier value = (BoxSpecifier) storage.get(System.getProperty("os.name"));
if (value == null)
return DefaultBox.value;
return value;
}

public static void register(final String key, final BoxSpecifier value) {
storage.put(key, value); // Should guard against null keys, actually.
}

static {
WindowsBox.register();
UNIXBox.register();
MacBox.register();
}
}

// ----------

// BoxSpecifier.java
public interface BoxSpecifier {
String getStatement();
}

// ----------

// DefaultBox.java
public class DefaultBox implements BoxSpecifier {// Singleton Pattern

public static final DefaultBox value = new DefaultBox();

private DefaultBox() {
}
@Override
public String getStatement() {
return "This is not a box.";
}
}

// ----------

// UNIXBox.java
public class UNIXBox implements BoxSpecifier {// Singleton Pattern
public static final UNIXBox value = new UNIXBox();

private UNIXBox() {
}

@Override
public String getStatement() {
return "This is a UNIX box and therefore good.";
}

public static final void register() {
OSDiscriminator.register("SunOS", value);
OSDiscriminator.register("Linux", value);
}
}

// ----------

// WindowsBox.java

public class WindowsBox implements BoxSpecifier {// Singleton Pattern
public static final WindowsBox value = new WindowsBox();

private WindowsBox() {
}

@Override
public String getStatement() {
return "This is a Windows box and therefore bad.";
}

public static final void register() {
OSDiscriminator.register("Windows NT", value);
OSDiscriminator.register("Windows 95", value);
}
}

// ----------

// MacBox.java
public class MacBox implements BoxSpecifier { // Singleton Pattern

public static final MacBox value = new MacBox();

private MacBox() {
}

@Override
public String getStatement() {
return "This is a Macintosh box and therefore far superior.";
}

public static final void register() {
OSDiscriminator.register("Mac OS", value);
}
}

Their claim:

Aaaaahhhhh. And besides, I added important functionality – Mac OS.

Evaluation

Here we have turned the OS objects into singletons, so there can be only one such object in each of these classes. This may be desirable or not. If it is not, then the factory wouldn’t return the objects in the hash table, but would return clones of them instead.

Here we have maintainable code. To add a new OS, like the Mac OS, we just add a new class and add its registration to the factory. To change the functionality we change the OS classes. To add new functionality, we either modify the OS classes, or extend them. Note that there is NO ad-hoc polymorphism here except the single test for null in the factory.

Deconstruction

Whether it is clear or not, the mental processes of the programmers who wrote these different versions was quite different. The hacker wanted to get the immediate job done at all cost. The procedural programmer views the nature of computation as a decomposition of a function into sub-functions (helper functions) that solve sub-problems. The object-oriented programmers see the nature of computation as a swarm of interacting agents that provide services for other objects. Further, the sophisticated OO programmer lets the system take care of all polymorphic tasks possible. This programmer sees the essence of object oriented programming as the naive object-oriented programmer may not.

Notes

Singleton and Factory are discussed in Design Patterns by Gamma, Helm, Johnson, and Vlissides (Addison-Wesley, 1995). This is the now famous “Gang of Four” or GOF book. The DefaultBox is a kind of Null Object. This pattern is by Bobby Wolfe and can be found in Pattern Languages of Program Design 3, edited by Martin, Riehle, and Buschmann (Addison-Wesley, 1998)

While object oriented programmers try to avoid ad-hoc polymorphism it isn’t always possible. The hard-to-impossible cases are when dealing with primitive (non-object) data in hybrid languages like Java, parsing input, and when creating new objects. Here, however, we have solved the creational problem with a simple factory containing singletons. The creational problem can be solved in general through the use of reflection, such as the Java Reflection API. The other situations are less tractable.

For more on ad-hoc polymorphism, see Bergin’s Selection Patterns.
For more on dynamic polymorphism, see Bergin’s Polymorphism Patterns.
For more on how the object oriented programmer thinks, see Bergin’s Object Patterns.

More

Here is another perspective on the same ideas from out friend and colleague Dung X. Nguyen of Rice University. By the way, Dung has done a lot with using patterns to enhance object-oriented code seen by students. He often works with Stephen Wong of Oberlin College. They have some nice papers on this in the last few SIGCSE conference proceedings.

Note: For a discussion on why replicated code, such as that in the if structure of the procedural solution, is bad see Kent Beck’s discussion on OnceAndOnlyOnce on the Wiki Wiki Web.

Last Updated: July 30, 2000

Comments