When I use the term “hacker”I mean someone who enjoys programming and is good at it.1 Hackers in my experience tend to be an opinionated and individualistic lot and they tend to appreciate strong opinions and independence of thought in others. The slogan of the Perl language is “There’s more than one way to do it”, and most hackers are motivated to exploring different ways of doing the same job. That said, if adopting a standard way of doing something provides leverage for building better software, then most hackers will agree to adopt the standard (after much dickering about the details, of course).
If you write code because you want other people to use it, it behooves you to use a language that others are familiar with, adopt standard conventions for input and output so that others can interact with your code without learning some new set of conventions, and provide your code in a format so that others can use it as a component of a larger project without having to understand all the details of your implementation. The struggle to meet these basic criteria requires the hacker to negotiate, make concessions and, generally, work within a community of potential users to produce, adopt and adhere to reasonable standards.
Building great software requires a great deal of discipline and interpersonal skill — in sharp contrast with the stereotype of a hacker as an unkempt, uncommunicative obsessive compulsive lacking basic hygiene and addicted to highly caffeinated drinks. It also requires adherence to the lessons of modern software engineering practice. I know how to program in Java (more or less) but I generally don’t think about programs from the perspective of software engineering. I’m pretty good at designing and implementing solutions for specific, narrowly defined problems, but I stink when it comes to structuring large programs of the sort typically encountered in industry.
Students today, budding software engineers, are being taught how to structure large programs. Most of them will never have to implement a basic utility (say for performing numerical calculations or graphical display) of any complexity except as an academic exercise. Most of the basic utilities that anybody might actually need are already implemented as libraries — or will be as soon as anyone recognizes their value. In designing a new program nowadays, you borrow most of the required functionality by “including” or “importing” utilities from existing libraries. I’ll illustrate some lessons from modern software engineering by critiquing some of the Neanderthal coding practices I used when writing code to automate constructing a web site.
I mentioned in the first chapter that in preparing to write this book I kept a journal. At one point, I decided to convert my journal entries into a web site so I could solicit feedback. The plan was to create a separate page for each journal entry and then link them together so a visitor to the web site could jump from one entry to the next in the order in which they were written. Since I’m lazy I didn’t want to create all of these pages by hand; instead, I wanted a program to go through all my journal entries and produce a web site with all the requisite web pages, linked appropriately and formatted for a web browser.
Object-oriented programming(or OOP as it’s often called) as a programming style and a software engineering discipline is touted as facilitating reuse and sharing of code, encapsulation of expertise and management of information by controlling access to implementation details. You don’t need to use object-oriented programming language in order to get these benefits, but the prevailing wisdom is that OOP makes them easier to achieve.
My first introduction to object-oriented programming was an academic exercise in graduate school requiring us to write a program in the language Smalltalk. A little later I had to learn about object-oriented language extensions for a Lisp dialect called Zeta Lispin order to translate some of my software to run on the Lisp Machine (a computer specially designed to run Lisp, soon made obsolete by low-cost, high-performance commodity PCs).
In neither case did I really learn much about object-oriented programming; I just figured out how to get these languages to do what I usually did, namely implement procedures for solving problems that came up in my research. If I had used these object-oriented programming languages as they were intended or if I had absorbed the lessons revealed by the design of such languages, I might have saved myself a good deal of grief. Let me give you a simple example illustrating the benefits of controlling access to implementation details.
In designing the software for building my journal web site, I needed a data structure for storing information about each web page corresponding to a journal entry. This data structure, which I called a record, would include a field (the term used to describe the components of a data structure) called the entry corresponding to the relevant parts of the path in the file system leading to the files for the journal entry, a field called previous corresponding to the path in the file system leading to the files for the chronologically prior journal entry, a field called next corresponding ... well, I expect you can guess, and fields for the date and title of the entry. I needed to be able to create such records, access their fields and modify selected fields.
Here’s how I used lists in Scheme to implement the record data structure. By way of background, the Scheme function listcreates a list whose items are initialized to be the values of the arguments in the invocation of the function. Lists in Scheme use zero-based indexes (the first item in the list has the index 0, the second has the index 1 and so on) and (list-ref record n) returns the nth item (zero-based) in the list corresponding to record. (set-car! (list-tail record n) new-value) assigns the nth item (zero-based) in the list corresponding to record to be new-value. The first function defined creates a new record initializing all the fields with values supplied as arguments. The next five functions access the five fields in the record data structure and return the value of the specified field. The last two expressions are two of the five functions used to modify the fields in the record data structure.
(define (make-record entry previous next date title) (list entry previous next date title)) (define (record-entry record) (list-ref record 0)) (define (record-previous record) (list-ref record 1)) (define (record-next record) (list-ref record 2)) (define (record-date record) (list-ref record 3)) (define (record-title record) (list-ref record 4)) (define (set-record-entry! record new-entry) (set-car! (list-tail record 0) new-entry)) (define (set-record-previous! record new-previous) (set-car! (list-tail record 1) new-previous))
You might notice that the prompt is missing in this code. As programs get larger, it becomes convenient to type them into files and then load them into Scheme so the functions can be used in interacting with the Scheme interpreter. In the program listings in the remainder of this chapter, you can assume that if the prompt isn’t displayed then I’ve put the function definitions in a file and loaded them into Scheme. This method of creating files consisting of definitions and other code fragments is the way programs of any size are usually developed. Indeed, large programs are typically composed of hundreds if not thousands of files.
I can also make the record data structure more useful by defining functions for doing things with records. For example, it’s often handy to have a print function for data structures that displays their fields clearly:
(define (print-record port record) (fprintf port "Record object:~%~ Entry path = ~A~%~ Previous entry path = ~A~%~ Next entry path = ~A~%~ Entry date = ~A~%~ Entry title = ~A~%" (record-entry record) (record-previous record) (record-next record) (record-date record) (record-title record)))
Now I’ll create a new record, modify one of its fields (correcting the mistyped year in the date field), and then print the modified record:
> (define test-record (make-record "/02/08/24/" "/02/08/16/" "/02/08/28/" "August 24, 2001" "Sharing and Reuse")) > (set-record-date! test-record "August 24, 2002") > (print-record (current-output-port) test-record) Record object: Entry path = /02/08/24/ Previous entry path = /02/08/16/ Next entry path = /02/08/28/ Entry date = August 24, 2002 Entry title = Sharing and Reuse
This is all very well, but it uses a lot of code to do something that programmers have to do far too often. Most modern dialects of Lisp have one or more methods of creating data structures. In Scheme, the invocation (define-struct record (entry previous next date title)) does everything my implementation of the record data structure does and more. But my implementation helps illustrate the point I want to make: whatever the merits of this particular implementation, a problem can arise when someone else tries to use my code.
Suppose my friend Sulee Jenerika decides to use my record data structure in her code. Rather than copy my code into her program, she simply puts a require statement in her code that loads the file (or library) that I’ve conveniently provided containing the code that implements my record data structure.
In Sulee’s software project, she needs to search for a record containing a particular entry; having looked at my implementation of record, she writes two procedures for operating on records. By way of background, the Scheme function assoctakes an object (a string corresponding to the path for an entry in this case) and a list of lists (a list of records in this case) and finds the first list whose first item is equal to the object. The function constakes an object and a list and returns a new list whose first item is the object and whose remaining items are the items in the original list.
(define (record-lookup entry records) (assoc entry records)) (define (record-insert record records) (cons record records))
This works fine until I discover vectors, another primitive data type available in most programming languages and applicable in many of the same circumstances as lists. Let’s suppose that I buy some hacker’s impassioned argument that I’ll get better performance if I implement records using vectors. I do this and then update my record library, expecting everyone using my record data structure to be pleased when their code suddenly runs faster. Here’s what the reimplemented record data structure might look like:
(define (make-record entry previous next date title) (vector entry previous next date title)) (define (record-entry record) (vector-ref record 0)) (define (record-previous record) (vector-ref record 1)) (define (record-next record) (vector-ref record 2)) (define (record-date record) (vector-ref record 3)) (define (record-title record) (vector-ref record 4)) (define (set-record-entry! record new-entry) (vector-set! record 0 new-entry)) (define (set-record-previous! record new-previous) (vector-set! record 1 new-previous))
And so on. My other record functions, for example print-record, work fine as written, but Sulee’s record-lookup and record-insert functions are badly broken: her implementation relies on the details of my initial implementation using lists. If Sulee uses my code in a product and she recompiles her code (including my record library) in the next release of her product, her customers are in for some unpleasant surprises.
Is Sulee at fault for writing her extensions based on my initial implementation, or am I at fault for changing my implementation? Clearly there’s a need for some sort of a contract between library consumers and library producers that stipulates who is responsible for what. Object-oriented programming languages help enforce such a contract by requiring that a library (called a classin object-oriented-programming parlance) producer publish an interfacespecifying exactly how a library consumer is to use a given library, what functions to call and what behaviors to expect. Consumers for their part are expected not to assume anything about a library beyond what is specified in the interface. To enforce this discipline, library producers can make certain parts of a library’s implementation private, thereby making it impossible for a consumer to access those parts or exploit them in any fashion. As the producer of a library, I can modify the implementation as long as I preserve the interface. And as a consumer of a library, I can rely on a stable interface, both in terms of the calling conventions for invoking procedures (the names of the procedures and the number, order and types of their arguments) and their resulting behavior.
What’s good for the goose is good for the gander. A major cause of my software woes over the years has been my tendency to violate contracts with myself. I implement a data structure — typically in a rush — and then exploit its implementation in various, sometimes subtle ways throughout my code. Later, when I realize that I need to improve the implementation to get better performance, I have to find and fix all the places where I exploited the initial implementation — a task both tedious and error prone. I always rationalize my impatience and lack of discipline in coding by saying that it’s just a prototype and I’ll fix it later, but invariably the prototype code persists and causes problems later.
When I learned Java a few years back, I got frustrated working my way through the recommended two texts, ArnoldandGosling98’s authoritative The Java Programming Language for a description of the language and Budd00’s excellent Understanding Object-Oriented Programming with Java for a balanced introduction to object-oriented programming in Java. My problem was that most of the functionality I was being introduced to I didn’t need (or at least didn’t think I needed). Why would I want to publish an interface describing the procedures I intend to write, why would I want to use or extend a class, why would I want to make some parts of my implementation public and other parts private, why would I want inherit some parts of a class and override others? The answers to all these questions have to do with the benefits derived from sharing and reusing in the context of software engineering in the large — the art and science of writing, maintaining and improving large programs.
I’m still not entirely sold, and I expect I’ll always want to implement everything from scratch rather than borrow someone else’s code (my attitude here underscores one of the major obstacles to software reuse, as summed up in one of Alan Perlis82’s epigrams, “It is easier to write an incorrect program than understand a correct one”). But the fact of the matter is, if you’re a mere mortal and want to produce any interesting software, you’ll probably have to use significant amounts of code written by other people and reuse your own code. Consider: all the compilers, editors, debuggers and other tools are written by others, so why not use well-crafted libraries to expedite and improve your own programming projects? Here’s an example illustrating how object-oriented programming facilitates code sharing, again based on my experience building a web site for my online journal.
One of the first things you have to deal with in implementing an online journal involves specifying dates in various calendars. I didn’t think too much about calendars, having resolved early on to simplify my life and use the Gregorian calendar. To get the flavor of the potential confusion arising over dates in various calendars, consider the date August 24, AD 2002 in the Gregorian (Reform) calendar, one of the days on which I wrote a journal entry.
Nowadays, the letters AD/BC (“Anno Domini”/“Before Christ”) are beginning to be replaced by CE/BCE (“Common Era”/“Before Common Era”), and hence our example date is August 24, 2002 CE (the letters now appearing after the year by convention). The year 2002 CE in the Gregorian calendar is 5763 in the Jewish calendar and 1423 in the Islamic calendar. August 24, 2002 CE at 12:00 UT (“Universal Time,” sometimes called “Greenwich Mean Time” and abbreviated GMT) is 2452515 JD (“Julian Date”) in the Julian calendar, in which dates are computed as a continuous count of days and fraction of days since noon UT on January 1, 4713 BCE. It makes your head swim.
But, having finessed this one aspect by fixing on the Gregorian calendar, I still had plenty of room for ambiguity in dealing with dates. The date format most common in the U.S. is mm/dd/yy, which is easily confused with the European date format dd/mm/yy. There are alphanumeric formats such as August 24, 2002 and 24 Aug 2002. And there is a proposed international standard, ISO 8601, for representing dates and times in which the string 2002-08-24T14:10:00-05:00 corresponds to August 24, 2002 at 2:10PM US Eastern Standard Time, where the -05:00 specifies an offset from Coordinated Universal Time (UTC). Calculations involving dates specified in accord with ISO 8601, say the difference in time between two dates, have to deal with time zones, leap years and even leap seconds. Happily, I was only concerned with years, months and days.
Most object-oriented programming languages, including Javaand C++, have libraries, called classes, for calendars, dates, time zones, and the like. A class specifies the mutable internal state, stored in fields, and the immutable procedures, called methods, for all objects or instancesof the class. The Java Date class provides methods to determine if one instance of the class Date comes before or after another and whether two instances are equivalent. Date methods let the user extract from an instance the year, month, day, hour, minute, second and millisecond.
If I were programming in Java, I’d just type “import java.util.Date;” at the top of my Java program and I’d be in business. But we’ve been steeped in Scheme syntax with all its lovely parentheses for most of the last three chapters, so let’s stick with it just a little longer to give you the basic idea. Keep in mind that object-oriented programming is not about syntax, it’s about a particular way of structuring programs and thinking about computation; you can do object-oriented programming in any language.
I could invent my own idiosyncratic syntax for object-oriented programming in Scheme, but there is a much better alternative. The folks at PLT2 provide a very nice library written in Scheme that implements classes and objects. The PLT library, class.ss, mirrors much of the functionality found in Java but uses lots of parentheses instead of curly brackets and semicolons. But just so you won’t feel you’re missing anything, as soon as we’ve explained the basic ideas using a Scheme-like syntax, we’ll take a look at the same program in Java.
First, let’s see what a PLT Scheme date class might look like (I’ll use the capitalized Date for the Java class and the all-lower-case date for the PLT Scheme class). This is a simplified version of what’s provided in the Java Date class. I begin by specifying an interface promising that any class implementing this interface provides methods for getting the year, month and day of an instance of the PLT Scheme date class and a method for checking whether one instance is before another instance:
(define date-interface (interface () year month day before))
The next expression describes the class specifying the various fields of the class (init-fields are fields that are initialized when an instance of the class is created) and defining the methods specified in the indicated interface. The function definitions within a class description implicitly take as an argument an object of the class and can refer to the fields of the class directly. The class date is said to inherit functionality from its superclass, which is the base class (the simplest, most basic class possible, referred to here as object%). The class description indicates that all the methods specified in the interface are public and hence accessible to anyone who imports the class. We’ll ignore the rest of the syntax for our immediate purposes.3
(define date (class* object% (date-interface) (public year month day before) (init-field (this-year 0)) (init-field (this-month 0)) (init-field (this-day 0)) (define (year) this-year) (define (month) this-month) (define (day) this-day) (define (before that) (cond ((> this-year (send that year)) #f) ((< this-year (send that year)) #t) ((> this-month (send that month)) #f) ((< this-month (send that month)) #t) (else (< this-day (send that day))))) (super-instantiate ())))
In the next exchange with Scheme, I create (instantiate is the term for creating an instance of a class) a couple of date objects and then invoke the before method, illustrating the somewhat clunky syntax for invoking methods (as we’ll see, Java has a much nicer syntax for instantiating and calling methods on objects, but then Java was designed from the ground up as an object-oriented programming language). An expression of the form (send object method arguments) invokes the method on specified object and additional arguments (the object being an implicit argument in every method definition). Java uses the convenient dot (“.”) operator, so that this invocation would appear as object.method(arguments); this seems rather elegant to me but perhaps I’ve brainwashed myself by looking at more OOP code than is healthy.
> (define date-one (instantiate date (2002 8 16))) > (define date-two (instantiate date (2002 8 24))) > (send date-one before date-two) #t
In addition to implementing dates there is the messy problem of parsing strings to see if they contain dates and, if so, extracting the dates and converting them into the format needed to create an instance of the Date class. There is also the problem of formatting dates for display purposes. Some of this functionality is handled in the Java DateFormat class. There’s lots more to the story of how dates, calendars, locales, time zones and the like are supported in Java; if you’re interested, check out out ArnoldandGosling98’s book for details.
For purposes of illustration, let’s pretend that someone else implemented the date class and I just want to use it. I’m going to do so by defining a new class, the journal-date class, that relies on or extends the date class. The journal-date class also implements an associated interface, the journal-date-interface, which extends the data-interface. Again, don’t sweat the details, but you might be interested to know that super-instantiate refers to the procedure that instantiates the superclass, the date class in this case. Instances of the journal-date class deal primarily with strings and so I have to convert the strings to numbers for use in instances of the date class. This sort of conversion, often referred to as type coercion in languages like C, C++ and Java that require explicit type declarations, is quite common in programming. Instances of the journal-date class, besides storing the original strings presumably extracted from my text, also reformat the date according to the ISO 8601 standard:
(define journal-date-interface (interface (date-interface) iso)) (define journal-date (class* date (journal-date-interface) (public iso) (init-field (year-string "")) (init-field (month-string "")) (init-field (day-string "")) (define (iso) (format "~A-~A-~A" year-string month-string day-string)) (super-instantiate ((string->number year-string) (string->number month-string) (string->number day-string)))))
And here’s a simple test to make sure everything works as advertised with instances of the journal-date class responding to methods inherited from the date class as well as to those (well, one) implemented in the journal-date class:
> (define journal-date-one (instantiate journal-date ("2002" "08" "16"))) > (define journal-date-two (instantiate journal-date ("2002" "08" "24"))) > (send journal-date-one before journal-date-two) #t > (send journal-date-one iso) "2002-08-24"
Most real classes are much more complicated than my simple examples suggest. If real classes were this easy to write, there wouldn’t be much point in sharing them. A good class is well documented and easy to tailor to the needs of a specific project. It takes effort to understand someone else’s code, but the benefits of well crafted and widely used classes in terms of better performing and more easily shared, maintained and extended software can more than offset the disadvantages. Way better than sliced bread.
As I hinted, I feel a little guilty about all that Lisp code in the last section. I talked a lot about the benefits of object-oriented programming but I showed you hardly a scrap of code from languages specifically designed to support object-oriented programming such as Java or C++. I know it’s just syntax, but I also know from experience that struggling with unfamiliar syntax can be frustrating — hence my reluctance to code in Java or C++ simply because I’m less familiar with the syntax. Trading parentheses for curly brackets and semicolons and different formatting and commenting conventions and different names for often-used functionality is more than a little disorienting, and sometimes important concepts can get lost in translation. I chose the PLT Scheme class.ss library because it captures the most important features of object-oriented programming by mapping object-oriented-programming concepts directly onto appropriate Scheme syntax.
The class.ss library introduces syntax to make coding in Scheme seem like programming in an object-oriented programming language. Since the class.ss library is written in Scheme, it’s obvious that we really didn’t need the syntax of class.ss in order to program in Scheme in an object-oriented style. So why all the extra syntax? Don’t programming languages have enough annoying syntax already?
Syntactic sugaris features added to a language that make it more palatable for humans but don’t affect its ability to specify computations (such features neither increase nor decrease the expressiveness of the language). Syntactic sugar is meant to encourage programmers to write better code by hiding both features that are seldom useful and might distract someone trying to make sense of the code and features that are difficult to use and can lead to bugs. In some languages, (x + y) is syntactic sugar for plus(x,y) — the former notation is more familiar and thus makes a programmer less likely to make mistakes in longer, more complicated formulas.
Contrast syntactic sugar with syntactic salt, which makes it more difficult to write good code, and syntactic saccharin(also called syntactic syrup), which introduces gratuitous syntax serving no useful purpose. Some programming-language mavens believe that syntactic sugar can itself be distracting or that the need for it in a programming language is evidence that the language was poorly designed (“Syntactic sugar causes cancer of the semicolon” is another of Alan Perlis82’s epigrams). And it’s true that a programmer addicted to syntactic sugar is less likely to understand or use the features that the sugar sweetens by masking.
The PLT Scheme implementation of classes does abstract away some important features of Java and C++, specifically the requirement of declaring the types of all variables, constants and return values of functions. But these features are not pertinent to object-oriented programming per se, and individual preferences regarding parentheses versus curly brackets are similarly beside the point. Therefore it’s my belief that any sugaring in the notation introduced in class.ss is of the good sort. However, I’m also sensitive to the criticism that syntactic sugar is more syntax and too much of even a good thing can rot your teeth.
So, let’s take a look at some “real” object-oriented programming syntax. As an exercise, I translated the classes I designed using the class.ss library into Java classes. I was pleased with how easily the translation went and how directly the abstractions in class.ss mapped onto standard Java abstractions. Showing off the code will require some tools from the Sun Java Development Kit (JDK), but we’ve never been reluctant to introduce new tools and inscrutable (and all too briefly explained) notation in the past so I don’t know why we should start now. Also, we’ll be using a somewhat different style of developing and executing programs from what we’re used to in interacting with a Lisp interpreter.
To implement the necessary classes, I created three files: one file for each of the Date and JournalDate classes and one file for a class called TestDate to demonstrate that the code actually works. TestDate has no associated instances and exists only so that I can call its mainprocedure to build some instances of the Date and JournalDate classes and apply methods to these instances to demonstrate that the classes work as advertised.
% ls Date.java JournalDate.java TestDate.java
Here’s what the code for the DateInterface interface and Date class looks like. Again, we’ve kept it simple for illustration; the Java code implements the same behaviors as the Scheme code. In Java Object is the base class. I use the Unix cat program to print the Date.java file to my terminal from the shell. I won’t explain the Java syntax in any detail except to note that the DateInterface describes the interface that the Date class implements and that the Date class defines four methods, year, month, day and before, in addition to explicitly defining the Date instantiator.
% cat Date.java interface DateInterface { public int year (); public int month (); public int day (); public boolean before ( Date that ); } class Date extends Object implements DateInterface { protected int year, month, day; public int year () { return year; } public int month () { return month; } public int day () { return day; } public boolean before (Date that) { if ( year > that.year ) return false ; else if ( year < that.year ) return true ; else if ( month > that.month ) return false ; else if ( month < that.month ) return true ; else return ( day > that.day ) ; } public Date (int y, int m, int d) { year = y; month = m; day = d; } }
Next we have to compile this code using javac, the Java compiler, which converts the Java source code, for example the contents of the Date.java file, into Java bytecodes. Most compilers produce assembly code that is then converted by an assembler into machine code to run on a particular machine. The Java compiler produces bytecodes, which are the machine code for the Java Virtual Machine (JVM), a piece of software that runs on lots of different machines and serves to simulate a virtual or software machine. Java bytecodes run on any machine that has an implementation of the JVM. Let’s invoke the compiler.
% javac Date.java
Now there is a file called Date.class in the directory along with Date.java. Date.class contains the compiled version of the Date class.
Next let’s look at the file implementing the JournalDate.java class. It begins by declaring its dependence on the Date class and then proceeds to extend DateInterface before getting around to describing the JournalDate class. Here, as in the Scheme version, I invoke the instantiator, called super in Java, for the superclass Date.
% cat JournalDate.java import Date; interface JournalDateInterface extends DateInterface { public void iso ( ); } class JournalDate extends Date implements JournalDateInterface { protected String yearString, monthString, dayString ; public void iso () { System.out.println( yearString + "-" + monthString + "-" + dayString ); } public JournalDate (String y, String m, String d) { super( Integer.parseInt( y ), Integer.parseInt( m ), Integer.parseInt( d ) ); yearString = y; monthString = m; dayString = d; } }
We compile the code in JournalDate.java:
% javac JournalDate.java
TestDate.java contains the description of a class, TestDate, whose only reason for being is to run its main method to exercise the code in Date.java and JournalDate.java. It’s not very fancy; it just creates an instance of JournalDate and then invokes the iso method on the instance:
% cat TestDate.java import JournalDate; class TestDate { public static void main(String[] args) { JournalDate today = new JournalDate ( "2002", "08", "28" ); today.iso (); } }
It has to be compiled, just like all the other classes:
% javac TestDate.java
Now we list the contents of the directory and notice compiled versions for all of the classes and interfaces:
% ls Date.class JournalDate.java Date.java JournalDateInterface.class DateInterface.class TestDate.java JournalDate.class TestDate.class
In order to run the program we just compiled, we use the Java interpreter, called simply java:
% java TestDate 2002-08-28
Not very interesting output, but at least the iso method defined in the JournalDate class appears to work as advertised.
As another example of using objects in Java code, say that in defining a procedure I declare and instantiate two JournalDate objects as follows:
JournalDate today = new JournalDate ( "2002", "08", "28" ); JournalDate yesterday = new JournalDate ( "2002", "08", "27" );
Then, later in same procedure, if I were to write today.before(yesterday) and yesterday.before(today), these statements would evaluate to false and true, respectively, thereby exercising the (inherited) methods in the superclass Date of the JournalDate class.
That probably felt like an awful lot of syntax to wade through for something so simple as comparing a few dates. But libraries, classes, interfaces are the basis for building industrial-strength applications. If you’re a company doing anything that has to be dated (contracts, accounts, design documents), then you’d be well advised to use a carefully crafted date library, one that adopts widely accepted standards and can be easily extended to handle new standards as they become available. Industrial-strength applications have to be designed to last, to be easily maintained and upgraded, to scale to handle millions of records or dates, and to be modular so that various pieces can be used for other purposes. Object-oriented programming languages help to achieve these properties, but they can’t guarantee them. Perhaps this is why one of the best known and most widely read series of books on computer science and computer programming is Don Knuth’sThe Art of Computer Programming.
The software engineering lessons in this chapter are rather subtle and easy for an inexperienced or stubborn programmer to ignore. These lessons have nothing to do with Java, C++ or any other object-oriented programming language; they have everything to do with developing engineering discipline and protecting against laziness, impatience and hubris. Larry Wall, the creator of Perl, claims that laziness, impatience and hubris are the three cardinal virtues of a truly great programmer.4 I claim that my statement and Wall’s are perfectly consistent with one another, and if you don’t believe me, then you should seek out your local Zen master for enlightenment.
So that’s about it: inheritance, interfaces, public and private methods; all good stuff from what I hear tell. But remember that a major part of what makes it all work is in your head, the discipline and good sense you bring to designing good code. No amount of fancy syntax, sweet, salty or otherwise, is going to turn you into a good programmer if you don’t have discipline and common sense in good measure. Java is a nicely designed language and might well be my favorite language now had it been the first language I did any serious programming in. But it wasn’t and it isn’t and I’ll probably stick to Scheme for most of my mid-size programming jobs. There have been times in the past, however, and I expect there are times ahead, when Java and C++ are the right tool for the job and I won’t shy away from using them. As I’ve said before, it’s good not to get too wedded to a single idea, if for no other reason than that programming in different languages encourages you to think about computation in new and interesting ways.
1 I love the original definition (actually one of several definitions claimed as the original) of hacker as “someone who makes furniture with an axe”, especially since in a former lifetime I made chairs, tables and large sculptures with an axe, hatchet, maul and chainsaw in an unsuccessful attempt to earn a living in rural Virginia.
2 PLT Scheme is an umbrella name for a family of implementations of the Scheme programming language and PLT is the group of people who produce PLT Scheme. I recommend Felleisenetal01’s How to Design Programs as an excellent introduction to programming in general and programming in Scheme in particular.
3 The expression (cond ...) is an example of a generalized if statement. In an expression of the form (cond (test1 result1) (test2 result2) ... (else resultn)), Scheme evaluates each test in order until the first test returns the Scheme equivalent of true, at which point it evaluates the corresponding result. In Scheme, #t evaluates to logically true and #f evaluates to logically false. If no test returns true, then Scheme evaluates the last result (resultn) corresponding to the else keyword. Exactly one of the results gets evaluated and is returned as the result of the cond statement. The cond statement, like other generalized conditional operators, for instance case and switch statements, is an example of flow-of-control mechanisms that considerably simplify some types of programming.
4 In the online documentation for Perl, you’ll find the following line: “The three principal virtues of a programmer are Laziness, Impatience, and Hubris”. While I don’t necessarily advocate these virtues even in the narrow context of programming, they do provide the impetus to write code to automate tedious tasks, the inclination to produce efficient code and the chutzpah to try something even if everyone tells you it’s impossible. All these tendencies and encouragements have to be tempered in good programmers, however, if they are to produce reliable, maintainable and reusable (sharable) software.