February 12, 2020
Sit back. This is gonna be a long one.
I read “Object-Oriented Programming — The Trillion Dollar Disaster” recently, and I have some thoughts on it I’d like to share. I’ve written a lot of code over the years in a lot of languages, and while I agree with some of Ilya’s argument, I don’t fully agree that existing OO languages are inherently broken — just their current usage patterns are, and usage patterns, the way we think about using our languages, are something that can be fixed.
(First off, as Ilya rightly points out, if you haven’t learned what Alan Kay’s concept of object-orientation was — the Smalltalk form of it — that’s worth studying. Likewise, Erlang is mentioned in the article several times as well, and Erlang is a very interesting language in its own right, with a number of unique ideas attached to it. If you haven’t spent any time looking at those, go spend some time looking at those, maybe even buy some books on them, and learn why they think the way they do.)
Now, personally, I don’t believe the notion of “object oriented” is inherently bad. But… OO as it’s currently practiced is a mess that often leads to awful things like IAbstractProxyBridgeFactoryAdapterProviders
, and any time you find yourself reaching for the Design Patterns book or writing a class whose name is some generalized verb ending in “er”, you’re probably already in over your head and should ask for help. The author of the article is right that there are a lot of pitfalls in OO code, and he’s right that current thought-leadership in the C# and Java communities isn’t helping the matter.
So how do you steer clear of the worst pitfalls of OO? Here are four of my better recommendations, which I follow in my own code:
Avoid Inheritance
(This is all about fixing a major encapsulation problem in mainstream languages.)
Inheritance is a strong coupling between two shapes. Yes, there are times when it helps with code reuse, but it’s such a strong coupling between classes that it’s almost impossible to refactor later on: If B
and C
inherit from A
, you actually have to understand all of A
and B
and C
in order to make changes to any of the three of them. A small change to C
could actually end up affecting B
as well! It’s literally the exact opposite of encapsulation, and I think we can all agree that encapsulation is a good thing.
I tend to think of inheritance like goto
: Use it if it’s a major shortcut to solving a problem, but otherwise, the T-rex will eat you.
What are the alternatives? Containment, usually: Instead of X extends Y
, just make an instance of Y
that is private to X
, or just make Y
use an existing X
. Sometimes, I’ll even just copy the code: If classes B
and C
don’t share code, they can be modified independently. That doesn’t scale well, but it’s fine when it’s small.
// Bad, because it tightly couples `Child` and `Base`: A change // to `Base` necessarily *is* a change to `Child` as well. public class Base { ... } public class Child : Base { ... } ...versus... // Better, because many kinds of changes can be made to `A` and // `B` without the other class knowing. public class A { ... } public class B { private readonly A _a = new A(); }
Interestingly, this means that every time you see a keyword like virtual
or abstract
or override
, you’re likely looking at a code smell: It’s not necessarily evil, but if you see it a lot, you probably have a serious antipattern on your hands.
Immutability Everywhere
(This is all about fixing the problem of shared mutable state.)
This tends to break the traditional notion of “object orientation,” in that an object that can’t be modified isn’t really an object in the classic sense. But who cares about whether we precisely follow a forty-year-old design philosophy that’s showing its age? I care about reliability, and if you can’t change my object, you can’t break it either.
On top of that, we use immutable shapes all the time: string
is immutable, for example, and nobody gets all bent-out-of-shape over it: The fact that a + b
makes a new string instead of modifying a
in place doesn’t bother anyone.
Really, the big objection to immutability isn’t that you can’t do it: It’s that current languages make it “mildly difficult.” Languages like F# and Haskell and friends have built-in mechanics to make duplicating structs easy, but C# and Java don’t. But just because it’s not built-in doesn’t mean you can’t do it. If all your fields in C# are readonly
, and all of your properties are get
-only (not even a private set
), your objects will be safe. A few simple patterns can be used to handle those scenarios where you “need” to modify things, like this:
// Not this: public MyObject { public AObj A { get; set; } public BObj B { get; set; } public CObj C { get; set; } } obj.A = newA; obj.B = newB; obj.C = newC; // Do this instead: public MyObject { public AObj A { get; } public BObj B { get; } public CObj C { get; } public MyObject(AObj a, BObj b, CObj c) { A = a; B = b; C = c; } public MyObject WithA(AObj newA) => new MyObject(newA, B, C); public MyObject WithB(BObj newB) => new MyObject(A, newB, C); public MyObject WithC(CObj c) => new MyObject(A, B, newC); } obj = obj.WithA(newA).WithB(newB).WithC(newC);
That’s not as pretty as the Haskell version of the same basic code, but it’s just as safe, and you can code it in the C# you’re programming in today. That said, there are times I don’t do that, but just like with inheritance, I opt for immutability first in all my code these days, and I only make things mutable if I have no other easy choice.
(Also, if you’re in C#, there’s a tool to help you implement immutable lists and dictionaries and sets in the form of Microsoft’s lovely System.Collections.Immutable package. It’s very well-implemented, and I can’t say enough positive things about it.)
Functions > Interfaces
(This is all about getting rid of those pesky FactoryProviderInjector patterns.)
Interfaces, just like inheritance, are a form of coupling, not as strong a coupling as inheritance, but still a coupling. When A
and B
both implement IC
, you’re stating upfront certain requirements about their shapes. You can no longer change B
completely independently from A
, and vice-versa, and that means that when you need to change B
, you now need to figure out how to untangle it from IC
, and possibly even from A
.
So what do you do instead to describe common behavior, if you can’t inherit and can’t use interfaces? C# has proper lambda functions: True lexical closures over state. And these are an incredibly powerful tool that isn’t used anywhere near often enough.
(Java has something they call lambdas, but they’re weaksauce anonymous blobs of code that aren’t actually lexical closures like you find in C# and F# and Lisp and Smalltalk and JavaScript and Python and Ruby and… Sorry, Java, but you guys really got this wrong. Java’s flavor is useful in some of the answers I describe below, but not all.)
Let’s explore this one by example: Let’s say you have a SequencePerformer
class that runs a set of methods on unknown objects, in order, like this:
public interface IActor { public bool DoIt(); } public class SequencePerformer { private List<IActor> _actors = new List<IActor>(); public void AddActor(IActor actor) => _actors.Add(actor); public bool RunSequence() { bool succeeded = true; foreach (IActor actor in _actors) { if (!actor.DoIt()) succeeded = false; } return succeeded; } }
This kind of concept is pretty common in OO code: An interface that describes a behavior, and everything interacts with the objects that support that behavior through the interface, so that you can substitute in various different objects as necessary.
So… how might you do all that without an interface then? As it turns out, an interface with one method is almost exactly the same as a delegate, which is just a C# named version of an anonymous function! So let’s use anonymous functions instead:
public class SequencePerformer { private List<Func<bool>> _actors = new List<Func<bool>>(); public void AddActor(Func<bool> actor) => _actors.Add(actor); public bool RunSequence() { bool succeeded = true; foreach (Func<bool> actor in _actors) { if (!actor()) succeeded = false; } return succeeded; } }
Notice something interesting here: The shape of the code is exactly the same. But now there’s no longer an interface coupling everything together: Any method that takes void
and returns bool
can be passed to AddActor()
, not just those objects that happen to have a bool DoIt()
method. We took what was previously a weak coupling and made it completely decoupled: Consumers of SequencePerformer
don’t need to support any prerequisites to use it.
(For the record, SequencePerformer
is an awful class: First, because the whole entire class can be replaced with a single invocation of a Linq method — actors.Select(a => a())
or actors.Aggregate(true, (s, a) => s & a())
. And secondly, it’s bad because it has an ugly name that includes one of the generic-verb +er
names that shows up all-too-often in “enterprise” OO code, like Producer
or Provider
or Generator
or Accessor
. Don’t make generic-verb +er
classnames: These are always a code smell, and a good indication your architecture is already borked.)
A better way to think of an interface is that it’s really just a way to “bag up” methods of certain signatures together, but it also enforces a naming convention too. Functions by themselves don’t enforce naming, which is a good thing:
public interface IFoo { public void Bar(string a, int b); public double Baz(); } public void DoSomething(IFoo foo) { ... } ...versus simply... public void DoSomething(Func<string, int> bar, Func<double> baz) { ... }
In the latter form, the caller can pass in any methods with the right signature, or even just inline lambdas, not just its own instance methods that have been forced to be named Bar
and Baz
. It can create lambdas on the spot, or glue in any methods it has that fits, or even pull in methods from another class, static or instance. The consuming method is completely decoupled from any usages of it: It doesn’t know and doesn’t care what functions it’s running — only that those functions exist.
Is this form always appropriate? No. There are times when there’s value in having an interface connecting things together, and times when there’s value in enforcing certain naming conventions. But if the only goal in your code is to be able to indirectly access a method on one class from another and not couple them tightly, Func<>
(or its sibling Action<>
) are the way to go. I usually start out with Func<>
in my code when I want to very loosely and abstractly connect things, and I only “promote” it to an interface if it turns out an interface is absolutely necessary.
Also, you can even just make the “bagging” of functions a more explicit operation by making a shape to group them:
public class BarAndBaz { public Func<string, int> Bar { get; } public Func<double> Baz { get; } public BarAndBaz(Func<string, int> bar, Func<double> baz) { Bar = bar; Baz = baz; } }
This “BarAndBaz” works a lot like an interface, but it doesn’t enforce naming conventions on the caller’s code: The caller can still cram into this class any methods or functions it wants, but the consuming code still has the nice guarantee that the methods and functions it needs will be available as — and can be passed around as — a set.
Copypasta > Code sharing!
(This is all about knowing when to actually create abstractions.)
This is one of those things that seems on its face to be completely bonkers: We as programmers were taught in school never to copy — always reuse! Don’t reinvent the wheel! Make a shareable wheel and then let everything use it! This concept is so ingrained in people’s brains that they’re often loathe to copy even one line of code. I’ve seen people create a whole class hierarchy rather than copy one line of code in two different places.
But… what if conventional wisdom is wrong?
The starting assumption of “Don’t ever copy” is that the code you have is good, safe, correct, reliable, the right solution for the problem, and fully reusable.
— but when the hell was the last time you saw any code that was all of good, safe, correct, reliable, the right solution for a problem, and fully reusable? I’d venture that you can pick any codebase you want, and 99.9% of it violates at least one of those: Most code everywhere is at least one — if not more — of badly-written, unsafe, buggy, unpredictable, the wrong solution for the problem, or tightly coupled. So that means that of that typical codebase, maybe 0.1% is actually worth sharing. The rest…? Maybe some of it can be fixed, but the vast majority of most codebases shouldn’t be shared.
And remember, sharing is coupling, even if it’s just a single function used in two places, and coupling is bad, because it decreases your ability to change or fix one piece of code without thinking about the other.
I don’t like prefactoring as a concept (even though its book has many other good ideas in it), because you often end up solving a problem that’s not actually a problem. Instead, I follow a “rule of three”:
- The first time, just write the code you actually need. Don’t attempt to “pre-factor it” for reuse. Just make it clean, straightforward, understandable, and correct.
- The second time you need it, either COPY THE CODE, or maybe even write it from scratch. Don’t refactor the first use. Just make the second use clean, straightforward, understandable, and correct.
- The THIRD time you need it, look now at ALL THREE use cases: If all three have a LOT in common, then refactor the first two so they’re sharing the code, and then use that for the third instance. Otherwise, COPY THE CODE for the third instance, because you’re not looking at a “common use case,” but rather three separate use cases that only superficially have anything in common.
- (If the third instance has a little bit in common with the others, extract out just that part that’s common to all three and reuse only it.)
You see, if you “prefactor” it upfront, you’re making educated guesses about what the use cases will be. Sometimes you guess right, but more often, you guess completely wrong: These two need a logger, but that one doesn’t — or this operates on collections, while those two operate on individual items — or that needs a database connection, but these two don’t. And when you guess wrong, you end up saddling all of them with baggage required by the other two.
Don’t try to guess your use cases upfront. You don’t know the requirements until you actually know the requirements. Just write clean, simple, straightforward code, and if it eventually happens to have things in common with other code, refactor it to use code sharing when you know beyond any doubt that it has things in common.
So there you go. Four of my answers for writing better code in C# and Java and other traditional OO languages, two of which are stolen straight from functional programming (FP), and two of which are just based on my own hard-won experience. Switching to pure-functional programming has its own problems and isn’t likely in some environments, but there’s no rule that says we can’t steal some of FP’s best ideas to make our OO code better — if anything, the entire history of programming languages is a slow march toward mainstreaming the best ideas of FP.