I think there's some confusion about the analogy being drawn here. Biological classification is, like you said, a question of shared implementations. That's useful and it works especially well in biology because organisms are largely built through specialization; i.e. it really is a tree, where ancestry groups organisms by shared genetic material.
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.
> Software is designed; you don't (or at least shouldn't), construct your programming abstractions by progressively mutating the different parts of your 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).
> Is this another ad for big design upfront, aka waterfall?
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).
> 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. ... You have more tools than that, so as a metaphor evolution as a designer can be described by a simpler ontology.
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.
I'm not an absolutist on any of this either, and in practice I make classes inherit each other when it's convenient and makes my code work well. And it sounds like we agree that using mixins is a powerful way of avoiding the ontology traps that lurk in class inheritance structures, and that not having them is painful and limiting [1]. So I'm not even sure there's a real disagreement here.
[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.
In C#, you define an interface, and in the same namespace as the interface, you define a set of extension methods to the interface. Any class that implements the interface gets the extension methods. The interface can even be empty.
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
}
}
}
I find myself not using extension methods so much these days unless I want behavior for a specific instance of a generic type. The problem is state and polymorphism, which can be handled well enough via delegation to a nested mixin object + an interface to tag objects that have this nested mixin object.
With C# borrowing "everything must live in a class" from Java, extension methods are the only way to do sane functional programming in C#. That, LINQ is developed entirely through extremely generic extension methods. Indeed, extension methods were added to C# to enable LINQ, which was created to drag .NET developers over to FP.
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.
The problem is the types. Those methods do know how to handle 0-sized collections, but they must be operating on nullable types.
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))
Ah, yeah, I'd forgotten about extension methods and how they could be used as mixins. I did indeed switch to that when C# 3 came out. I guess I was just remembering earlier pain.
It's not a crime, but Allman bracing is normally used. Every official guidelines, every programming book, every open source project I know of, uses Allman for C#, so anything else stands out like a sore thumb. It's about convention, consistency and resulting readability. I'm not a fanboy of bracing style :) I think that in Rome you should do as Romans do. When I use Java or PHP, I use K&R - it's a context thing, it would just look weird and out of place otherwise. I believe it's better to embrace the "native" (common) coding style in each language, unless you only ever work on your own.
Allman bracing sucks on wide screen laptops, and it was only when I found the 3-font point brace line Visual Studio extension that I was able to switch. I'm really getting annoyed with braces, we should get rid of them.
Python replaced them with indentation. Funnily enough it is one of the most complained about things with Python. I hated it at first, and its still a pain when switching between JavaScript and Python, but you never get the mismatched brace problem which more than makes up for the problems.
Fair enough. I just compose my mixins directly and bridge the gap through delegation. I wish these rants included the obvious solutions, I'm really tired of the argument of OOP sucks because...Java. Or OOP sucks because...single inheritance. There are plenty of things we can do to make our languages not suck, and this is not a fundamental limitation of OOP.
"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."
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.
"Is this another ad for big design upfront, aka waterfall?"
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.
Anecdotally: The best projects I've ever worked on (happy clients, met budgets for time/cost) had the most up-front planning and thought on the requirements.
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.
I don't disagree that encapsulation has a place, but it's not the universal right answer. Sometimes it makes sense to use inheritance, and sometimes it makes sense to use encapsulation.
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.
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:
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.