This is a thoughtful book written by two leading researchers and academicians, Turing-award winner Barbara Liskov, whose previous work included the development of, what was for its time, an innovative programming language (CLU) which integrated data and operations into parametrizable clusters, and John Guttag, who at the time was a leading researcher into algebraic specifications of abstract data types.
The authors write carefully about some key issues in the paradigm shift entailed from shifting from purely procedural programming to object-oriented programming, bolstered by use of semi-formal specification methods (that is, specifications are written in English with occasional but light forays into first-order logic) to help provide a disciplined approach to developing more robust programs. For Liskov and Guttag, the key to such development is the correct use of five key abstractions: procedural abstraction, data abstraction, iteration abstraction , type hierarchy and polymorphic abstraction, all of which have some measure of support in Java.
Procedural abstraction consists in first focusing on object methods as mathematical partial functions mapping inputs into outputs. Since they are partial functions, it is important to specify their exact domains and the relationship that holds between outputs and inputs. A requires clause specifies preconditions that must be satisfied by the inputs to a method in order to guarantee that an effects clause specifying a postcondition holds on the corresponding output. In order to develop more robust programs, the authors recommend that these partial functions be totalized by, in general, throwing exceptions when the input lies outside the original function´s domain. In short, the authors meticulously look into the use of exceptions as a defensive programming technique, taking time to present Java´s Throwable class hierarchy distinguishing between unchecked and checked exceptions.
Data abstractions consist of abstract data types, a class invariant is added to the preconditions and postconditions of the class methods and careful attention is paid to showing how the class as a data abstraction representation satisfies the abstract data type specification. A number of important issues on "benevolent" side effects, object mutability, operation (method) categories and locality and modifiability are also dealt with.
Nowadays, the chapter on iteration abstractions while well done, simply does not appear at the same level of importance as the rest of the abstractions. However I vividly recall how this idea struck me as strikingly original at the time of writing -perhaps the change is more a measure of how commonplace and mainstream the idea has become.
Perhaps the lasting key original contribution of the book is in section 7.8 which deals with the meaning of subtypes. While the mechanisms of object-oriented programming languages like Java, allow class methods to be overwritten or added in a subclass in a very liberal fashion, Guttag and Liskov develop the idea of a more restrictive subclass substitution principle based on two rules (the methods rule and the properties rule) that lie at the heart of a more disciplined and safer use of subclassing. This principle has been extensively endorsed by practitioners and academicians alike.
Finally the chapter on polymorphic abstraction does a fine job of showing how Java´s mechanism can implement an interesting subset of this kind of abstraction.
Since the book is based on the use of specifications, it is only fitting that a chapter (chapter nine) should be devoted to what constitute good specifications. The authors clearly explain the most important trade-offs involved between restrictiveness, generality and clarity.
The rest of the book proceeds to show how the previous ideas can be used to advantage in such typical software engineering activities as testing and debugging, requirements analysis and specifications, and design. There are some great insights in some of these chapters; for example I like how Liskov and Guttag link their ideas to cohesion and coupling design principles -though the reader should be warned that their (reasonable) terminology is non-standard- and particularly how they show how initial, requirements-level class models evolve and change during the design process. Two recurring case studies, one on a polynomial abstraction which eventually includes hidden dense and sparse representations, and the other on developing a search engine are worth studying in detail. The reader should be warned that Liskov and Guttag use notations for class and class-dependency diagrams, which have been superseded by clearer UML notations.
The book´s final chapter looks at design patterns. As an introduction to the topic, I found the chapter, unfortunately rather confusing, especially in comparison with the classic (and older) Gamma, Helm, Johnson and Vlissides Design Patterns book (1995) and with the much more recent and cheeky Head First Design Patterns(2004) by Eric Freeman, Elisabeth Robson, Bert Bates and Kathy Sierra. However, Liskov and Guttag point out interesting relationships between such design patterns as Adapter, Decorator and Proxy. The application of these patterns to the case studies mentioned above is invaluable.
Curiously, this book contains no references or bibliography. While a case might be made for providing a book on topics such as these with no distracting references, the lack of at least some "further readings" sections is, in my opinion, to be strongly deplored.
In spite of its age, I would strongly urge teachers searching for a book on which to base a course on topics on Software Development Methods as described in the ACM, IEEE Computer Society and AIS 2013 curriculum recommendations, that bridges fundamental algorithms and data structure, and software engineering to consider this book. As in many MIT textbooks, it occasionally gets ahead of itself and references material which most students will understand only later on (when they learn more about compiling fundamentals, for example), but it definely provides a disciplined framework for reflection that ought to distinguish the professional software engineer or computer scientist from the amateur programmer. I would also recommend this book to practitioners as a healthy reminder of the kind of approach that should not be thrown away in order to deal with the urgency of many real-life development situations.