19 March 2016
Inheritance as implemented in most object-oriented programming languages (Ruby, Java, Python, etc) is terrible. We need to stop using it.
Imagine you're implementing software for an online store that sells books and movies. You're adding functionality to allow users to add these items to their shopping cart. In particular, you're writing the function that increases the user's total purchase by the price of the item. What do you do?
The inheritance-oriented answer is something like this:
class Item def add_to_cart(cart) cart.increase_total(this.price) class Book extends Item this.price = $10 class Movie extends Item this.price = $15
On the surface, this looks reasonable. The code to add items to the cart is shared between
Movie and nothing looks super complicated. You set your keyboard down and go to lunch.
Later, however, the business needs to start tracking the royalty paid to the author of a book separately from the rest of the price. For reasons internal to the book class, you really want to do something like:
class Book extends Item this.royalty = $2 this.markup = $8
This obviously breaks the contact
Book has with
Item, though, so you need to revise your code. You end up with something like this:
class Item def add_to_cart(cart) cart.increase_total(get_price) def get_price raise NotImplementedError class Book extends Item this.royalty = $2 this.markup = $8 def get_price this.royalty + this.markup class Movie extends Item this.price = $15 def get_price this.price
This works, and it protects
Item from further changes to the way the price of a
Book is calculated. But it kinda sucks.
First of all, notice how a change totally internal to the
Book class forced us to change the way
Movie is implemented. This happened because the superclass,
Item, relied on a particular internal structure in its child classes. We've fixed that problem now, but it's way more code.
Item now operates at multiple levels of abstraction. It specifies the public interface for adding an item to a cart, but also defines the private responsibilities that something must fulfill in order to be added. Why are these descriptions in the same place? Moreover, it's unclear if
get_price is intended for public consumption or if it's supposed to be hidden. In Java we might mark it as
protected, but ideally the intended visibility would be obvious without the need for a modifier.
The better design is to separate the description of how to add an item to a cart from the description of what it means to have a price:
interface Priceable get_price def add_to_cart(cart, priceable) cart.increase_total(priceable.get_price) class Book implements Priceable this.royalty = $2 this.markup = $8 def get_price this.royalty + this.markup class Movie implements Priceable this.price = $15 def get_price this.price
In this design, everything operates at a consistent level of abstraction. The visibility of methods is obvious from the structure of the program rather than requiring declarations. The amount of work required to get a new class to work with
add_to_cart is minimal.
For a long time I was the token Go apologist at my job. I've since mellowed in my enthusiasm for Go, but one thing it absolutely gets right is the lack of emphasis on inheritance in favor of interfaces. Rust has done the same thing. I hope this trend continues; I'm disappointed to see that Scala has largely preserved Java's inheritance model.
A few people have pointed out that Scala has traits, which do exactly what I want. This is true and awesome! However, Scala still has an
extends keyword which functions more or less the same as in Java. I'd have preferred to see it removed.
This post has generated a lot of discussion on Hacker News if you're interested in reading more.comments powered by Disqus