Neat book - definitely outside my usual tastes as a hobby programmer but well fitting given my current internship at $COMPANY_THAT_USES_MOSTLY_RUBY
.
There’s a lot of fairly standard information about the Liskov Substitution principle, the Single-responsibility principle, and Demeter’s Law.
I hadn’t heard of the last one, so let’s just go through them for the sake of it.
- Liskov Substitution principle
- if a property holds for an object of type T, then that property should hold for an object of type S if S is a subtype of type T
- Single-responsibility principle
- a module should be responsible to exactly one actor
- Demeter’s Law
- a class should talk to only immediately related classes and not use the implementation details of those classes
Some of these are pretty useful things to consider. In general I think they generally ring true. I would recommend people try to think of them at a bit higher level than they are sometimes presented though. There are languages that don’t have first class “objects” but could still benefit from the principles underpinning these ideas.
C, for example, is generally not considered to be an object oriented language. Even given that though, the idea of isolating responsibility is still very useful. Sometimes logic in a class is better abstracted to methods that operate on a struct in order to enable easier future refactoring. Similarly, the Liskov Substitution principle can be applied for things like function pointers - not that it’s necessarily a good idea to use those. :)
This model of programming definitely feels dated by now. A lot of the biggest resources are now a few decades out of their hey-day and OO is now the dominant way of teaching engineers (at least at UW) how to program. (When a programming model is the popular one at a school, that means that it was hot in the industry at least 10 years ago.)
One legitimate concern against it comes from Casey Muratori’s Clean Code Horrible Performance. As pointed out by Mr. Muratori, abstractions are generally not free.
So by violating the first rule of clean code — which is one of its central tenants — we are able to drop from 35 cycles per shape to 24 cycles per shape, impling [sic] that code following that rule number is 1.5x slower than code that doesn’t. To put that in in hardware terms, it would be like taking an iPhone 14 Pro Max and reducing it to an iPhone 11 Pro Max. It’s three or four years of hardware evolution erased because somebody said to use polymorphism instead of switch statements.
I won’t get into caches and the way that hardware is designed to speed up computation, but this formula to show Average Memory Access Time should help.
$$ \textrm{AMAT} = \textrm{HitTime} + \textrm{MissRatio} * \textrm{MissPenalty} $$
As you have multiple levels of cache, you expand the miss penalty into the AMAT of the lower level cache. As a very crude metric, the more “pointer chasing” your algorithm has to do, the greater the miss ratio will be.
Part of this has to do with vtables required to have dynamic dispatch. Mr. Muratori explains much better than I will, but the tl;dr is that having something like this:
class Ancestor {
public:
virtual int foo() = 0;
}
class DescendantOne : public Ancestor {
public:
int foo() override { return 5; }
}
class DescendantTwo : public Ancestor {
public:
int foo() override { return 6; }
}
int foo_checker(Ancestor& a) {
return a.foo();
}
(The silly virtual int foo() = 0;
is a relic of a design decision by Bjarne Stroustrup.)
When we call foo_checker
on something like a vector of Ancestors (mixed between DescendantOne’s and DescendantTwo’s), we cannot possibly know when we compile the code which version of foo
we are referring to. As a result, we use something called a “vtable”, which allows us to check one pointer embedded in the object at runtime to call the specific code that is used by that subclass of the Ancestor. (This is the aforementioned pointer chasing.)
Polymorphism is definitely a powerful concept, but it’s a little shortsighted to use it as just the “default” without considering the potential tradeoff you might make. In many circumstances though, performance is not the most important aspect of software development. Many people will tell you otherwise, but they are likely not being as pragmatic as they should.
Andrew Kelley’s Practical Data Oriented Design has some good ideas in it w.r.t. how modern performant computing is very cache bound.
One thing I should heavily emphasize is that I do not believe that you can absently claim that more performant is more “good”. A great number of people use Python, even knowing that it’s remarkably wasteful compared to writing straight C in terms of the number of cycles required to do simple things.
As a result of having seen this more modern “performance aware computing” media, I found it harder to take what was said in the book as reasonable by default.
Some pieces I found were eyebrow raising. One chapter makes mention that runtime type exceptions (including nil) are significantly reduced by using OO techniques if done right. To me, this is kind of a cop-out.
I get that people always complain about different things and there’s really no way to satisfy an engineer, but this doesn’t really ring true with my limited time at one of the biggest Ruby-based companies in the world. The engineers I’ve worked with are smart people and there are clearly a lot of people who know how to write good OO patterns. That said, a non-negligible amount of our errors are from type issues.
As I’m still an inexperienced engineer (not even legally yet in Ontario), I reserve the right for this to be ideas not founded on experience as much as a small taste of what professional programming is like, but I found this book didn’t really convince me to join the “dark side”.
The testing and sequence diagram chapters were very good though. I think those alone are worth reading the book for.
If you buy into using OO to maintain your code (and you probably should to some extend - even if a limited one), you should absolutely spend time reading this. The ideas, while not gospel in my opinion, are ones from a very smart person who is doing a good job of summarizing a lot of the ideas we spent the first 60 years of modern computing learning how to do. It would be a waste (and just as silly as blindly trusting OO) to completely reject OO’s core ideas.