Here's mostly from what I wrote down after reading it. Indeed, the "reasonableness" is part of the problem.
What's agreeable is mostly only so because it's such a straightforward platitude. "Things that are not important should be hidden, and the more of them the better. But when something is important, it must be exposed." Ok? Anyone want to argue to the contrary? This is not teaching or learning anything new or of value, it's not even inviting argument like CC makes it easy to do. I was also hoping that with the book being so short it would be concise, but alas, it's full of this sort of stuff. The single page summary of design principles at the end is similar. A few of them you could quibble about, but arguments would likely just be in fully understanding the meaning of the terminology and what background contexts are assumed. Much advice is dependent on context! Context is something not really called out much in this book. As one example there was only a very slight hint that the author is aware that writing for the code reader means a reader from a particular audience, often your co-workers, and that gives you certain affordances you wouldn't have for say random blogger.
Elsewhere, not in the book, the author once wrote "The strong typing of object-oriented languages encourages narrowly defined packages that are hard to reuse. Each package requires objects of a specific type; if two packages are to work together, conversion code must be written to translate between the types required by the packages." This is actually a nuanced point and is good to discuss. The context of whether you have static types or dynamic types or a half-baked OOP system or a full-baked OOP system is very important context. But it seems a completely absent point of consideration from his "philosophy", even when you'd think it'd be appropriate to go over in the final chapter where he highlights OOP as a "trend".
A lot of the author's rants seem to be snipes at Java. Fine, whatever, though Java has answers to the complaints. (Especially modern Java.)
Lastly, and originally my first complaint because it's about the very beginning of the book (including the cover art), he's on shaky foundations with its definition of simplicity/complexity by conflating it with the subjective easy/hard. I was hoping for a post-Hickey (of Clojure/"Simple Made Easy" talk fame for anyone unaware) understanding that complexity is objective, but alas. It's not like Hickey invented that understanding, but in current year, I think it's quite questionable to disagree. So, the book: "For the purposes of this book ... complexity is anything related to the structure of a software system that makes it hard to understand and modify the system." Sorry, that's not a useful definition of complexity, and now the whole book is harder to read/easier to misinterpret because of the custom definition. Well, at least it's explicit that it's custom.
He could have chosen a different word besides "complexity" but I don't really think it would have affected much. The subjective easy/hard is specifically what Ousterhout is trying to talk about.
Ultimately the problem he is (and all of us are) facing is that "good software design" can't really be measured with the right linting ruleset or static analysis. So if you're trying to break the concepts down each level, while still maintaining a scope that should include all software, that probably means it's impossible not to come off as squishy and non-specific at several points. I still think he strikes a really good balance in general here.
I agree that there could be more discussion around context and audience. Ousterhout says "if you write a piece of code and it seems simple to you, but other people think it is complex, then it is complex", but then what can possibly be done if everyone on my team was replaced with new hires who had next to no experience writing code? Did the same codebase go from simple to complex?
If you dismiss all the parts of APOSD that you agree with as straightforward and trivial, then obviously the only parts left for consideration are the parts you disagree with. APOSD is not an academic paper, it does not claim to be wholly and truly original. You are presumably an expert programmer, so it makes complete sense that much of the content discussed in APOSD appears to be "straightforward platitudes". To you, the content is trivial and obvious. But to the new grad with one year of work experience, the content is novel and informative. Perhaps you should take your own advice and consider the context.
I don't think it's fair to dismiss criticism because of skill. I said I think it's a bad book, and my most disliked, but it's not worthless, and if it's all someone has, they can indeed learn things from it even if they won't learn very much per page. However, there are many other books available, and by my own opinion all of them that I've read are superior. Any value you'd get from APOSD, you'd get from any book aimed at or inclusive of a similar audience, and the other book would give even more value that's absent from APOSD. (As another example, I was introduced to The Pragmatic Programmer in college. I believe it can serve the role of APOSD just fine but I never liked it enough to finish it, so perhaps I'd rank it lower if I did.) I also think you'd get most of the value just by writing more programs.
Anyway, the favored book I did highlight, The Practice of Programming, shares some things with APOSD: it's also not academic, is also quite short (maybe 70 pages longer), and is also more productively read earlier in one's career or study but it's still appreciable by those with more experience. You'll learn things about design. But it has so much more than APOSD: you'll learn things about implementation and debugging and considerations for libraries for yourself or others rather than just applications, and so much more in so few pages; just lots of things central to writing programs, which is the fundamental task at the end of the day, more so than just "designing" things.
I guess another complaint is that APOSD just doesn't have enough code in it. And perhaps an implicit philosophy I have is that you can't actually master good design without writing good code. Learning from the feet of masters is a good way to learn, but they actually have to teach by example. To that end, The Practice of Programming has many programs as examples (like a markov chain text generator, written in multiple languages with performance and effort-of-writing comparisons) and invites the reader to do many various exercises (like commenting on comments, or rewriting part of an example to use a different implementation decision and compare the different approaches).
When that book happens to make a claim I agree with, I don't tend to also just dismiss it as a platitude, because it's better argued and reasoned (or argued and reasoned at all), and supported and contains even more information to consider. Let's expand the bit I quoted about interfaces from APOSD, it's actually from the section on exceptions.
"Defining away exceptions, or masking them inside a module, only makes sense if the exception information isn't needed outside the module. ... However, it is possible to take this idea too far. In a module for network communication, a student team masked all network exceptions: if a network error occurred, the
module caught it, discarded it, and continued as if there were no problem. This meant that applications using the module had no way to find out if messages were lost or a peer server failed; without this information, it was impossible to build robust applications. In this case, it is essential for the module to expose the exceptions, even though they add complexity to the module's interface. With exceptions, as with many other areas in software design, you must determine what is important and what is not important. Things that are not important should be hidden, and the more of them the better. But when something is important, it must be
exposed (Chapter 21 will discuss this topic in more detail)."
I find the student example here pretty weak, but it'd be stronger if the actual code was shown and developed, especially if done in a context where it's understandable how the students might have thought it was a good idea at first, rather than just making an obvious mistake because they're students. Chapter 21 does discuss things in more detail, but not much more, and again there are no code examples much beyond pointing back to a prior chapter's dozen lines of strawman Java. It starts off with:
"One of the most important elements of good software design is separating what matters from what doesn't matter. Structure software systems around the things that matter. For the things that don't matter as much, try to minimize their impact on the rest of the system. Things that matter should be emphasized and made more obvious; things that don't matter should be hidden as much as possible."
Does that not read to you as terribly verbose and information sparse? Capable of eliciting a "duuuuuh" even from a beginner programmer? Almost tautological even? The rest of the chapter is similar and doesn't actually give much more information at all. Sure there are a few tidbits of use in there, like the idea of "leverage" and what that means as an approach, and a throw-away line that deserved more elaboration about shallow classes needlessly increasing what seems "important". (Yegge's "Execution in the Kingdom of Nouns" post is a good expansion of that and other things, if it's at all helpful to understand examples of what I find valuable in comparison to this book.)
Let's compare now some similar bits from The Practice of Programming. This comes as a partial summary after a worked section on designing an interface for parsing CSV files in C and C++ with many design decisions detailed and discussed.
"Good interfaces follow a set of principles. These are not independent or even
consistent, but they help us describe what happens across the boundary between two
pieces of software.
*Hide implementation details.* The implementation behind the interface should be hidden from the rest of the program so it can be changed without affecting or breaking anything. There are several terms for this kind of organizing principle; information
hiding, encapsulation, abstraction, modularization, and the like all refer to related
ideas. An interface should hide details of the implementation that are irrelevant to the
client (user) of the interface. Details that are invisible can be changed without affecting the client, perhaps to extend the interface, make it more efficient, or even replace
its implementation altogether.
The basic libraries of most programming languages provide familiar examples,
though not always especially well-designed ones. The C standard I/O library is
among the best known: a couple of dozen functions that open, close, read, write, and
otherwise manipulate files. The implementation of file I/O is hidden behind a data
type FILE*, whose properties one might be able to see (because they are often spelled
out in <stdio.h>) but should not exploit."
If you squint, kind of says much the same thing, right? But it's richer, includes whys, and points to a real-life example, not a student project. It also criticizes the C I/O library right after because of its exposure of publicly visible data.
More on the topic of exceptions, the book takes a rather classic approach that I don't fully endorse ("Use exceptions only for exceptional situations"), but one unique bit is a more thorough treatment of handling errors without having to alter control flow, and why that might be important. In the markov generator program, one worry is that there might not be enough input to start the algorithm. One could exit prematurely (with a special value or an exception) but the book chooses instead to do some padding to ensure the problem goes away. Emphasis mine:
"Adding a few NONWORDs to the ends of the data simplifies the main processing
loops of the program significantly; it is an example of the technique of adding sentinel
values to mark boundaries.
As a rule, try to handle irregularities and exceptions and special cases in data.
Code is harder to get right so the control flow should be as simple and regular as possible."
You don't have to take this rule as given, you immediately see it in action, and an exercise later invites you to re-implement without a sentinel value to compare.
APOSD has an entire chapter on errors, but this idea is only barely hinted at in the whole chapter on errors with the idea of defining errors out of existence (it uses a more controversial example, I think, from TCL) and this bit that clarifies that by "exception" he doesn't necessarily mean a stack-unwinding thing: "However, exceptions can occur even without using a formal exception reporting mechanism, such as when a method returns a special value indicating that it didn't complete its normal behavior. All of these forms of exceptions contribute to complexity."
It's just such a shallow treatment, and I think that last bit is more focused on the other basic idea that Practice of Programming spells out:
"Exceptions should not be used for handling
expected return values. Reading from a file will eventually produce an end of file;
this should be handled with a return value, not by an exception."
That's followed by a code example showcasing said behavior that doubles as a less-strawman swipe at classical Java. (The Java code loops in.read() until it's -1, and has separate exception handlers for a file not found exception, which the book thinks isn't all that exceptional, and a generic IOException.) But to APOSD, it doesn't seem to matter, they all just contribute to complexity. Maybe they contribute to different degrees? (This would require an objective definition of complexity that lets you count the twists, though.) Maybe leveraging the type system (if you have such a language) to define away errors should be mentioned? Maybe (though this one is truly a rhetorical fever dream wish) acknowledgement of Common Lisp's condition system as yet another powerful alternative should be given?
You can count the things, and count the twists. When a set of things has fewer twists (or even knots) than another set of things, it's simpler. When you pull on something, if it's attached to other things by twists, you are dealing with complexity. When you intentionally entwine things, you are creating complexity. You might say you are "complecting" things together, and once done they are "complected" together.
This is relevant from the smallest details of programming like state (being a more complex twist of value and time, compared to simpler immutable values that are timeless) to the largest issues of modularity (being a property of systems composed of smaller things; when you can disconnect such things without needing to untwist them from each other, you have achieved a simpler design).
This is separate from being easy or hard, though one could assert that a simpler system will tend to be easier to change, because you don't necessarily have to deal with as many things twisted together at the same time. But this isn't a given, because we programmers learn and get better at complex things such that they can feel quite easy, and we also love making tools to try and wrangle sources of complexity, either those inherent to a problem domain, or those we unnecessarily inflict on ourselves, and it can be quite easy to make changes to really complex systems once you've learned some of these tools. Complex things can also be very helpful from time to time, especially when they claim to solve a problem and you just want the problem solved yesterday without caring so much how. But regardless, whether something is simple or complex is a property that remains the same no matter who looks at it. Under APOSD's definition, something basic like immutable collections in a program would make it harder to understand because most people aren't taught about them as part of basic education, and many languages don't offer them as part of the standard library. They're unfamiliar, essentially. Even when you do get used to them, they can still be a bit difficult to work with depending on what you're trying to do. But are immutable collections more complex than mutable ones? No.
I don't see that that is markedly different in function from the definition provided by Ousterhout, or at least both seem to describe to me the same concept, just using different words/terms/analogies.
>For the purposes of this book ... complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.
>Complexity: things twisted together. You can count the things, and count the twists.
Presumably twisting makes things harder to understand and having more things requires a greater effort at understanding?
Not seeing that mutable vs. immutable plays into the APoSD definition --- if a system was suited to being represented by immutable collections and if the structure of the software system was designed to make use of immutable collections in its representation that would not make it harder to understand or to modify.
> Presumably twisting makes things harder to understand and having more things requires a greater effort at understanding?
This is exactly the presumption that is wrong. Sometimes it's right, but often it's not. Programmers are addicted to complexity in part because in many circumstances producing more of it is so easy and convenient, especially right now -- it may make things more difficult in the long run, but not always and anyway not everything has to suffer from the tradeoff of long-run considerations. (e.g. many video games are still ship-and-move-on.)
And yes, mutable vs. immutable doesn't fit nicely with the custom APOSD definition either. Immutable is strictly simpler because it no longer twists together the value with the current time of the program. It's just a value. Another example would be (non-Common Lisp) classes: a (non-Common Lisp) class twists together state (values+time) with behavior (methods) and typically also namespaces and a data type. The alternatives you can use for simpler designs are immutable values, pure functions, and explicit first-class namespaces. It might not be easier, especially at first if you haven't gotten practice using such simple tools together in a non-twisty way, or if you design your program in such an obtuse way or the domain is so inherently stateful that the tradeoffs for the simpler approaches lead to unacceptable effects (try writing a game with no compromises on a pure functional style, it's not easy!). But there are still benefits. The more honest definition means that simplicity isn't an unalloyed good that always leads to more ease, but is just another (important) element to consider in the various tradeoffs programmers have to make.
I am unfamiliar with your usage of "twist" in the sense you seem to be using as it relates to complexity --- the APoSD definition seemed far easier for me to understand at least.
Thank you for taking the time to discuss this --- looking forward to reading the Google book you recommended --- hopefully it will come up as a point of discussion here at some point in the future.
What's agreeable is mostly only so because it's such a straightforward platitude. "Things that are not important should be hidden, and the more of them the better. But when something is important, it must be exposed." Ok? Anyone want to argue to the contrary? This is not teaching or learning anything new or of value, it's not even inviting argument like CC makes it easy to do. I was also hoping that with the book being so short it would be concise, but alas, it's full of this sort of stuff. The single page summary of design principles at the end is similar. A few of them you could quibble about, but arguments would likely just be in fully understanding the meaning of the terminology and what background contexts are assumed. Much advice is dependent on context! Context is something not really called out much in this book. As one example there was only a very slight hint that the author is aware that writing for the code reader means a reader from a particular audience, often your co-workers, and that gives you certain affordances you wouldn't have for say random blogger.
Elsewhere, not in the book, the author once wrote "The strong typing of object-oriented languages encourages narrowly defined packages that are hard to reuse. Each package requires objects of a specific type; if two packages are to work together, conversion code must be written to translate between the types required by the packages." This is actually a nuanced point and is good to discuss. The context of whether you have static types or dynamic types or a half-baked OOP system or a full-baked OOP system is very important context. But it seems a completely absent point of consideration from his "philosophy", even when you'd think it'd be appropriate to go over in the final chapter where he highlights OOP as a "trend".
A lot of the author's rants seem to be snipes at Java. Fine, whatever, though Java has answers to the complaints. (Especially modern Java.)
Lastly, and originally my first complaint because it's about the very beginning of the book (including the cover art), he's on shaky foundations with its definition of simplicity/complexity by conflating it with the subjective easy/hard. I was hoping for a post-Hickey (of Clojure/"Simple Made Easy" talk fame for anyone unaware) understanding that complexity is objective, but alas. It's not like Hickey invented that understanding, but in current year, I think it's quite questionable to disagree. So, the book: "For the purposes of this book ... complexity is anything related to the structure of a software system that makes it hard to understand and modify the system." Sorry, that's not a useful definition of complexity, and now the whole book is harder to read/easier to misinterpret because of the custom definition. Well, at least it's explicit that it's custom.