Readers of OOP the easy way will be familiar with the distinction between object-oriented programming and procedural programming. You will have read, in that book, about how what we claim is OOP in the sentence “OOP has failed” is actually procedural programming: imperative code that you could write in Pascal or C, with the word “class” used to introduce modularity.
Here’s an example of procedural-masquerading-as-OOP, from Robert C. Martin’s blog post FP vs. OO List Processing:
void updateHits(World world){
nextShot:
for (shot : world.shots) {
for (klingon : world.klingons) {
if (distance(shot, klingon) <= type.proximity) {
world.shots.remove(shot);
world.explosions.add(new Explosion(shot));
klingon.hits.add(new Hit(shot));
break nextShot;
}
}
}
}
The first clue that this is a procedure, not a method, is that it isn’t attached to an object. The first change on the road to object-orientation is to make this a method. Its parameter is an instance of World, so maybe it wants to live there.
public class World {
//...
public void updateHits(){
nextShot:
for (Shot shot : this.shots) {
for (Klingon klingon : this.klingons) {
if (distance(shot, klingon) <= type.getProximity()) {
this.shots.remove(shot);
this.explosions.add(new Explosion(shot));
klingon.hits.add(new Hit(shot));
break nextShot;
}
}
}
}
}
The next non-object-oriented feature is this free distance
procedure floating about in the global namespace. Let’s give the Shot
the responsibility of knowing how its proximity fuze works, and the World
the knowledge of where the Klingons
are.
public class World {
//...
private Set<Klingon> klingonsWithin(Region influence) {
//...
}
public void updateHits(){
for (Shot shot : this.shots) {
for (Klingon klingon : this.klingonsWithin(shot.getProximity())) {
this.shots.remove(shot);
this.explosions.add(new Explosion(shot));
klingon.hits.add(new Hit(shot));
}
}
}
}
Cool, we’ve got rid of that spaghetti code label (“That’s the first time I’ve ever been tempted to use one of those” says Martin). Incidentally, we’ve also turned “loop over all shots and all Klingons” to “loop over all shots and nearby Klingons”. The World
can maintain an index of the Klingons by location using a k-dimensional tree then searching for nearby Klingons is logarithmic in number of Klingons, not linear.
By the way, was it weird that a Shot
would hit whichever Klingon
we found first near it, then disappear, without damaging other Klingon
s? That’s not how Explosion
s work, I don’t think. As it stands, we now have a related problem: a Shot
will disappear n
times if it hits n Klingon
s. I’ll leave that as it is, carry on tidying up, and make a note to ask someone what should really happen when we’ve discovered the correct abstractions. We may want to make removing a Shot
an idempotent operation, so that we can damage multiple Klingon
s and only end up with a Shot
being removed once.
There’s a Law of Demeter violation, in that the World
knows how a Klingon
copes with being hit. This unreasonably couples the implementations of these two classes, so let’s make it our responsibility to tell the Klingon
that it was hit.
public class World {
//...
private Set<Klingon> klingonsWithin(Region influence) {
//...
}
public void updateHits(){
for (Shot shot : this.shots) {
for (Klingon klingon : this.klingonsWithin(shot.getProximity())) {
this.shots.remove(shot);
this.explosions.add(new Explosion(shot));
klingon.hit(shot);
}
}
}
}
No, better idea! Let’s make the Shot
hit the Klingon
. Also, make the Shot
responsible for knowing whether it disappeared (how many episodes of Star Trek are there where photon torpedoes get stuck in the hull of a ship?), and whether/how it explodes. Now we will be in a position to deal with the question we had earlier, because we can ask it in the domain language: “when a Shot
might hit
multiple Klingon
s, what happens?”. But I have a new question: does a Shot
hit
a Klingon
, or does a Shot
explode
and the Explosion
hit
the Klingon
? I hope this starship has a business analyst among its complement!
We end up with this World
:
public class World {
//...
public void updateHits(){
for (Shot shot : this.shots) {
for (Klingon klingon : this.klingonsWithin(shot.getProximity())) {
shot.hit(klingon);
}
}
}
}
But didn’t I say that the shot
understood the workings of its proximity fuze? Maybe it should search the World
for nearby targets.
public class World {
//...
public void updateHits(){
for (Shot shot : this.shots) {
shot.hitNearbyTargets();
}
}
}
As described in the book, OOP is not about adding the word “class” to procedural code. It’s a different way of working, in which you think about the entities you need to model to solve your problem, and give them agency. Obviously the idea of “clean code” is subjective, so I leave it to you to decide whether the end state of this method is “cleaner” than the initial state. I’m happy with one fewer loop, no conditions, and no Demeter-breaking coupling. But I’m also happy that the “OO” example is now object-oriented. It’s now looking a lot less like enterprise software, and a lot more like Enterprise software.