Class Hierarchies? Don't Do That
raganwald.com
raganwald.com
Former biologist here. Actually...most living things do work that way. A human ISA primate. Genetically. Functionally.
If you merely focus on outward behavior, you get to the same place that early biologists got to with what was called "morphological classification" -- you find the weird examples of convergent evolution (e.g. "OMG egg-laying mammal!"), and you're tempted to throw out the whole classification system, even though it mostly works, and the errors are merely distracting from the inherent truth of the idea (that we're all related by genetic phylogeny; we literally share implementation).
Anyway, programmers, learn from biology: when you see these kinds of errors it probably means that you're classifying things incorrectly, not that you should stop classifying altogether.
That being said.
Classifying things using inheritance leads to a software architecture that is difficult to change. This is especially the case if you follow the principal of don't break interface.
In the real world, people can build out classification systems and easily add in edge cases when we find new ways things can be classified. In software, that could lead to a complete change in the software architecture.
I think sticking to the 'age old wisdom' of using direct inheritance for IS-A relationships and composition for HAS-A is still the right way to go. It's been tested, it works, and using HAS-A for everything makes for less understandable code imo. Often you can fix an IS-A by simply refactoring your graph with a better understanding of the problem space - as it turns out biologists have done as well.
A lot of the time, you'll think you're defining a superclass when you start writing things like "Square IS-A Shape; Rectangle IS-A Shape". But Shape will turn out to not define any common behavior, just a category you want to restrict your inputs and outputs to; and you'll want to be able to assert that anything is, in addition to whatever else it is, a Shape. So shape is an interface.
A lot of the time, you'll think you're defining a superclass when you start writing things like "User IS-A Person; ProjectOwner IS-A User". But it'll turn out that you want to keep People around even when they stop being Users, and to keep Users around even when they stop being ProjectOwners. So you'll rearrange things and find that you're now asserting "User IS-A Role; ProjectOwner IS-A Role; Person HAS-MANY Roles." And Role turns out to be, again, an interface.
The only example I can think of that does fit single-inheritance is when modelling objects that directly express a genealogy. For example, GithubForkOfProjectA IS-A ProjectA, or Ubuntu IS-A Debian. But these aren't typically things you'd express as types; they're just instances (of, respectively, GithubProject and LinuxDistribution.) Each instance HAS-A clonal-parent-instance.
I guess there's one possibly-practical use of inheritance which I've nearly implemented myself: if you force your database schema migrations to always follow the Open-Closed Principle, and you want to migrate the rows of a table as you encounter them to avoid taking the DB offline, then you could have two separate models for a Foo table, FooV2 and FooV3, where "FooV3 IS-A FooV2". Each row has a version column, and is materialized as the model corresponding to that version. Your code that expected FooV2s would then be satisfied if it was passed a FooV3.
Does anyone actually do this, though? I don't just mean "row-by-row migrations", I mean the "with two models, one version inheriting from the other" part. And, if so, what do you do when you make a change to a model that doesn't obey the Open-Closed Principle: where FooV3s break the FooV2 contract?
Shape can lay itself out with regards to its enclosing rectangle for example.
From my experience, using IS-A only works on simple concepts, and almost never on more than 2 layers. But it's still useful.
I suppose that if languages had the same "automatic function redirection" to sub-components, such as Golang, then people would use composition a lot more, though. That's actually the thing that seduces me the most about this language.
How so? I mean, sure, Shape can declare, say
bool is_inside(Rectangle enclosing_rect)
...but how would Shape know how to calculate that? It'd be an abstract method. All of Shape's methods would be abstract methods. Thus, a Shape is an interface: a contract an object makes with the system to say that it has a given API. bool is_inside(Rectangle enclosing_rect) {
return enclosing_rect.contains(this.get_bounds());
}
Now you don't need to implement cookie cutter is_inside methods everywhere, but rather the simpler get_bounds(). In fact, you can add is_inside after all your shapes are implemented and it will just work, as long as they have get_bounds.Maybe you want to add some sanity checking to make sure bounds never have negative width, so you implement get_bounds() in Shape, and implement get_left(), get_right(), get_top(), get_bottom() in the child classes. Now you don't have to add cookie-cutter assert(left < right) stuff everywhere, just in one place.
Other similar ideas: if you write an "intersects(Point)" method on each shape, then you can pull a concrete implementation of sampling-based area approximation up to the base Shape class, and leave analytical area calculation to the children. This is useful if you are working with distance-field, parametric or noisy shapes, for example.
For example: HTTPResponseHandler IS-A TCPResponseHandler because its primary role requires a TCP socket on construction. The only inherited methods should be management of the data from construction.
Problems with this only arise when people use subclass to gain the interface but not the data. You should never need to do this and it's a problem with using classes instead of interfaces for your parameter declarations – it's not a problem with class inheritance.
I think the right solution is simply to have firmer constraints about the relationships between parent and child classes - just like decoupling a class's implementation from its interface, it should be possible to separate out the interface it exposes to child classes as well. The one library/framework I've seen that does this really effectively is Wicket - it makes extensive use of final classes, final methods, and access modifiers to ensure that when you need to extend Wicket classes you can do so via a well-defined interface that won't break when you upgrade your Wicket dependency. It works astonishingly well.
But the Decorator pattern is not "inheritance without the syntactic sugar." Decorated objects, unlike subclass instances, are allowed to break the contract of the object they decorate: they don't have to claim to obey any of its interfaces, they can hide its methods, they can answer with arbitrarily different types to the same messages, etc.
If a language made defining decorators simple, I think it'd remove a lot of what people think of as the use-case for inheritance. (I mean, you aren't supposed to use inheritance for Decorator-pattern use-cases--it will likely break further and further as you try--but people will keep trying as long as the first steps are so much easier than the alternative.)
I actually agree with this, and I'd be interested to hear of language efforts in that direction. But until there's this easy way to do decorators, just telling people "don't use inheritance" isn't going to work.
In case you're hankering for a language that makes delegated composition easier, Go's "embedded fields" are sugar for exactly that: http://golang.org/ref/spec#Struct_types
Perhaps I'm missing something, but I think the article makes the point that single inheritance results in a tree structure, not a DAG. That aside, I feel it's unfortunate that he chose to illustrate his point by way of analogy, as it seems some of the arguments here are refuting the analogy and not his original point.
My personal experience has been that as software grows in complexity, isa relationships tend to fall apart. Generally this is because although they have some characteristics in common, they only have SOME characteristics in common. Further, the more the tree grows in breadth, the fewer the shared characteristics there are. Over time, this overlap becomes so small as to utterly rob the isa relationship of meaning. Saying X is a Y simply means that (generally for historical reasons) you chose to emphasise the commonality of X and Y over the equally valid relationship that X is a Z. Oh and often an A and a B.
I say grows in breadth because, with inheritance, having a tree grow in depth suggests specialisation in each branch WITHOUT behaviour shared between some (but not all) nodes in different branches of the tree. In practice I've found this happens so infrequently as to barely worthy of consideration.
>> when you see these kinds of errors it probably means that you're classifying things incorrectly
I accept that this may be entirely true. However, I have seen significant resources (time, mental effort, etc) dedicated to discovering the correct classification - although that presupposes that there is a correct classification, so perhaps I should say more useful classification - of a class hierarchy in a project, and have not been able to find something adequate. Whether this was due to our stupidity, or whether this is because there is indeed no clear way to express the relationship in hierarchical terms, the fact remains that it made inheritance an unsuitable way for us to model our problem domain.
> Often you can fix an IS-A by simply refactoring your graph
Your use of the word suggests to the reader that this is an easy undertaking. In practice I have found this is often note the case. Saying that we CAN refactor the inheritance tree doesn't mean that it is simple to do, and I posit that by the time you have learnt enough about your domain to recognise you have modelled it incorrectly, the hierarchy is of such complexity that this is generally a very difficult undertaking. Again, I'm not arguing against refactoring, but rather that refactoring inheritance hierarchies is often difficult.
I think ericHosick (above/below?) says it best:
> In the real world, people can build out classification systems and easily add in edge cases when we find new ways things can be classified. In software, that could lead to a complete change in the software architecture.
My experiences are obviously anecdotal, but since I find that I - and many other software developers whom I talk to - struggle to make domain modelling using class hierarchies work, his argument that is inherently flawed rings true.
I disagree. I think it's extremely fortunate. You are acting as if you know all the answers - and you may even think you know all the answers - but I think refusing to consider that biology went through something similar (and I bet there were numerous biologists who wanted to destroy the classification system as it was 'wrong') and found an answer that differs with yours does not mean there is no link. You could be wrong too. Maybe we are missing tools to allow us to alter our classifications later and that classifications are the best solution when given the correct tools.
this._currentBalance = this._currentBalance - cheque.amount();
"In JavaScript (and other languages in the same family), classes and subclasses share access to the object’s private properties."Self-encapsulation ensures internal variables are only directly referenced twice: via accessors. This adheres to the DRY principle. The above line should be written:
balance( balance() - cheque.amount() );
Or more explicitly as: setBalance( getBalance() - cheque.amount() );
Even if the superclass is later modified to use a transaction history, which would violate the Open-Closed Principle, the subclasses would continue to function correctly. (The transaction history would be implemented inside the accessors, making the additional code transparent to subclasses.)Source code that eschews self-encapsulation will be brittle. Developers must grok the DRY principle. Code that directly sets or retrieves internal variable values in more than two places should be refactored to eliminate the duplication.
Also, the Account class is incomplete. The Account class should have a withdraw method to mirror the deposit method. The ChequingAccount would overload the Account's withdraw method to take a cheque object, such as:
ChequingAccount.prototype.withdraw = function (money) {
super.withdraw( money typeof Cheque ? money.amount() : money );
return this;
}
I am unfamiliar with the JavaScript OO syntax to call the superclass, but this revised design is otherwise sound. In this way, the parent class can vary its account balance implementation (e.g., introduce a transaction history) without affecting its children.I think this touches on a big difference between science and software. In science, models are not "thrown away" because there is no need to throw them away. Multiple models can exist side-by-side, whereas it seems to me that this is not the case in software.
For example, Newtonian mechanics were great for a really long time. Then relativity came along and was found to be superior to Newtonian mechanics in some situations. Does that mean that we threw out Newtonian mechanics? No! It's still a useful model, regardless of the fact that it's wrong in some cases.
The point I'm making is that biology uses "single inheritance" not because it's right, but because it's useful in some cases. In other cases, where it breaks down, you will be forced to use a different model.
No one is releasing ‘Biological life forms 2.0’ and worrying about whether penguins stop working.
The problem is that writing reusable software is hard, inheritance is a really easy way to introduce reusable code into your software but a bad abstraction or base class has viral effects.
But to begin with, we're not classifying things; we're building them. Software is designed; you don't (or at least shouldn't), construct your programming abstractions by progressively mutating the different parts of your code. Nature had to reimplement flying for the bat, but you don't have to! You get to build a bat by strapping wings onto a rat, and you know how to build wings because you already did it for birds. But the hierarchy system doesn't let you do that; you'll have to do what nature did and completely reimplement flying for the Rodent subclass of Mammal, since Bird is nowhere near it in your hierarchy diagram. That's the essence of the author's point, as I read it.
I suspect Raganwald would probably go a lot further than an OOP+mixin approach (and there is no shortage of good alternatives), but the simplest way around this kind of thing is something like this piece of Ruby:
class Bat
include Flying
include Rodent
include Echolocation
end
You get the idea, and it's pretty easy to port to JS. It also lets you build the griffin and the centaur, because the software doesn't have to follow nature's rules about how to derive one thing for another. Constraining your code's structure to evolution's is-a model is a straight jacket.So that's implementation, but what about interface? We actually do care about outward behavior, and making our interfaces reflect that is important. Morphological classification may not tell you much about how a bat works inside and what other species a bat is evolutionarily close to, but it remains the case that if you want to know what a bat does, flying is a big part of that. What if I want to know whether a given animal can get over my fence? How do I ask if a creature can fly? I don't care at all that bats are closely related to mice because they're acting nothing like mice when zooming over my fence. Do I really add canFly() to some Animal superclass and have flying creatures override it? How many features will I detect that way? But with our mixins, we can just ask if it includes Flying, because the structure of our code captures that. Codifying the structure of your interface into the structure of your classes is part of the point of OOP, but it turns out that interfaces don't fit a simple inheritance mold. So you need more powerful tools to describe that structure.
This issue comes up enough in gaming that the no-hierarchy-lots-of-mixins approach has a name: the entity-component model. I bring this up not because I think the point applies differently to games but because it's a domain where you might actually build a bat.
Finally, how far would we really be willing to take biology's classification-by-implementation philosophy, even if classifying pieces of code were really our goal? We could end up with superclasses like UsesAStar and HashSet. Not really the point of all of this, right?
Long story short: I agree with the article and think the biology doesn't contradict it, but think we should probably stop using evolution as a conceptual model for how we organize our code.
Is this another ad for big design upfront, aka waterfall? It is a nice ideal, but when you get down into it, software really is evolved since requirements are constantly changing and your understanding becomes better.
> You get to build a bat by strapping wings onto a rat, and you know how to build wings because you already did it for birds.
Really you don't. The bat flies quite differently from the bird, with substantially different attributes. Strapping bird wings onto a rat often isn't going to work.
> This issue comes up enough in gaming that the no-hierarchy-lots-of-mixins approach has a name: the entity-component model.
This is actually much older than that (Common Lisp Flavors), and languages like Scala include mixin-like traits, not to mention the work of Bracha and Cook. Inheritance is still heavily involved in that though, it is not "mixins or inheritance" but "inheritance with mixins," it still very much is OOP, and you are still defining a DAG if not tree.
> Finally, how far would we really be willing to take biology's classification-by-implementation philosophy, even if classifying pieces of code were really our goal? We could end up with superclasses like UsesAStar and HashSet. Not really the point of all of this, right?
Why not? Why not classify all of our code at the finest grain we can? Of course, existing languages are poorly suited to that task, but we can always define new languages where this isn't a PITA. Or we could just use machine learning to figure out what the connections are automatically (this will happen eventually, just not for another 10 or 20 years I hope).
Not at all. You won't even know what the right abstractions are upfront. If you want to express the point here in terms of coding process, it's something like "refactor your code into this". That the design emerges iteratively isn't the same thing as saying there's no design. Contrast to evolution, which is not just iterative but also has no design at all. No one gets to sit around and think, "huh, let's reorganize this". Because if they did, maybe organisms in convergent evolution would share code.
Edit, let me try that point again, because I don't think that was very clear. You may design iteratively, but you're still designing. "Oh, these things have x in common, so let me build an abstraction that captures that commonality and have them use it." But evolution can't do that. The only design tool it has is mutating existing code into additional subclasses. You have more tools than that, so as a metaphor evolution's "designs" can be described by simpler ontologies. So why limit yourself to those ontologies? You can think of evolution as having ridiculous limitations on the kind of design work it's capable of, which are independent of the fact that it's iterative.
> Strapping bird wings onto a rat often isn't going to work.
That you have to implement genuinely different things differently is true enough, but not really challenging the point that class hierarchies make encapsulation harder. Also: maybe an engineer would design a bat a bit differently inside so that it could use the avian wing tech? Just a thought.
> Why not classify all of our code at the finest grain we can?
My point there was that if you had choose one axis on which to classify parts of your code, implementation details wouldn't likely be it. That you have to choose is a consequence of the strict single-inheritance model here. Completely classifying a software abstraction is identifying the point n-dimensional space that describes it in terms of all the different ways to categorize it. A fool's errand in general, I suspect, but at any rate not possible under the class inheritance model (single or multiple).
Edits: multiple for clarity
I would claim that our process of building things very much follows the process of evolution even if we have the ability to design explicitly rather than perform random mutations follows by natural selection. Our ability to classify things and carry over implementation fits with this way of building. OOP is not just a way of implementing a design, I would say this is actually secondary: its a way of coming up with a design in the first place where you really don't have it all down in your head before you start coding.
This is what so annoys me about Haskell: it works very well when you can figure out everything before hand; in fact, you are expected to think long and hard about your problems because its language abstractions are very unforgiving otherwise.
Haskell designs are then elegant out of necessity, and this is very limiting when you just need to get something done (aka worse is better). OO implementations are typically over-engineered and less elegant, not because OO thinking is somehow defective, but because elegance is not required to encapsulate complexity and just get something working.
> My point there was that if you had choose one axis on which to classify parts of your code, implementation details wouldn't likely be it. A fool's errand in general, I suspect, but at any rate not possible under the class inheritance model (single or multiple).
Implementation details provide the strongest signals of similarity than artificial interfaces. If you were going to let a DNN or RNN classify your code, it would probably be along this axis as well as with how the code was actually used.
> That you have to choose is a consequence of the strict single-inheritance model here.
I rarely let myself be constrained by single inheritance, even when I'm coding in C# I have my mixin patterns handy (ya, more boilerplate, but I can type fast; all my own languages support mixins).
> Completely classifying a software abstraction is identifying the point n-dimensional space that describes it in terms of all the different ways to categorize it.
I agree, but if we aren't limited by single inheritance (say linearized multiple inheritance via mixins...or gasp...aspects), then this argument no longer holds.
[1] I too used mixins in C#. Not sure how you'd do it now, but at the time you had to use Castle DynamicProxy and it was...unpleasant. But worth it.
Upvotes for an intelligent debate.
namespace Yarp{
public interface IMixinFoo{
}
public static class IMixinFooExt{
public static void DoBar(this IMixinFoo _this){
Console.WriteLine("poo");
}
}
}
namespace Grog{
using Yarp;
class Baz : IMixinFoo{
public static void Main(string[] args){
Grog g = new Grog();
g.DoBar();
g = null;
g.DoBar(); // still works, if DoBar guards against a null-ref exception
}
}
}Extension methods are great for writing anything that chains a lot of method calls together, specifically because they aren't methods on the object. Because it's possible to call an extension method on a null instance, you don't have to inject arbitrary error handling in the middle of your call chain. You can do it at the function site.
That said, there are a number of places where LINQ doesn't chain very well. It's built-in aggregating functions, for one. Average, Sum, Min, etc., don't know how to handle a 0-sized collection. And of course, those functions are notionally not defined for 0-sized collections. But I think a better design is to allow the user to specify a default value in the case of a 0-sized collection, or to just return null (signifying "there is no Average, there is no Sum"), rather than throw an exception. So I have a polyfil of sorts to make similar extensions that play nicer.
Enumerable.Empty<int?>().Average() returns null.
If you have a sequence of integers, you can get the behavior you want by converting the elements to nullable types like this. seq.Average(x => (int?)(x))
This is not my experience at all. I find refactoring in Haskell to be quite pleasant, and you can generally start from something small and build up. You do have to get the types to line up before it will build, but if the types aren't lining up your logic is likely incorrect, and figuring out why can be instructive.
Interesting comment. I have no particular leanings towards any methodology[1], but I do find it useful to understand a situation before presuming to have appropriate ideas about changing it[2].
To many that constitutes big design upfront. So be it. My projects tend to deliver on time, and to budget.
[1] https://www.wittenburg.co.uk/Entry.aspx?id=99bb5987-e08d-4e8...
[2] https://www.wittenburg.co.uk/Entry.aspx?id=46870dcd-70cb-4ef...
The worst projects, with runaway costs, unhappy stakeholders and a real brutal grind for developers had the least requirements planning.
I've seen code design up-front go wrong often enough, but I've yet to see up-front specification and requirements gathering be anything but much more successful than the alternative.
As a matter of fact, knowing you have to deliver C, but then focusing development on A, with an idea that there might be a B, and not putting any real thought into any of them is so so painful IME. Nobody ends up happy. When I hear "iterative development" these days from a developer, I don't hear "pragmatically responding to a change in requirements". I hear "I don't feel like thinking this through, so I'm going to do something I know doesn't meet the requirements first, and we'll rework it at some point".
I've rarely seen core project requirements change much over the course of a project. I've seen developers cargo-culting and generating rework a number of times though.
As an early "Agile" fan, who was a developer when the Manifesto debuted, it's real frustrating that today's development landscape seems to promote the idea that planning is bad and you should just start diving into writing PostIt notes and setting up your first week's sprint without a thought to an overall schedule.
That's not Agile, that's just cowboy coding with the appearance of rigor (IMO). The whole point is to make a plan, research, investigate, get a solid idea of what you need to do, then do it. And while you're doing it, if circumstances change, be prepared to change with them.
> "Responding to change over following a plan".
> "while there is value in the items on the right, we value the items on the left more"
Which totally makes sense. Yes, don't stubbornly insist on delivering a feature the stakeholders don't want just because it's in the plan. And if you have a fixed time budget, yes, focus on the scope that's most important and pick your battles. None of that says "don't plan". "there is value in planning". That is what the Manifesto says! So stop using "agility" as an excuse to do a bad job, generating tons of rework, burning people out, and spending other people's money. There's nothing "agile" about that.
"Iterative Development" is just code for exactly that IME. Kaizen is about improving your process. Not throwing it away.
Sorry if that's a bit too ranty. ;-)
The article was arguing that inheritance is wrong, and using a (broken) biological metaphor to back up that argument. In fact, it's pretty easy to argue that the coding example was broken in the same way -- you could resolve the stated dilemma (using a transaction history instead of a current balance) by creating a new subclass at some level of the original hierarchy. And if you can't do that, you possibly had a broken design from the start, or you really did need to re-work all of your original code to adopt to the new implementation (or both).
Mixins don't make your code magically adaptable to re-implementation. They force you to encapsulate more stuff, but you could just as easily design an inheritance hierarchy with the same properties (i.e. make subclasses all work with private data via protected methods), or you could adopt a hybrid approach -- for example, one could imagine making the class hierarchy in this post using a Strategy pattern to implement the actual tracking of balances.
Usually, when people have an axe to grind with inheritance (or encapsulation), it's because they've run into the problem that code design is subjective and imperfect. Programmers don't like that much, and it's more fun to blame the tools.
Do you have any other good examples of situations where it makes most sense to use inheritance?
This is breaking the Liskov Substitution Principle[1]. By saying that Penguin is a subclass of Bird, you're implying that I should be able to stick a Penguin into any code that expects a Bird, that code will just work, without it having to care about the particularity of that Bird actually being a Penguin.
[1] http://en.wikipedia.org/wiki/Liskov_substitution_principle
[edit: grammar]
Overall agreed, though. This ties into timr's point that if there are errors in your classifications, then your class hierarchy is incorrect.
All ridiculous discussions of biology aside, having a class based hierarchy is a maintenance issue in comparison to designing classes through compositing. There are 2 ways to design a 'cheeseburger': a) meat sandwich is a super class of burger which is a super class cheeseburger b) meat sandwich which contains, bun, cheese, patty lettuce
B is a more maintainable system and allows for better class flexibility in the design. It avoids having complex bugs where 'meat sandwich' changes some internals and breaks something down the hierarchy chain in some subtle way (for example in releasing a access to a critical resource).
These turn out to be remarkably tricky questions which is why Linnean classification is not as stable as we would like to believe..... It mostly works, but only to some extent, which is the problem.
All models are wrong. Some are useful. As you pointed out, there is no such thing as a species in the real world -- it's just in the model that there's a species.
The model has been so successful that, unfortunately, it's treated as "true" by many people, which means that 1) they don't understand its limitations, and 2) they don't work to improve it.
+1, I'm glad you mentioned this.
The dissonance comes in with timeless ontologies. Ontologies whose members don't share a common ancestor. Rather, the members share traits. Inheritance works in some cases, but not in all. Once it stops supporting all cases, it causes unnecessary complexity to the model.
In my limited understanding of biology this works fantastically well. For a platypus, "Is-a" is ridiculously broken, and one would not want to try and reproduce the underlying source code. However for classifying and interacting with the platypus "has-a" warm blood, "has-a" egg-laying-thing (#) "has-a" receptor for x is all we care about.
I suspect that is-a works fine at the cellular level and tends to break higher up - and that is pretty much my experience - in software we want to build Eco-systems - and forget that an individual humans body is more an Eco-system than a single machine.
interesting food for thought
(#) what I said about limited biology
You're confusing the model with reality. The classification system is simply a model that describes many things correctly, and many other things incorrectly. If you feel that there is inherent truth of the idea, then that probably means that it's a really good model. But it's still just a model.
For example, you said that a human ISA primate. But that's only in the model. Outside of the model, there is no such thing as a primate. Reality is far more complicated, and "human ISA primate" misses many corner cases. You also said we literally share implementation, which, again, may be useful but isn't quite true, whether one is talking about DNA, epigenetics, or macroscopic properties.
But that's okay -- it's still a good model, because it's useful and simple and easy to understand and works much of the time. We just have to be careful not to forget its limitations and applicability.
what is the morphology of a class? I would argue that it is the interface. That is the part that determines the form that other programs interact with. that's the part that actually matters for object oriented design.
so while you're right that in biology morphological classification is a distraction that belies the existence of deeper relationships, in computing morphological classification is actually the most important part and what we should focus on.
Inheritance hierarchies have their issues, and raganwald touches on them, but there's a strawman argument here.
(Incidentally, raganwald, I've noticed this about all your OO articles. You seem to have a bias against class-based design. It's causing your essays to be less brilliant than they could be.)
Fundamentally, you can think of inheritance as a special case of composition. It's composition combined with automatic delegation.
In other words, if you have A with method foo() and B with method bar(), "A extends B" is equivalent [1] to "A encapsulates an instance of B and exposes 'function bar() { return this._b.bar(); }'."
This is very useful when you want polymorphism. Writing those delegators is a pain in the butt.
More importantly, it tells us how to use inheritance safely. Only use inheritance when 1) you want to automatically expose all superclass methods, and 2) don't access superclass variables.
Now, JavaScript does have the specific problem that you can accidentally overwrite your superclass's variables, and that's worth talking about. But I think that saying "inheritance is bad" goes too far. The article would be stronger if talked about when inheritance is genuinely useful, the problems it causes, and how to avoid them.
Edit: In particular, I want to see more about polymorphism. Polymorphism is OOP's secret superpower. Edit 2: I'm not saying polymorphism requires inheritance.
[1] Not quite equivalent.
See Clojure's Protocols or Haskell's type classes for examples of this.
Second, parametric polymorphism in Haskell is statically resolved. You can have polymorphic functions, but any given container still contains a single type. You can still do dynamic polymorphism in Haskell (by storing a list of records of functions, rather than storing data directly) but it doesn't typically involve type classes.
This is backwards. Polymorphic values without constraints admit almost no operations at all - you can "copy" them, that's it. This is true to the point that (save diverging) given f :: a -> a, f can only have one meaning.
Invariance of sequence elements is also far less problematic in ML-family languages because they have sums.
[exists a . (a -> b, a)]
is literally completely equivalent to [b]
as the types ensure there is no other thing that can be done with those pairs.The convenience factor is thus almost never the case. There are some nice theoretical properties and a great embedding of OO in Haskell via existential typing [0], but it should rarely be used.
[0] http://www.cs.ox.ac.uk/jeremy.gibbons/publications/adt.pdf
data Test i o = Test String i (i -> o) o
and then say your testing function is something like runTest :: Eq o => Test i o -> IO ()
runTest (Test name input f output) = case f input of
o | o == output -> putStrLn $ name ++ " passed"
| otherwise -> putStrLn $ name ++ " failed"
Then let's say you had a bunch of tests. For example, you want to test that addition works: test1 = Test "addition" (1, 2) (\a b -> a + b) 3
And you want to test string concatenation: test2 = Test "concat" ("hello", "world") (\a b -> a ++ b) "helloworld"
Then you could write your tests as doTests = runTest test1 >> runTest test2
Now if you have a lot of tests, it would be nice to put them in a list: doTests tests = forM_ tests runTest
However, this would require that every test have the same inputs and outputs. You couldn't do doTests [test1, test2]
Even though the resulting type is known (it will be an IO () regardless), and even though runTest will operate on each one, because test1 and test2 have different types, you can't put them all in a list.I think that `forall` and similar allow you to get around this restriction somehow, but I don't really know how that works.
But, for completeness, here's how you could write your type
{-# LANGUAGE ExistentialQuantification #-}
data Test = forall i o . Test i (i -> o) (o -> o -> Bool) o
Although, note, this is exactly equivalent to `Bool`, although in two ways—if we knew the comparator function was commutative then there'd be just one way to convert to `Bool`. testBool :: Test -> Bool
testBool (Test i fun cmp o) = cmp (fun i) o
testBool' :: Test -> Bool
testBool' (Test i fun cmp o) = cmp o (fun i)
But in either case there are no other ways to "observe" the existentially quantified types since we've forgotten absolutely everything besides `Bool`. More likely we would want to also, say, show the input. data Test = forall i o . (Eq o, Show i) =>
Test i (i -> o) o
and this type is now equal to `(String, Bool)`. testOff :: Test -> (String, Bool)
testOff (Test i fun o) = (show i, fun i == o)
So, in general, if you're using existential types you really want to either be using multiple variants or when you have such a combination of observables that it's not worth expressing them all directly.This is the case for all typed programming languages, not just Haskell. So-called dynamic languages like Python, Ruby or Javascript are merely static languages with but a single type[0].
[0] http://existentialtype.wordpress.com/2011/03/19/dynamic-lang...
std::list<Parent *>
then some of those pointers may actually point at a Child. The client code doesn't care. This is an important kind of polymorphism.In Haskell, you can't reasonably express this with typeclasses, which surprises folks new to Haskell. You can still express it (as I mentioned), it just takes a different form (and precisely which form is best can vary with other considerations).
For what it's worth, I do think people underestimate the applicability of closed sum types.
data P = forall a . Parent a => P a
type PList = [P]
where Parent is a type class.You have to be a bit more explicit when using a Child in the position of a Parent when adding to the list and you have to use (fully polymorphic!) pattern matches to extract elements from the list, but personally I consider this a good thing as it's more explicit.
Aside: Existential types is what OOP interfaces are -- and interfaces are often encoded as such in language semantics; see e.g. Types and Programming Languages (Pierce).
EDIT: Typos in code -- unfortunately my Haskell is a little rusty :(.
The idiomatic way you'd create a "heterogeneous container" is to store a single algebraic datatype with different constructors, rather than try to store different types at all. This doesn't actually give you a heterogeneous container, of course, but it works perfectly in most cases, because in most cases the set of things you need to store in the container is closed. You really only need a truly heterogeneous container when you need the ability for someone else to come along and extend that set. Concretely, if you're writing a ray tracing application, you know all the possible shapes you may need to handle, and this approach is perfect.
On the other hand, if you're writing a ray tracing library and you want the library user to be able to define new shapes, you may want to consider another approach. The idiomatic approach here was already mentioned by dllthomas: this is a functional programming language, so use functions! Specifically, use a record of functions, with each function in the record serving the same role as a method in OO. The functions can have private data by closing over it.
Haskell has a couple of other options available, as well.
You can use existential types, but they don't really buy you anything over the record of functions approach other than perhaps looking superficially more like how you'd do things in an OO language. With this approach you define a typeclass and make all the types you want to store instances of it. The container then stores instances of the typeclass, rather than a concrete type.
You can also use Data.Dynamic to create dynamic values, which will allow you to store a truly unconstrained mix of types in the same container. Since you have to cast the dynamic values back to their real type before using them, though, this isn't a great solution - you end up with code that looks similar to chains of 'instanceof' in Java or 'dynamic_cast' in C++.
This is also how one does OO programming in C: just roll your own v-table using a struct of function pointers. You don't get to close over an environment in that case, so you have to be careful to pass everything in.
data Showable where
Showable :: Show a => a -> Showable
This allows you to create a polymorphic container like [Showable 5, Showable "hello"] where the polymorphic type is constrained to be a member of the Show typeclass.However as mentioned elsewhere ( https://news.ycombinator.com/item?id=7500280 ) if most of what you do to organise your code involves passing functions to functions and very little of it involves creating class hierarchies or prototype chains, it's a fair assessment that the language that you are using is more functional than OO.
The language that you are using may be a subset of the whole language, but with JavaScript that's given - you have to find the good parts or go mad trying. I was wrong about JavaScript as a whole, but maybe less so about JavaScript as it is successfully used.
Private members are variables/functions defined within the constructor's closure. Public members are properties added to "this" by the constructor. Mixins can be done by calling another class's constructor on yourself.
The point is that class-based OO can be trivially imposed on JavaScript objects without abandoning the native object construction mechanism like ember.js does. In fact, CoffeeScript does this in order to implement its own classes.
Everything* in JS other than numbers, strings, and booleans are objects. Functions are objects.
See http://www.objectplayground.com for details. (Temporarily down due to server problems, but hopefully back up soon.)
*Not really everything. Not objects: undefined, null, number, string, boolean. Objects: object, array, regexp, function, everything else.
And encapsulation in JS is fundamentally broken - everything is public.
I agree.
Others have replied (rightly) that under the hood, JavaScript has a lot of objects. Functions are objects. So in that sense I am wrong: JavaScript is an OO language.
However the experience of programming well in JavaScript feels more like using a functional language than using a OO language. Good JS has a lot more to do with thing such as passing functions to functions or understanding how " fn().then(fn()) " works, than it relies on class hierarchies, protypical or classical.
One practical example: In Javascript (CoffeeScript), null values can propagate for a very long time. Calling a non-existent method throws an error immediately, while using a nonexistent field (because it changed in the super class) is not that easy to track down. From my CoffeeScript experience, almost any inheritance brought us a headache when we rapidly iterated on our code - but that doesn't mean that CS class construct isn't useful in defining recipes for well-encapsulated objects.
Your invocation of Effective Java made me look for Effective Javascript, and it does exist. Amazon users give it five stars: http://www.amazon.com/Effective-JavaScript-Specific-Software...
Can anyone comment on how well this books fulfills the expectations implicit in a book calling itself "Effective X?" Or just how effective the book is with respect to accepted javascript practice?
By every account I've seen, it's a great book and the only reason I haven't read it is that I've already been bitten by all of JS' pitfalls once or twice :)
The real reason I'm posting a comment, though, is to give you a link to the JS Jabber episode in which they talk to Dave about the book:
http://javascriptjabber.com/044-jsj-book-club-effective-java...
That episode will give you a good idea of what the book is like.
It's also fairly comprehensive, ranging from - some evilness (type inference with ==, eval and its performance toll) - functions and higher order functions - objects and prototypes. Some good explanations of the all prototype and constructor thing - array, dictionnary and some things to know about their prototypes - api design and concurrency
It's 200 pages full of content, I recommend it.
Your point would hold only if all subclasses agreed to use only the public interface of their parents. But if you do that, your "inheritance" isn't really classical inheritance any more, it's just something that saves you typing when implementing delegation, like ruby's method_missing.
The article is not taking issue with composition + "an easy, terse delegation mechanism." The article is taking issue with actual inheritance: the sharing of private state between class and subclass. Your claim that the two things are equivalent just isn't true.
That's why I said it was a strawman argument. (It's also a bit hypocritical: raganwald says, "JavaScript does not enforce private state, but it’s easy to write well-encapsulated programs: simply avoid having one object directly manipulate another object’s properties." Somehow he fails to apply the equivalent principle to inheritance.)
What is happening here is that you are redefining "inheritance" in a new, more restricted way that is not in line with common usage, and then saying "But real inheritance doesn't have these problems...."
i think the problem is you're both just running on anecdotes.
to add fuel to that fire: i would definitely side with jdlshore on this one. in Objective-C, for instance, you cannot even access a superclass's private properties[1]. most every team i've worked on has avoided protected properties (for languages like Java which even have them) and encouraged even subclasses to talk to their superclass via the superclass's public interface.
[1] of course, you can always declare a category for the superclass which can expose whatever it wants, but subclasses are in no sense privileged in being able to do this.
His concluding remarks about class hierarchies are: "Class hierarchies create brittle programs that are difficult to modify.", but he hasn't established that - he's only shown that ignoring encapsulation within a class hierarchy creates brittle programs.
If he wants to argue that class hierarchies encourage that sort of behaviour, and should therefore be avoided, then he is welcome to, but he didn't make that argument.
His evidence only shows that breaking encapsulation is bad, even if it is contained inside a class hierarchy. That make it a strawman because the thing he has torn down is not the thing he is arguing against.
But he also argues that they don't accommodate change well by their nature (he refers to this as the semantic problem):
>> Furthermore, the idea of building software on top of a tree-shaped ontology would be broken even if our knowledge fit neatly into a tree. Ontologies are not used to build the real world, they are used to describe it from observation. As we learn more, we are constantly updating our ontology, sometimes moving everything around.
>> In software, this is incredibly destructive: Moving everything around breaks everything. In the real world, the humble Platypus does not care if we rearrange the ontology, because we didn’t use the ontology to build Australia, just to describe what we found there.
His exact quote was "Class hierarchies create brittle programs that are difficult to modify."
But he has not demonstrated the first part of that except in one specific case. That case may (?) be common, but it not inherent in the problem - class hierarchies do not require breaking encapsulation.
His conclusion is not supported by his argument because his argument applies only the the strawman he created, and not to the general case to which his conclusion refers.
But that's a universal issue with JavaScript OO in that it provides no direct language support for encapsulation.
Developers have, for the most part, learnt to be disciplined about not accessing "private" fields in JS objects. That they (we) have not learnt to apply that lesson with respect to class hierarchies is evidence for how willingly we throw away good principles when we are working in a "special case".
It is also a lesson in why having language features that force developers into good practices is sometimes a net win, even though we might also rally against them (because we dislike their verbosity and/or hand-holding).
Languages that make this special form of encapsulation easy suffer for it indirectly (I'm thinking esp. of java/C# here). Auto-passthrough allows huge API's that would otherwise be unwieldy. Unfortunately, inheritance isn't enough, and when that happens, the poorly designed api's with huge surface areas encourage really bad hacks. All in all I'm not convinced that api pass-through is really a net win for a language. There are alternatives too, such as mix-ins or extension methods, that would allow you to manually pass through only the smallest truly necessary core, and just re-mixin the extras.
But this is all about the best-case for inheritance. Inheritance in common usage also contains two other less ideal aspects, however: semi-private methods, and virtual methods (method overriding). I'm skeptical either has value. Overriding almost necessarily means tight coupling - you need to understand at some level how the internal state of the superclass works to replace calls (even those that the superclass itself makes!) to methods of the superclass. Protected methods suffer from a similar problem - what kind of api is public enough to allow access by a subclass but not public enough to allow access by a wrapper? Of course, using protected methods makes the previously described problem of discouraging bad design even worse; it makes it even harder to write a wrapper, necessitating inheritance even when that's perhaps not exactly what you want.
Finally, if all you want is polymorphism, you just need interfaces, not implementation inheritance.
I think you're right in pointing out that the OP's post doesn't do OOP's subtlies justice, but beyond interface inheritance, I don't think I care much for other aspects of inheritance.
I'm not sure why you say this is a narrow subset. I'm describing a way of thinking about inheritance (inheritance is like composition + automatic delegation). That way of thinking can be applied to any subclassing operation, and I think it's instructional to do so. It can help you see what's a good idea and what's not.
Let's run down the list. Assume you have a class A with a method foo() and class B with method bar().
The equivalent* of "A extends B" is:
function A() {
this._b = new B(); // equivalent* to calling superclass constructor
}
A.prototype.bar = function() {
return this._b.bar(); // manually delegate
}
Now let's say A accesses its superclass's private variables in order to do something. The equivalent* is: A.prototype.foo = function() {
return this._b._privateVar * 2; // obviously bad, don't do that!
}
You wouldn't access the private variables of an object you compose; it's obviously bad form. Don't do it when you inherit, either.Now let's talk overriding the superclass's methods. There are several different ways you could do that. The equivalents* are:
// Replace superclass method
A.prototype.bar = function() {
return "my own thing"; // obviously fine
}
// Replace superclass method and access superclass state directly
A.prototype.bar = function() {
return this._b._privateVar * 2; // obviously bad, don't do that!
}
// Extend superclass method
A.prototype.bar = function() {
var result = this._b.bar(); // equivalent* to calling superclass method
return result * 2; // perfectly okay
}
// Extend superclass method and access superclass state directly
A.prototype.bar = function() {
var result = this._b.bar(); // equivalent* to calling superclass method
return result * this._b._privateVar; // obviously bad, don't do that!
}
Don't access the private variables of your superclass (or objects you compose) and you'll be fine. Sure, you'll be in trouble if the superclass changes the semantics of the parent method, but that's true of all functions everywhere. If the semantics of a function you're using changes, your code probably just broke. It doesn't matter if the function is defined in a superclass or not.The one thing that's unique to inheritance is the idea of semi-private ("protected") methods that are only visible to subclasses. I agree that they're something to be used sparingly, but they're no different than any other superclass method in how they should be used and overridden. It's a moot point, though, because JS doesn't have them.
*Not exactly equivalent, but close enough for these examples.
If you're open to inheritance, you're necessarily open to composition - making that messy serves no purpose.
As to this example:
// Replace superclass method
A.prototype.bar = function() {
return "my own thing"; // obviously fine
}
This is NOT fine - it's really quite nasty. You're breaking encapsulation by affecting how the internals of B work - calls from B's code to bar() will now fail to work as expected. What you want is to use your bar to outside code, but not affect the encapsulation of the superclass.And note that none of this mitigates the pit-of-complexity that inheritance encourages as described in the post you replied to (i.e. bloated, hard-to-wrap API's). There are inheritance-like techniques that work better and don't have the downsides.
Somebody's been downvoting thoughtful posts like yours (because they disagree with them, I guess). I wish they'd stop and I wanted to let you know it wasn't me. :-)
Given the subtly of these issues (what is the right design of such an artificial construct), I guess the only obvious thing is that there's no obviously right answer - let alone that it's easy to explain the pros+contras in a reasonable amount of time on an online forum such as this :-).
Your problem seems more with badly constructed public apis rather than overriding itself. The only public methods of a class that should be overridable are those with a simple and well-defined expected behavior. Overriding doesn't have to imply tight coupling. So long as it adheres to some expected contract (e.g., input/output ranges and exception handling), an overridable method should be just as much a black box to the parent class as the subclass. An overridable method with no side-effects is equivalent to an initialization parameter to an encapsulated object.
* Protected methods suffer from a similar problem - what kind of api is public enough to allow access by a subclass but not public enough to allow access by a wrapper?*
You have to think of the consumers of the class. Consumers that only need to use the class and are satisfied with the publicly available interfaces only need access to the public methods. Consumers that want to change the behavior of the class (often for the benefit of other consumers) will override the protected methods. Protected methods offer a set of extension points to consumers while providing useful default behavior for those that do not need them.
> This is very useful when you want polymorphism.
> Writing those delegators is a pain in the butt.
What's a scenario where you'd be exposing a large number of delegators? Reading this made me think - maybe your class hierarchy needs to be abstracted more deliberately into structs-with-interfaces vs domain logic classes. (It's more likely I just haven't thought about the kinds of problems you are thinking of.)That said, that's a fairly rare case. Classes with such a large surface are often a code smell.
Your point about "don't access superclass variables" is also spot-on--I don't see how he misses that "self-enforce not calling other objects' properties" (because the language doesn't do it for you) is really not very different than "self-enforce not calling superclass properties".
Per his article, I agree that fragile base classes are a problem, but not every base class is automatically fragile--you can design an API for subclasses (in Java/C# worlds, by being very explicit/thoughtful about what you make private vs protected), just like you design an API for external callers.
But he takes lessons from a language that has no access control to member variables, and tries to apply them to all OO languages. That's the problem with the article, IMHO.
The problem IMO is that we are stuck in a view that inheritance is really about ontology, when what we really mean, and want, is code reuse. It's very hard to make a 5 level deep ontology not break down. This is why we have this whole 'prefer composition over inheritance' business.
But while we are stuck with that kind of mindset in Java (at least pre java 8), we miss the capability of using inheritance as a way of adding mixins. There is much power in using mixins as a way to clarify composition, while we still keep the inheritance tree very shallow.
That's one thing we get with judicious use of the Scala cake pattern, which you could easily reproduce in javascript: Composition without really having to write a bunch of useless boilerplate. There's a nice talk out there about it. Cake Pattern: The Bakery from the Black Lagoon.
The trick, as with most other programming techniques, is to use it carefully.
Having written Objective C full-time for 5+ years, it is rare to need to use inheritance in classes that actually implement application level logic, however it's used in just about every class you make as you inherit from Apple's base. (UIViewController, UIView, etc..) It's useful to subclass and add categories as needed. At a functional level it's not an issue, however with that in mind I'm not sure it's a great pattern for Javascript.
I'm not sure if you meant it this way, so I'll underscore this point. Code reuse is truly the worst "reason" to use inheritance, and in fact an anti-pattern.
Composition is the only proper way to reuse code because composition is explicitly stating you are using the composed code.
And every other pattern is not suggesting this very same thing?
There are plenty of examples of this in practice - someone pointed out NeXT/Apple's UIKit for example elsewhere in this thread (which also uses composition where appropriate). The Java collections hierarchy is also a good one (LinkedHashSet < HashSet < AbstractSet).
I strongly agree with your main point though: that the way we get "good" OO design is through composition and mixins
Inheritance gives you a great characteristic -- isa relationships. And this is something you don't get with composition.
That said, the fragility with poorly constructed base classes is real. But a succinct base class can be very valuable and useful, and not that brittle. Just don't stuff cruft in it that is of questionable value. Ask yourself what's the least you can put in the base class and still provide value.
And this is where interfaces are also useful. You can get the isa relationship w/o much of the brittleness as there is no internal state associated with the interface. But you are still creating a hierarchy (just not of classes, but of interfaces).
It's a useful article, especially for those new to the idea. But the takeaway should be to use care. Not to avoid at all costs.
I think the takeaway here is to not try to use JavaScript as if it was some other language that has support for classical inheritance, but rather to embrace the prototypal inheritance model and compose your apps in a different way. It's not a "GOTOs considered harmful"-style rage post, but rather a warning to budding JS developers who are coming from a more classical perspective. It's not that you "shouldn't use classes", it's that you shouldn't build complex hierarchies and type systems. Keep it simple!
You can even get the relationship without brittleness but with some internal state, as long as the superclass wraps the internal state in a property. For example, in Python, the currentBalance private state would obviously be implemented as a property, not an instance variable, so that it could be mutated by subclasses while still hiding the private implementation in the superclass. (This would be harder to do in JS, I admit, since JS doesn't have anything corresponding to Python's properties.) So the lesson here isn't really "don't use class hierarchies", it's "don't use class hierarchies stupidly".
In the main it seems that classical OOP with problem domains modelled in inheritance heirarchies is what's been taught as the standard to aspire to for the last two decades. The fragile base class problem and other criticisms aren't unknown, of course, but awareness doesn't seem to have dispersed as widely as the practice of lots of subclassing, which is usually introduced as an essential OOP component in the first few lessons.
It might be too much to say "don't use inheritance, ever." But it might not be too much to paraphrase Knuth about optimization and give people two rules regarding it: (1) don't use it (2) (for experts only) don't use it yet.
I think it's still a bad idea - as you note, isa is available without necessarily having inheritance; we know inheritance is dangerous, so why include that?
In languages like C++, C# and Java, it has long been expert advice to avoid inheritance where possible, and use interfaces. But the language and language culture is leading everyone astray. Defining interfaces is usually harder or stranger-looking, and is taught as an adjunct to inheritance.
Other languages get this more right. Haskell has what it calls typeclasses, but they are more similar to interfaces in the aforementioned languages.
We seem to be making exactly the same mistake in the ECMAScript standard, except it's even worse; you can't define an interface even if you wanted to.
I don't agree that it's better to have an "interface" which exists purely in the head of every programmer working on the code. Languages can be doing more for us, and it doesn't have to be the kind of verbosity that Java offers.
But I'm relatively convinced that interfaces/protocols that require coupling of implementations are clearly inferior to the ones that are implicit.
I think you mean typeclasses?
One of the things that always bothered me about interfaces was the inability to define a default implementation, especially when developing UIs. A proper mixin system (like what's used by React's components) has solved this particular problem for me.
Fixed. Thanks.
I agree with you about Haskell. I almost went into a discussion of how Rust does this. They don't have subtypes, but they do have code reuse if you explicitly declare it. Caution, I'm just an admirer of Rust, not a user. So maybe it sucks too, but this seems closer to how things ought to be.
http://static.rust-lang.org/doc/master/tutorial.html#default...
https://mail.mozilla.org/pipermail/rust-dev/2014-March/00895...
"JavaScript does not enforce private state, but it’s easy to write well-encapsulated programs: simply avoid having one object directly manipulate another object’s properties. Forty years after Smalltalk was invented, this is a well-understood principle."
However, the author doesn't seem to really understand this as he makes the case that access to private state and behavior of a "superclass" violates encapsulation:
"In JavaScript (and other languages in the same family), classes and subclasses share access to the object’s private properties. It is not possible to change an implementation detail for Account without carefully checking every single subclass and the code depending on those subclasses to see if our internal, “private” change will break them."
Well, yes they do allow access, but that doesn't mean you have to use it! This is considered bad practice when extending any class in other languages that I'm familiar with (C++, Ruby). Please take some of your own advice.
I do agree that hierarchies do not fit the real world as well the contrived examples from my first OOP classes and they should be used with extreme caution. Let's not throw out the baby with the bath water, however.
That said whenever I see something on raganwald.com I'll still read it :)
The alternative he presented is not to share private state between class and superclass. This really isn't a "know the right tool for the right situation" kind of thing -- it's almost never a good thing.
You could accomplish this alternative, among other ways, by:
1. Using composition and delegation. 2. Using mixins, if your language supports them. 3. Using inheritance, but depending only upon your superclass's public interface.
This is an important distinction. Javascript doesn't have classes, but it does have inheritance. The problems raganwald points out with the Account example come not from the organization of the code into pseudo-classes, but from the fact that it uses Javascript's inheritance mechanism.
It would be perfectly possible to write a version of the example that exhibited the "fragile prototype problem," and conversely, one can easily write a version in Smalltalk that uses composition instead of inheritance and thus has no fragile base classes.
People hate conditionals. People hate polymorphism.
Everybody is wrong about everything pretty much all of the time, it appears.
For example:
var objA = {doIt:function(){console.log('hello from a')}};
var objB = {doIt:function(){console.log('hello from b')}};
var doItDynamically = function(doitObj) {
doitObj.doIt();
};
doItDynamically(objA);
doItDynamically(objB);
a and b do not share a common class (since classes do not exist in JavaScript), but they implement the same interface. For this reason, they can be used polymorphically, as if they shared a common base class or interface in Java or C++.Misanthropy Driven Development? I would like it if I didn't already hate everyone and everything.
Common Lisp philosophy of classes and OO fits the thicketness of the real world better. Generic functions, multimethods, mixings etc. Just embracing the notion that classes and encapsulation might be orthogonal issue opens up the way to use class system better ways.
Yes it is inconvenient to do traditional OO programming with JavaScript, but I'm not convinced that is a bad thing. Encouraging subclassing, as pointed out by the author, could actually be detrimental.
I am bit concerned that the standard committee is going to make the language worse while trying to fix it.
AFAIK the goal is write what you mean and not "fake stuffs because the language lacks structure".Dont want to use any ES6 new syntax, dont use it. And yes , the committee SHOULD meet developpers demands as much as possible. Javascript has no clear paradigm,everything should be on the table.
Everyone has their biases, and my bias would be to embrace the dynamic and functional aspects of the languages that separate it from Java, rather than creating new syntax that pastes over the fact that the language is fundamentally different.
FWIW I'm with you: I think that treating JS as a functional language is a cleaner fit that crowbar-ing OOP into it.
Our decision is to deal with JS on it's own terms. Clojurescript would be appealing if it didn't involve an AOT compiler (and one that's not trivial to set up in Windows)
The cruft, the lack of DRY as the mapping between what is being modeled and what is represented in the code -- accumulates with class hierarchies for sure.
But the idea of inheritance and tree structures has been around and strong for the past 50 years -- not because people are inherently lazy -- but because it models something very primitive about programming: the evolutionary nature of development and thinking.
This is extremely non-mathematical, but extremely non-trivial.
Tree structures (and by extension 'hierarchies') are perhaps the most fundamental way to organize data. There is nothing more sophisticated than a tree search -- it is the basis of all exponential / efficient access times and all knowledge and organization of memory. It's why evolution proceeds in tree structures. We organize data in our mind in tree structures. Life over millennia organizes itself in tree structures.
I'm sad to report that the world will not quickly become clear and abstract and orthogonal a la Haskell or other pure languages. Knowledge, life, everything proceeds via evolution, not something a priori. The sooner one really accepts that, the sooner one has new ways to interface with this reality more practically and effectively.
People sometimes make the point of use the right tool for the job - as a reason for still using OOP, etc. I'd not say that -- but that the right tool for the job where the job is an evolving code base is actually something object-oriented with inheritance. Except where the domain is very precise and known ahead of time. Otherwise, the manner in which it models evolution is actually quite useful (despite the fragility of inheritance and the quick ability to spin out of any orthogonal clarity).
People who program purely in Clojure or Haskell put the burden of this evolution in their own development as a programmer (there is no extension, there is just clarity and then rewrite). That's ok. People who use Java or whatever in the enterprise because it's easier for other people to get on board with it -- put the burden in the code. But the cost of modeling a problem that evolves goes somewhere.
The benefit of is-a hierarchies -- perhaps the only big benefit (to be weighed against all the other alternatives available) -- is that it solves for making the next 'adaptation' as simple as possible. It optimizes for the very next extension step. It is slightly less conceptually complex to extend an object than to composite a new feature.
It does that at the cost of complexity and brittleness. But in both the real world and and in source maintenance this is occasionally useful.
Let's say I'm a startup practicing lean and experimentation as much as possible. What I want is a framework that allows me to tweak things with as few steps as possible. What I don't want is to refactor every time I want to do an experiment.
Anyway, code I think generally should model our understanding of the problem space and the real world concepts in play. Extension is not a bad way to begin modeling that understanding.. Because growing the first steps (even though they will clutter because there is nothing encouraging orthogonality in OOP besides one's own sense of cleanliness) is often good for the growth of our understanding.
I have been writing about exactly this topic, of why classes don't make sense (specifically for JS), in my second title of the "You Don't Know JS" book series, "this & Object Prototypes".
In particular, Chapter 6 makes the case for an alternate pattern to class-orientation design, which I call OLOO (objects-linked-to-other-objects) style code. OLOO implements the "behavior delegation" design pattern, and embraces simply peer objects linked together, instead of the awkwardness of parent-child abstract class semantics.
https://github.com/getify/You-Dont-Know-JS/blob/master/this%...
https://github.com/getify/You-Dont-Know-JS/tree/master/this%...
Or does it mean something else?
https://gist.github.com/getify/9895188
For a bit more "real world" of a scenario, you can also compare the `LoginController` / `AuthController` mechanism here, first shown with inheritance+composition, and then shown with simpler OLOO-style delegation:
https://github.com/getify/You-Dont-Know-JS/blob/master/this%...
Yes but we have Single Responsibility Principle and while an animal is one object in physical world, it doesn't mean it should be a single object in OOP. Start breaking it down...
public interface BodyType {}
public class TwoArmsTwoLegs implements BodyType {}
public class FourLegs implements BodyType {}
public interface Locomotion<B extends BodyType> { void walk(B body); }
public class BipedWalk implements Locomotion<TwoArmsTwoLegs> { public void walk(TwoArmsTwoLegs body) {} }
public class Slither implements Locomotion<NoLimbs> { public void walk(NoLimbs body) {} }
public class Animal { BodyType body; Locomotion locomotion; }
Animal human = new Animal(new TwoArmsTwoLegs(), new BipedWalk());
(Code sample from an article in Software Development Journal by Łukasz Baran)
How many people still try to fit their software design into the class hierarchy model? I've been on the composition-not-inheritance side for so long I can't do "traditional OO" justice, but I'd love to hear the counter-arguments in case I'm wrong.
The distinguishing feature of OO is modeling concepts by encapsulating state and behavior. For some reason, introductions tend to focus on complex inheritance structures instead. I think that's really unfortunate because it sends entirely the wrong message about OOP.
That said, I do occasionally use inheritance for polymorphism and limited code reuse (such as [1]), but I keep it limited in scope, and I don't think I've ever used a multi-level inheritance hierarchy.
[1] In my Let's Play TDD screencast series, I use inheritance when modeling a "Dollars" value object. There's a Dollars base class and then various specializations: ValidDollars, InvalidDollars, and UserEnteredDollars.
Source code: https://github.com/jamesshore/lets_play_tdd/tree/master/src/...
The screencast: http://www.jamesshore.com/Blog/Lets-Play/
Superclasses are just a special case of this.
In so far as I have never found a use case for a class hierarchy 5 deep I agree with him. On the other hand the argument that because you can do it wrong means you shouldn't do it all isn't compelling. I don't think there is a language or language feature that you can't use incorrectly.I've found that the delegate pattern, for instance, can be incredibly powerful when used in conjunction with JS, especially when it comes to extending classes functionality that may not be inherent to its topology (think class Ant, to describe an Ant, and class FlyingAnimal -- wings, etc. --, to describe a queen ant).
In most actual OO languages, there is a clearly defined interface between a base class and it's inheritors: eg in c# you have protected (to expose state), abstract to force implementation and virtual to optionally allow implementation. In no way are you forced to expose all internal state (even though people often do).
I think there's a case to be made against class hierarchies, and also against using OO in javascript. But I'm not sure Ragan made either of them well here.
It is true that the natural world doesn't necessarily admit of perfect neat classifications generally, much less trees. However, when we are talking about purely engineered solutions, the same arguments don't apply in the same way.
Here's an example. In LedgerSMB 1.4, we use (shallow) class inheritance in a reporting framework. It works, and works well. Reports inherit an abstract class with lots of hooks for customization but a lot of defaults.
In future versions we will likely be moving away from an inheritance-based approach, not because of the arguments here or the maintenance issues (which will crop up any time you rely on external components) but because we think we can create clearer code by moving from a class/inheritance approach to a DSL approach.
I am not sure that class contracts and DSL syntax contracts are necessarily any different from a maintenance perspective other than the fact that the latter strikes me as resulting in clearer code.
FWIW, I noticed that in my old C++ book (Stroustrup '91) there's some really bogus inheritance examples - window -> window_w_banner -> window_w_menu -> window_w_banner_and_menu etc etc etc.
I don't think Stroustrup really gets OO. I have yet to read a book of his that doesn't have some absolute howlers.
It is mentioned in the GoF Patterns book[1], from 1994/1995. that's what I think of when I hear the phrase.
Now, look at all these bad things that I've done! NEVER DO ANYTHING SIMILAR EVER."
That pretty much sums it up.
Of course inheritance is not a solution for everything. Avoid using deep inheritance hierarchies, you'll paint yourself into a corner. It's advisable to prefer composition over inheritance. You should not be that guy who only has a hammer and everything looks like a nail to him. But it doesn't translate into: "hammer? don't do that!"
All the examples he's giving either demonstrate abusing the inheritance concept or just show that it has certain limitations. There are well known solutions and guidelines for dealing with problems such as fragile base class, other than throwing the whole paradigm out of the window
That completely chimes with my experience. I wondered if I was doing OOP wrong, as any time the size/complexity of a project (or module) gets above a certain level, a Thicket results in my code.
> Classes are the wrong semantic model, and the wisdom of fifty years of experience with them is that there are better ways to compose programs.
Where's your source/links? What? This needs expanding - if there are better ways, outline your evidence and show us where we're going wrong :)
Probably is the go OO-model?
If you implement an interface using delegation, you get something quite like subclassing, except that the subclass-superclass interface is explicit and the superclass's type is entirely hidden. Sadly, few languages make this easy so it often requires some boilerplate or meta-programming.
Since C++ doesn't have interfaces, only pure virtual functions, you'll pay the cost of the virtual dispatch, even if you use delegation, won't you? (The context of my comment is the use of interfaces, with implementations using delegation instead to do implementation inheritance)
e.g.
interface I
class Concrete implements I
delegate to Shared
class Concrete2 implements I delegate to Shared
Yes, if you don't use any interfaces at all, and just a concrete class, then it's a different situation.A JVM can have fast or slow performance depending on JVM flags, warmup time, and the phase of the moon.
For 80% of apps, that's just fine, but would you write a mobile web browser in Go, or say, Call of Duty? Seems you'd be leaving 50% performance on the table in some scenarios.
I tend to give 'systems programming language' special status. They're how you achieve maximum system performance if not writing in assembly. You write kernels, device drivers, virtual machines, and games in them. If they are imposing severe performance or memory overhead, then they are application programming languages, not systems programming languages.
Everyone who has spent enough time with OO and classical inheritance knows the problems, but most have never seen how to convert a mess of classes and inheritance into a more functional approach with composition.
Don't get me wrong, there was good content here, but I was hoping that after the conclusion, the post was going to go into how to use composition as an alternative.
data Event = PlayerDied Player Reason | PlayerMove Player Coords | ...
and use pattern matching on the event type. In C I would have used a combination of unions and structs with an event type. But how to do that in Java? I ended up with
interface Event { public update(Game g); }
and used e.g.
class DeathEvent implements Event {
DeathEvent(String player, String reason) { ... }
update(Game g) {
g.killPlayer(player, reason);
}
}Combined with a parsing function (public Event parse(String line) {...}) I could read from the socket and update the game in a convenient way, but to be fair I used that mostly because Java guys discourage you to use instanceof although it seemed clearer.
So is this the preferred way to do something like this? I think "they" (the OOP warriors) call this the Visitor pattern. However I really find the data-type encapsulation in Haskell and other languages (in Python a tuple (type, object) would do) superior. But maybe my Java just got rusty.
I think this highlights a problem with javascript more than OOP: We're trying to fit it into uses cases that are simply too complex for its design. It wasn't made to build your goddamn bank accounting system, it was made so that "nonprofessional programmers" could animate things on websites.
But it also highlights a problem it doesn't take about: Language in computer science and how it affects how we think about things.
In this instance, public and private are terrible names. They had to tell us in programming classes that they're not related to security, which means that the privacy metaphor is a terrible idea because it's not instinctual.
This in turn causes us to shoehorn class design into things they perhaps shouldn't be. At this point, for complex programs we should be describing things in a much more complex way than "accessible from the outside or not". As the article points out, it doesn't matter that the classes are external, because you can just as well break things from the inside.
This is a complex problem, but I think the beginning of a solution is to a) Depend on meta-information (or better implement a flexible, non-arbitrary "access" structure) and b) Use the right tool for the right job, in this case not JS.
https://en.wikipedia.org/wiki/Data,_Context,_and_Interaction
As mentioned at points in this thread, some objects need different behavior based on their context. DCI is a thought provoking way to represent that (though one that not in common use and that is often described as "being done wrong" on the object composition mailing list[1]).
[1]: https://groups.google.com/forum/#!forum/object-composition
The truth is that there's no silver bullet. You can build software based on hierarchy of classes because your mind works better with that structure, but others may find it completely inappropriate. We're human and our mind works differently from one another.
In that sense I truly believe software is much closer to art than engineering.
Best intro I've read on the topic I'm talking about is from Alex Sexton (just read the last example, it really hits the nail on the head):
https://alexsexton.com/blog/2013/04/understanding-javascript...
So you might want a wee bit of hierarchy, if you're thinking like those "shared options" scenario, but not in the "OO type abstraction tree".
So, you might have something like a "Account.prototype.primeInterestRate" property that you can change in a running program, and then all the other types of account can calculate interest based on that shared property.
However, the more experienced jS developers I've met might take those "Account.prototype.balance" and "Account.prototype.deposit" methods, and push those into a "mixin" type (like "CurrentBalance") where those methods are copied (not inherited) onto the child class prototype, and those methods, might have initializer helpers to set up the "currentBalance" property they use. This mixin approach only gets gnarly if there's any feature envy. (Document your object properties clearly, folks. This is where javaScript's flexibility often becomes a crutch - lots of issues happen if mixin code uses "this.foo" for different things.)
Anyhow, what's interesting here is that Account carries the property that's shared, but CurrentBalance carries "behaviors", and is not shared, and your "child classes" like VisaDebitAccount embed both in different ways. It is a very different way of thinking about object relationships, and often works smoothly.
But if you're using classes in JavaScript like you would Java, well, then, you're not really using JavaScript, right? And, this whole talk about biological-style ontology just becomes the wrong metaphor, because while "humans are a primate" we can't change aspects of primates to add behavior to people!
On the other hand, defining all the interfaces all the time, like in java was painstaking. I hope for some sort of middle ground.
Programmers are still inclined to use hierarchies. They are implicit to source code in a more general sense, with indentation, nested logic, etc. But we also need affordances to break up hierarchies and keep them limited.
ChequingAccount.prototype.process = function (cheque) {
this.setBalance(this.getBalance() - cheque.amount());
return this;
}
...Isn't good, easy readable code look very similar to requirements it was written upon?
I can't remember one class hierarchy more than one level deep that was worth it.
They save a little code - true, but at the cost of coupling, making it harder to change, and forcing early debatable decisions on programmer (which classification is more important and goes first for example).
For more quotes see :
http://harmful.cat-v.org/software/OO_programming/
Another, seeing as it's PG :
"The phrase 'object-oriented' means a lot of things. Half are obvious, and the other half are mistakes." — Paul Graham
In other languages, especially Java, class hierarchies are a matter of self-esteem.
Javascript programmers could do worse than emulate Python in this regard.
I don't mean to pick on vermooten here; lots of HNers post comments like this. Please don't post comments like this. Re-read what you post and, if it contains sarcastic sniping, edit it out.
I'll be pointing out examples of what's good and what's bad on HN, in the hope that the feedback will be helpful to the community. When I do that, I hope everyone understands that it's never personal, only about the content and only for trying to make HN better.