Inheritance Is Terrible
lionelbarrow.com
lionelbarrow.com
No, it actually doesn't.
class Item
def add_to_cart(cart)
cart.increase_total(this.price)
This is where the trouble is. It is not the inheritance, it is the add_to_cart method not being in the correct class. If this instead had been class Cart
def add(item)
.. add item to some collection of stuffs implementing price().
def sum()
.. calculate total sum.
then the whole problem evaporates. If your code reads do_something_to_foo(foo), then very often usually it really ought to be foo.do_something()Inheritance can be ok when used sparingly, but this example is not one of those.
Personally I'd make an exception when using extension methods in C#. If you make an extension method which looks (schematically) as follows:
add_to_cart(this item, Cart cart) {
cart.add(item);
return item;
}
You can write code that looks like: item.add_to_cart(cart)
.check_availability()
.do_something_else()
.etc()
Well, this example isn't the best, but you get the general idea.The actual problem of the article was that if the stuff you put in a cart simply derives from a single base class, then any new stuff-based behaviour (potentially) requires changing all subclasses. For example, if you wanted to to add shelf life to perishable items and have the cart maintain the minimum shelf life, then you have to add shelf life to the item class and potentially override it on all subclasses.
An alternative implementation would be to define a Perishable interface with a ShelfLife method and only implement that for perishable items.
class Cart
def minimumShelfLife
items.ofType<Perishable>().minimum(item -> item.shelfLife, ifEmpty: infinity)
This avoids cluttering up the base class with loads of methods which won't be meaningful for most of the types, and which may cause problems when extending things later.I'm not convinced, but I believe that's the essence of the argument the article intends to make.
I'd argue that shelfLife is meaningful for all Items, and it's unnecessarily presumptuous to assume only Carts will want to know about shelfLife. Plus it's less code to simply add a shelfLife method in Item to return infinity. Later, when you realize expirationDate is what you really want to know you'll have less code to modify to get the logic correct.
In most cases you'll have fewer problems doing things the traditional OO way and sending the expirationDate message directly to the item.
Sometimes cluttering the base class is a better solution if there is a meaningful default. For example, if you start with all non-taxable items and the requirements change for taxable items -- it makes sense to implement in the base class as a Tax property that defaults to zero. Then you don't need conditionals and interface checks whenever you want to display or do tax calculations.
The best way that I learned OO was when it was explained that, in most cases, its best to take objects as nouns (ie Item, Book, Prices etc) and the object methods should be verbs (ie do_something() or act_on_this() etc)
2. "Inheritance" itself is a fairly fuzzy concept. Do you mean implementation inheritance (for code reuse), polymorphic subtyping (for polymorphism with late binding), do you account for virtual types, etc.?
Person > Employee > Manager
Shape > Rectangle, Circle
Object > anything
Product > Book, TV
I think these are good examples where inheritance makes lot of sense. There is commonality between things and there is specialization (overrides) or definitions (abstract methods). It's wrong to say that the concept of inheritance is evil. Like pretty much everything, it can be severely abused (very deep hierarchies, multiple on inheritence etc). The classic example of OOP abuse is probably things like MFC library.
Vehicle < Car, Truck, etc.
Person < Employee, Customer, etc.
There's a pretty obvious logic to them that doesn't really demand much extra thought. I can't think of anyone outside of an irreverent ontologist who would suggest going beyond that unless there's a valid reason for it, like say: PhilosophicalBeing < Person < Employee, Customer, etc.I don't think anyone would argue that if you design system, and then change the requirements of what the system should do. You have two choices; build a new system that meets both the old and new requirements or patch the old system to kind of meet the new requirements.
And I get that this critique is looking at the example perhaps more literally than the message, which is that it is hard to make systemic changes to object oriented systems, but that criticism of OOP is well trodden. Bjarne and others will tell you that it is easy to make reliable and testable additions to an OO system and that is its strength. Refactoring such systems in the face of new requirements will always be hard, and yet the argument goes that if you do so correctly you will be able to re-use tests from the previous system to verify it still works the way it did and write new tests to show that it works the new way as well.
The example of books and movies is a good one because shows how initially the thinking that every item has a price, but what should have been considered is that some items are of a type 'media' that has other attributes that regular items don't. If you are familiar with the joke[1], it is the dichotomy between a toaster and a "cooker of breakfast foods".
The fragile base class problem is a very well known one since the 90's.
Already in the 90's we had programming languages like Component Pascal, whose designers pushed for the idea of component based programming. Basically what nowadays is known by using COM interfaces, regular interfaces, traits, protocols, whatever.
The first edition of "Component Software: Beyond Object-Oriented Programming" is from 2002:
Granted, you could have a lot of copy/paste code and NOT use inheritance, but then why use OO in the first place?
EDIT: I mentioned in another comment, and a commenter to this correctly noted- it's probably because it's easier to understand, and a fundamental piece of OO. It's actual usefulness in the real world varies, but it is also how one would step into understanding interfaces, composition, and other aspects of OO that get used heavily in OO in the real world.
type Doer interface {
DoIt()
}
type Foo struct {}
type Bar struct { Foo }
func (*Foo) DoIt () {
fmt.Println("Just do it!")
}
func Incite (d Doer) {
d.DoIt()
}
func main () {
b := &Bar{}
Incite(b)
}
One can transparently invoke Foo's DoIt on instances of Bar or, if Bar had its own DoIt defined, it would override that.Foot-gun wise, of course, if you convert a Bar to a Foo and pass it to something, you no longer get Bar's version of DoIt.
However, anywhere that accepts a Doer accepts a Bar without decomposing it into a Foo, so Incite will invoke Bar's DoIt and, if Bar didn't define a DoIt method, it would still implement Doer via composition of Foo.
One might argue that this is composition not inheritance, but although it's implemented as composition internally, from the outside it appears that Bar isa Doer, even though it doesn't implement DoIt.
Usually, composition would require either forwarding methods which disguise the fact that composition is happening, or for external code to actually access the components directly. Go allows something that looks very much like inheritance.
(I'd also note that, in reality, inheritance is internally implemented as composition. What Go makes explicit is what implicitly happens in C++ or Java anyway.)
>There's an important way in which embedding differs from subclassing. When we embed a type, the methods of that type become methods of the outer type, but when they are invoked the receiver of the method is the inner type, not the outer one. In our example, when the Read method of a bufio.ReadWriter is invoked, it has exactly the same effect as the forwarding method written out above; the receiver is the reader field of the ReadWriter, not the ReadWriter itself.
Fundamentally, invoking a (non-virtual) superclass's method in C++ doesn't do much more than forward that method to an inner struct.
struct Reader {
int fd;
}
struct Writer {
int fd;
void Write (int) {
// *this* points to the Writer, not the ReadWriter
}
}
int writeForMe (Writer *w) {
// *w* points to the Writer, not the ReadWriter
}
struct ReadWriter: Reader, Writer {
}
The only real difference, aside from the syntax, is that in C++ one can dynamically convert a pointer to a Writer that is part of a ReadWriter into a pointer to that ReadWriter, something Go doesn't support.Step 1: settle on technology X because it fits in rather nicely for our requirements.
Step 2: The requirements change a bit. Mostly you can handle it elegantly, but there are some rough edges.
Step 3: More new requirements. Now things are starting to look pretty ugly in places. Evangelist B says "See? I told you technology Y was the right choice! Now look at this mess!"
Step 4: Go with technology Y on the next project.
Step 5: GOTO Step 1
I want to chalk it up to inheritance being an inherently easier thing to think about in real world terms, with a name that has easily-understandable connotations, even if some of those connotations are probably harmful in the way they muddle up via leaky abstraction when thinking computationally. Whereas with mixins/interfaces...those terms have never made immediate sense to me.
And yet when it comes to implementing OOP, mixins/interfaces seem so profoundly more suitable as the go-to strategy that I wish I had learned exclusively about them, with inheritance being left for edge cases. Maybe I just had a bad curriculum or bad memory when it came to the lessons about interfaces. But if there is a stronger focus on inheritance when it comes to teaching OOP, is it due to the design of C++/Java? Or just the practicalities of teaching OOP? Teaching about classes seems like a good first step in teaching OOP, and teaching about inheritance requires fewer new concepts/syntax from defining a class. Whereas mixins/interfaces require introducing the concepts of modules.
There's an analogy here between inheritance -> interfaces and pointers -> object references as in a language like Java. Object references are pointers, in effect, but syntactically pared down so that it's (much) harder to break them. They are less powerful but safer. Similarly, if you have inheritance—particularly the dreaded multiple inheritance—you have everything interfaces can do and then some, but you also have much more responsibility and a lot greater chance of creating bad designs. In both cases, it makes sense when teaching the concepts to teach the fuller, more powerful concept. In day to day work, however, many programmers will make fewer mistakes using the more limited concept.
To say this a bit more clearly, but perhaps more insultingly: inheritance, properly and fully understood, is useful for a massive range of situations, of which mixins and interfaces are a part. I find that relatively few programmers (and I've worked in development for more than twenty years and taught programming at the Master's level for 7: I've taught a lot of programmers) can really grok proper inheritance to this level; therefore when they try to use it they end up shooting themselves in the foot, then blaming the tool. For them it's better to use the more limited tool. But it's still good to know the broader tool.
Java-as-taught is often ancient Java, from before enhancements like default interface methods. The only non-annoying way to inherit behaviour in ancient Java is inheritance. Even modern Java lacks automatic implicit composition ala Go.
Java also encourages large interfaces with many methods. Without any ability to inherit behaviour for those methods, it's often less tiresome to inherit from a class instead. Go, by way of contrast, encourages tiny interfaces with, ideally, one or two methods.
In this example, we might suppose that a quintessential "vintage" Java-esque design wouldn't have a single-method "Priceable" interface. It'd have a CartItem interface with hundreds of methods, all of which you'd have to implement - even if only to stub out with exceptions - in order to implement it. So you wouldn't actually use an interface, or else you wouldn't be avoiding all that typing.
An interface isn't simply a class whose methods are all abstract. It is a distinct kind of type which has no precise analogue in C++ because the language constraints which enable it to exist don't apply in C++. Namely, C++ has multiple inheritance.
To abuse a car analogy, this situation is akin to asking why older drivers weren't taught to use cruise control, and the answer being that many older cars don't have cruise control. Whilst it's possible to "emulate" cruise control by simply maintaining constant speed manually, learning to do that isn't the same as learning how to use cruise control.
From [1]:
"Bjarne Stroustrup: I had a lot of problems explaining that to people and never quite understood why it was hard to understand. From the first days of C++, there were classes with data and classes without data. The emphasis in the old days was building up from a root with stuff in it, but there were always abstract base classes. In the mid to late eighties, they were commonly called ABCs (Abstract Base Classes): classes that consisted only of virtual functions. In 1987, I supported pure interfaces directly in C++ by saying a class is abstract if it has a pure virtual function, which is a function that must be overridden. Since then I have consistently pointed out that one of the major ways of writing classes in C++ is without any state, that is, just an interface.
From a C++ view there's no difference between an abstract class and an interface. Sometimes we use the phrase "pure abstract class," meaning a class that exclusively has pure virtual functions (and no data). It is the most common kind of abstract class. When I tried to explain this I found I couldn't effectively get the idea across until I introduced direct language support in the form of pure virtual functions. Since people could put data in the base classes, they sort of felt obliged to do so. People built the classic brittle base classes and got the classic brittle base class problems, and I couldn't understand why people were doing it. When I tried to teach the idea with abstract base classes directly supported in C++, I had more luck, but many people still didn't get it. I think it was a major failure in education on my part. I didn't imagine the problem well. That actually matches some of the early failures of the Simula community to get crucial new ideas across. Some new ideas are hard to get across, and part of the problem is a lot of people don't want to learn something genuinly new. They think they know the answer. And once we think we know the answer, it's very hard to learn something new. Abstract classes were described, with several examples, in The C++ Programming Language, Second Edition, in 1991, but unfortunately not used systematically throughout the book. "
Another use like Priceable and CartItem in modern Java is Connection and AutoCloseable (database connection objects). Anything AutoCloseble can be automatically closed after a try/catch block is done (with some specifics I glossed over on how). And a Connection is that, some specific form of DB connection. You also have a DB ResultSet that is AutoCloseable. So by having another interface extending another, or a class implementing the interfaces you can drive very related functionality on classes that may do very different things.
CartItem could extend/implement the one or two method Priceable, along with all its other methods.
Isn't a class with exclusively pure virtual methods effectively an interface?
Meyer's OOSC doesn't. It explains inheritance 1/3 through the book, after generics (parametric polymorphism) and Design by Contract. Incidentally, OOSC has this paragraph about inheritance that is worth remembering:
"Neither the Open-Closed principle nor redefinition in inheritance is a way to address design flaws, let alone bugs. If there is something wrong with a module, you should fix it — not leave the original as it is and try to correct the problem in a derived module. (The only potential exception to this rule is the case of flawed software which you are not at liberty to modify.) The Open-Closed principle and associated techniques are intended for the adaptation of healthy modules: modules that, although they may not suffice for some new uses, meet their own well-defined requirements, to the satisfaction of their own clients."
(Emphasis in the original.)
The Item class is not an example of a healthy module. It has been written in an ad-hoc fashion, without concerns about its requirements or thoughts about what contracts it should offer to its clients (i.e. how it interacts with other modules). Unsurprisingly, adapting it through inheritance results in breakage.
I think it's because people just starting out are able to grasp the concept of a duck is a bird is an animal AND you can reduce the code you write by saying all animals walk, birds fly, and ducks quack. So you are building on from the parent class.
With interfaces you are saying all implementations of animals walking can be different, so you're going to have to write code for each one to this contract. You're not nexessarily suggesting that you could reduce the code you write in that case. Then when you get to all other sorts of class composition it gets pretty hard to understand at first.
I think that's the main reason, it's easier to get started with. Also, CS teaching is nothing like real life development, as you stated, interfaces get used quite a bit more than the idea of concrete, or even abstract base classes but you just might have an abstract base class that implements part of the interface.
"a lot more code" - Really?
http://avery.morrow.name/blog/2013/01/borges-chinese-encyclo...
is actually a list of decent categories (for the most part you can say if an animal is a member of a category or not) but they are not mutually exclusive, do not tile the space of all animals, etc.
So really, even nature favors composition over inheritance.
I've been turning this idea over in my head every now then as a mental exercise (bus ride material). One, it validates my reasoning that rigid single-inheritance trees are a bad representation.
What my mind kept turning to was the possibility of Neanderthals and early humans interbreeding yet being different species that had a common ancestor. It isn't really a tree in that case.
Two, this comment gives my past time more food for thought!
book.add_to_cart(cart)
It should be cart.add(book)Item (a thing that is sold at a final price and which goes in to a shipping container (has physical size / weight data)).
Book (a type of Item) which has a price that decomposes in to various costs...
BD_Movie (a different type of Item) which has other features that decompose differently from other Items.
Though an argument could also be made for simply having a generic ShippableItem object which has extra key-value tags in that same application.
Maybe the issue isn't that the ability to use Inheritance is terrible but that the particular design choice of doing so turned out to be a poor fit for expressing the business needs.
I suspect this is why LINQ kinda popular. No one cares if book inherits item. They want to query the random collection of crap someone ordered for 'is a book' or is 'food' so they can properly calculate the sales tax in Minnesota.
Scala builds on the JVM and exposes the Java standard library. It doesn't have much choice in supporting that model. In theory, Scala could provide additional support for an alternate model, but that would add complexity without taking any away, and would require defining how the two models interact.
OO inheritance is useful for codifying a limited set of real-world models. But to truly generalize, we need to stop pretending that it's feasible to make progress without actually investing the effort to codify state v time behavior contracts over software. Today, if anyone bothers to even try to document these behaviors, it's done inconsistently and not in a form that's readily usable by tools that help you through the maze of detail. This is a shame.
Re-usable software requires (a) strong contracts for data I/O (what we call API) and (b) state v time contracts in _machine readable form_. Conceptually, no hardware system on earth would exist if not for the fact that both (a) and (b) are formally defined. Don't take my word for it: go read up on state-of-the-art SoC design and IP blocks and ask yourself if exchanging _text files_ (IP blocks) is really any different than software. I think it's not.
public interface PriceAble
{
int getPrice();
}
class Book implements PriceAble
{
int royalty = 2;
int markup = 8;
@Override
public int getPrice()
{
return royalty + markup;
}
}
class Movie implements PriceAble
{
int price = 15;
@Override
public int getPrice()
{
return price;
}
}
class Cart
{
int cartTotal = 0;
void increaseTotal(PriceAble priceAble)
{
cartTotal += priceAble.getPrice();
}
}
public class forExample
{
public static void main(String[] args)
{
PriceAble movie = new Movie();
PriceAble book = new Book();
Cart cart = new Cart();
System.out.println(cart.cartTotal); //prints 0
cart.increaseTotal(movie);
System.out.println(cart.cartTotal); //prints 15
cart.increaseTotal(book);
System.out.println(cart.cartTotal); //prints 25
}
}"Actually I made up the term ‘object-oriented’, and I can tell you I did not have C++ in mind." — Alan Kay
That's the pre-history.
The more recent history is that classical inheritance fought and fought to avoid multiple inheritance. Interfaces were poor substitutes that still only act as API signatures to match, rather than providing orthogonal-inheritance properties like a logging interface. Naturally, over time, composition based multiple inheritance exists in almost every classical inheritance language ... or soon will as codifying common mixin behavior just allows for better compiler optimization (https://en.wikipedia.org/wiki/Trait_(computer_programming).
The interesting question is if Classical inheritance or Prototype inheritance is superior and on what KPMs? Javascript, Lua, Erlang, and more have had a lot of programmer time spent on trying to convert the prototypical systems to classical inheritance idioms. Is this because most programmers are poor and the poor programmers reason better with a flawed system? Is it because they can't comprehend prototypical chains clearly? Is it because of the positioning/dominance of prototypical languages in their respective tech niches that they still exist?
There's lots to be said about the weaknesses of inheritance, but it's far from terrible. Programmers are lazy and so far, it's been the ideal paradigm. These blog posts/youtube videos/rants just seem pathetically misguided. Once someone can provide some real data, an opinion based on the interpretation of that data is something worth reading.
And that's why C++ traces its origins to Simula, which predates Smalltalk.
The problems with inheritance in this article seems largely fabricated to make it look bad, while in reality (IMO) it's a very elegant and useful tool in any programmer's toolbox.
"In a language with generics & constrained polymorphism, do you really need subtyping too?"
Haskell even allows interface inheritance, just like Java, but calls it by... well, no name that I'm aware of, but it's there.
trait Priceable {
def price: Int
}
def addToCart(cart: Cart, item: Priceable) {
cart.increaseTotal(item.price)
}
class Book extends Priceable {
...same thing...
}
The only thing non-idiomatic about this in Scala is the use of a mutable cart.C++ has goto and labels, and although they are not needed for the most part in modern C++, they are still there. The key here is the programmer does not need to use them.
class Item
def add_to_cart(cart)
cart.increase_total(this.price)
The fault is really that Item.price isn't defined, so it can't be assumed to exist in Item.add_to_cart. When he later adds get_price to all subclasses, he is really just fixing that bug.In a language like Java, this problem could never have arisen in the first place, since the compiler would know that Item.price didn't exist.
In a language like Ruby, it isn't a problem. Ruby doesn't expose "fields" and methods differently, so the consumer of Book doesn't need to know that the price "field" is now a method that computes the price. That's proper abstraction.
His interface-based solution solves nothing, because exactly the same sequence of events would have unfolded if he'd originally been using an implicitly defined price field. If he then needed to make that a method, he'd then need to define an interface for get_price and then make all the things that should be Priceable implement that interface[1].
[1] And this bypasses one of the principle advantages of Go's interfaces. You don't say that a type implements an interface; types automatically implement an interface simply by having the appropriate methods.
Don't use inheritance to solve all your OO problems. Ever heard of Design Patterns?
If Item includes a Price object that has all the possible Price properties, it could be easily modified as specs change.
I'm not saying I'm smart, but it's funny now that it at least seems like the whole "modern approach" is anti-inheritance and more about "interfaces" and composition, which essentially is what I did anyway because inheritance OOP got me nowhere.
I also very much understand the thrashing about that occurs when trying to abstract and inherit and compose my classes while making progress on a new idea. I think I early on solved it by just ignore trying to do everything The Right Way and write code that works to the solution. Then as the code evolved I could easily see where code was duplicated and begin grasping where things should be sharing code in some way. It greatly improved my efficiency AND made my code cleaner in the long run.
To enjoy good inheritance, first learn to think in layers of abstraction. If this is hard for you (it's not easy for anyone), either train yourself to do it or don't use inheritance—but then don't criticize inheritance for your own shortcomings.
Second, make each class properly express its proper abstraction. Abstract things like "items" should be very abstract. They might have a price but have no idea how price is calculated. More concrete things like books and movies should have more concreteness, but no more than necessary. Often, "deep" layers of abstraction really are little more than interfaces. All the major languages have ways of expressing interfaces as distinct from classes as such, but there is no distinction between them at heart: an "interface" is just a very abstract class; that is, it offers methods without (or with few) implementations. Whether you use "interfaces" or "protocols" or "abstract classes", your language probably offers ways of expressing "lots of abstraction" as well as ways of expressing "more concreteness". Think in layers of abstraction and then use all the tools available to express that thinking directly.
When requirements, or insights into the design should work, change, then naturally anything can change, from interfaces to detailed implementations. No tool, language, or technique will prevent change, but a clear mental model of what is being expressed and code that succinctly and directly expresses that mental model is your best basis for coping with change.
Don't blame inheritance. Don't, for crying out loud, praise interfaces when they are simply a purified form of abstract inheritance. Learn to use the tools that have served millions of programmers well for twenty or thirty years and then the tools won't seem so dangerous and strange. Or, if the tools really just don't suit your way of thinking, don't use them; use other tools; but either way don't blame the tools.
(defclass priceable-mixin () ()
(:documentation "we can calculate a price for something"))
(defclass standard-priceable-mixin (priceable-mixin)
((price :initarg :price)))
(defmethod price ((i standard-priceable-mixin))
(slot-value i 'price))
(defclass shared-priceable-mixin (priceable-mixin)
((royalty :initarg :royalty)
(markup :initarg :markup))
(:documentation "the price has two components: royalty and markup"))
(defmethod price ((i shared-priceable-mixin))
(with-slots (royalty markup) i
(+ royalty markup)))
; we want to sell books and movies
(defclass book (shared-priceable-mixin item)
()
(:default-initargs :royalty 2 :markup 8))
(defclass movie (standard-priceable-mixin item)
()
(:default-initargs :price 10))
; there is a cart with items and a total price
(defclass cart ()
((total :initform 0 :accessor cart-total)
(items :initform nil :accessor cart-items)))
(defmethod add-item-to-cart ((i item) (c cart))
(push i (cart-items c))
(incf (cart-total c) (price i)))https://www.youtube.com/watch?v=QM1iUe6IofM "Object-Oriented Programming is Bad"
https://www.youtube.com/watch?v=IRTfhkiAqPw "Object-Oriented Programming is Embarrassing: 4 Short Examples"
A nice example might be a DatabaseConnection, and a TestDatabaseConnection, that doesn't actually talk to a database, but logs what was requested. The thing that sucks about inheritance is, you never really have a DatabaseConnection, you wind up having a MYSQLDatabaseConnection or a PGSQLDatabaseConnection, that take slightly different syntax. It's not real clear if you should make a TestDatabaseConnection for each database permutation, or just make a logging connection.
Anyway, traits can fix this up a little nicer than interfaces and delegates can.
Inheritance is helpful, but it kind of sucks because you only get to use it once, and it mixes two different concepts together. "easy" API extension by tacking on more features, and the cuckoo's egg that lets you slip different implementations in without a recompile. IMHO #2 is really what people are after, and you can get that by just using interfaces for everything.
On the back end C# properties are implemented as a pair of CLR methods named get_[PropertyName]() and set_[PropertyName](). The rest is syntactic sugar. So when you "simply modify the price getter", what you're doing is simply modifying the implementation of a method named get_Price().
It is definitely nice in C# not having to LOOK at all the damn getters and setter though.
Other ideas, with traits:
> Statistical traits combinaisons: class City with % Sinjar, Theba, Techonolochikan %
-> better auto tests (33/33/33) -> 3 tests
=> ! data config, not logical structure
class Cluster with % Node1, Node2 %
class Cluster with % 25 Node1, 75 Node2 %
=> class Cluster with %
% = 25 Node1, 75 Node2
> Dna traits combinaisons: class Dna3 with Θ dna1, dna2 Θ
The general approach I've seen to this sort of thing is to create a stack of rules, and each rule applies its changes if needed. And for that, an array of function pointers will work fine.