When I was introduced to Object-Oriented Programming (OOP), the focus was entirely on modeling our world using classes and hierarchies. Inheritance was taught immediately as a main concept. The problem with this is that Alan Kay coined the term OOP, and what we’re doing and teaching has nothing to do with what he envisioned.
I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages… OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things. – Alan Kay
OOP is not about classes, inheritance or even objects per se. The important part is messages.
Living organisms are extremely complex yet built from tiny, relatively simple, building blocks. Trillions of cells make up a human being. Even smaller components make up these cells.
Cells have a half-life, meaning that they will eventually decay and die. Blood cells will live for about 100 days and then die. Other blood cells are created to take their place.
Cells have walls that isolate them from their environment; they communicate with each other through tiny channels.
Like a living organism, the internet is a collection of billions of computers. These computers will die off or disconnect, and the internet doesn’t even notice. Computers are isolated from each other and can only communicate with each other through a narrow protocol. TCP/IP, the protocol that connects all of these computers together, is ~10k lines of code yet lives forever and scales well.
Takeaway 1: Build Small Components
If we’re trying to model a problem space then making a person class makes no sense. A person is a collection of many components and they have many different roles. Classes will get you in trouble. Adding inheritance and hierarchies on top of classes is just trying to fit the world into a model you invented. Chances are you will get the classification wrong and will have to redo it or desperately patch it.
Taking our cue from biology and networks, we should build small components. The components can be objects, but they can also be processes (e.g. Elixir) or modules and functions. Individual components should do only one thing.
Takeaway 2: Compose Components Using Messages
Components should be isolated from each other. They shouldn’t know anything about the internals of others. Components should expose a small public interface to the world to say “here’s what I can do for you.” For example:
LineGraph respond to just one message, and we don’t know anything else about them. They each do one thing.
If we have to clean the data, then we can pass it through
|> is a pipe operator – we’re just moving data to the right.
Let’s say that pie graphs would show this data better – then we can switch one component with
PieGraph. If we need to render this information in another way, then we can change that last part as long as it responds to the
One mistake that we made above is using explicit dependencies. In the example above, our code relies on knowing
LineGraph to do its job. We can instead “inject” these dependencies:
Now this component composes three parts without explicitly knowing about them. We can reuse this to build several different types of graphs or representations.
For more complex composition pub-sub might be useful. For example, we are selling books online, and after payment goes through we need to do several things. We need to send an order confirmation email, start preparing the book file, sign up the user to the book’s forums, etc.
All of these operations can subscribe to the order component. After payment goes through, the order component will publish/notify all subscribers. All of the subscribers will respond appropriately and do their unique operation.
Avdi Grimm has an example of using the observer pattern in a Ruby on Rails application.
Half-life and Testing
Like biological cells, we can replace these small components of code without breaking the system. We can more easily reason about them, and so we can refactor with confidence.
Components that have few dependencies and a small interface are easy to test. We can easily decouple them from slow operations like the database or HTTP and write fast tests that focus on their behavior.
Bear in mind that while the isolation tests check that each component works, we need to do end-to-end testing to prove that it all works together.
Inheritance is not evil
Inheritance is sometimes useful, but we have to be careful how we use it. Prefer composition unless you’re sure you have a good use case for hierarchies. When you use inheritance remember to keep it shallow (don’t subclass a subclass). Subclasses should inherit everything from their parent.
Object-Oriented Programming is essentially one simple idea: build small things and compose them with messages. We can do this in any programming language. There is no reason to complicate matters.