Ask YC: What exactly is so hard about OO?
I've been feeling a little confused lately, given the litany of blog posts heralding the return of procedural programming or detailing how the layman programmer incompetently uses object-oriented languages.
To me, the concept seems somewhat straightforward, so I have to ask: What exactly is the difficulty people have with grasping OO programming and using it to construct cleaner, more maintainable code? What are the common mistakes? Am I unknowingly using OO wrong?! Are we ALL??
I think that like any programming technique OO has it's place, and certainly not useful in many circumstances. I also don't think that it's an incredibly complex concept for a programmer with a little experience, and can be quite useful when implemented properly. Please correct me if I'm wrong.
60 comments
[ 297 ms ] story [ 647 ms ] threadLast year I learned Lisp, now I understand that I've only now mastered OO and that Java has some major limits (such as single-dispatch).
There is a real problem in that different people (e.g. the me from college vs. the me from now) have very different and incompatible ideas of what OO programming means.
For example, it's much easier to go from a concept like "I can use the SAME NAME for DIFFERENT FUNCTIONS in the SAME PROGRAM, and there are various ways to map useful semantics onto the resulting code" to "If I resolve the ambiguity at run-time then it's POLYMORPHISM and if I do it at compile time then it's OPERATOR OVERLOADING" than it is to start with the terms POLYMORPHISM and OPERATOR OVERLOADING and try to learn what they mean and what they are good for.
Frankly, multiple inheritance, and multimethods, are nice tools when you need them, but I'd say that most of the time, you don't. (Let me clarify that. Doing what Java does with multiple inheritance of interfaces IS very important, and it would be very bad to leave that out. What Java cannot do, namely multiple inheritance of implementation, is much less crucial. And you can very often get around it by using delegation. See "The Treaty of Orlando" which asserts that multiple inheritance and delegation have the same inherent power. (This was formulated at an OOPSLA conference in the 80's; Henry Lieberman was one of the main authors.)
Anyway, my point is that Java does indeed have limitations, but the fundamental concepts are very similar to those in CLOS.
I prefer CLOS over Java because of the list of features, but the point I was trying to make is that different people (and even the same person at different times) have different ideas of what OOP is. This leads to funky designs and all kinds of messes.
It's particularly amusing as a Lisp programmer to watch all these fads come and go because none of these ideas are new. The OO features of Java and C++ are just tiny subsets of the functionality in CLOS. XML and JSON are nothing more than (bad) re-inventions of S-expressions. Aspect-oriented programming is just a hack to get around the fact that Java doesn't have macros or multimethods. And on and on it goes.
The right answer, of course, is that OO (and every other programming technique) is good for some things and not other things. Figuring out what things a particular technique is and is not good for is a big part of mastering the art of programming.
Ahem.... right. Your response sounds more complete than mine below. :)
"It's particularly amusing as a Lisp programmer to watch all these fads come and go because none of these ideas are new."
The takeaway from this is not that programmers are stupid and we should all be using Lisp, but that user interfaces matter. Social conventions matter. Installed bases matter. After all, most of Web 2.0 is just fancy rounded-corner reimplementations of 30-year-old UNIX utilities.
C succeeded because in one critical area - creating a fast, responsive UI on the hardware & compiler technology of the early 1980s - it worked and Lisp didn't. C++ and Java succeeded because programmers could take their C knowledge and syntax and apply it.
XML succeeded because it could leverage everyone's knowledge of HTML. HTML succeeded because at it's heart, it's just text - you could start typing a plain old text document and it was valid HTML. A good part of JSON's success is because every JSON document is also legal JavaScript and legal Python.
Aspect-oriented programming generates interest because it's in Java, and people already use Java. And on and on it goes...
On purely technical grounds, the Lisp solution from 25 years ago is almost always better than the modern-day solution we're just rediscovering now. But the modern-day solution has the benefit of building on top of all the social conventions that have arisen in the last 25 years. That's worth remembering, particularly in a forum devoted to entrepreneurship. If you tell your customers "our solution is better, but you'll have to throw away everything you already know and start from scratch", they probably won't remain your customers for long.
Over on the Lisp lists I sometimes make the point that Lisp failed precisely because it was so powerful that one person could be productive enough to get useful work done by themselves. As a result, Lisp tended (and maybe still tends) to attract people who don't work well with others, whereas if you program in C you have no choice but to work as part of a team if you want to get anything done at all. At the end of the day teamwork wins, even when hobbled by inferior tools.
That probably tells more about the people and environment of MIT AI Lab, Symbolics and ITA Software than the team dynamics of Lisp.
You do not need to throw away everything you know to use Lisp. 90% of what you have spent your time on when you learn programming applies to Lisp just as well as any other language: variables, procedures, iteration, and on and on. At ITA, when we hire new hackers who don't know Lisp, we just give them a copy of Peter Seibel's excellent book "Practical Common Lisp", and they quickly pick it up.
Common Lisp, as it is used in real-world practice, isn't nearly as strange as many people think. (Yes, we have a job to do to explain that to the world.) It's not a new paradigm the way Prolog or Haskell are. You can get started, doing the kinds of things you do in Java, very quickly. Now, learning all the more-powerful stuff, like Lisp macros and how to effectively and tastefully use them, and more advanced object-oriented features of CLOS, does take some time to learn. But, then, learning all the useful stuff in the Java libraries takes time to learn, too, even if you're the best C programmer in the world.
I think Java is particularly bad in this department, since I think it often really encourages with the "Let the IDE handle it for you" mentality.
When you want to run water to your house, you don't hire someone to design new pipes for you; you use the ones pipe companies kick out. The boilerplate is just plumbing your IDE manufactures for you. The only difference between it and the magic that happens in dynamic languages with stuff like RoR is that boilerplate code is explicit rather than implicit. I prefer the former, truthfully, so I don't have to understand the whole stack to work on a small piece of the code.
* What the valid arguments are
* What will be returned
* What, if any side effects calling the function will cause
* (sometimes) What resources the function will utilize
Use of most other data types is simpler. Your framework or library should give you this information in the documentation. If your development environment is reasonably friendly, it will give you the arguments, and in many cases, you'll be able to guess the rest of it based on the function name.
If you have to RTFS to figure out how to use your libraries, something is wrong. Institutionalizing that by always putting the source right in front of you is a Bad Thing.
To make this concrete, take the example of implementing getters for a class in Python. If my class has four fields
I can do something like this inside the class to implement getters: Or I can do something like this: The latter does a good job of eliminating boilerplate, but at the cost of making it near impossible for me to find out how get_a() is implemented (assuming this was buried in a huge codebase).Just play along and imagine a more complex world. If it makes you happier, pretend the getter is intended to return the string version of the numbers or some other operation that would make just using instance.a wrong.
Think of this as generating code using a UI, except you record the minimal UI commands, rather than putting the full output into your source files. The commands themselves are still explicit, it's just that they contain only the interesting information with the minimum amount of boilerplate.
There are methods to create read-write and (I think? write-only) properties as well.
A simple example of this is generating getters/setters. Autogenerating the code works great 99% percent of the time. The problem comes in for that 1% case where a non-templated getter or setter is needed. That special case is easy to lose track of in the forest of automatically generated code. Groovy solves this problem by automatically generating all of the getters/setters for you, but if you need to you can still override the automatic definition with a specific implementation.
I won't argue you with that too much magic can be a bad thing. However, for the code I typically generate with my IDE I really think I'm wasting time and bloating my code.
I will assert, though I can't demonstrate it, that had this been done in Lisp, the boilerplate problem could have been substantially alleviated by using Lisp macros. One piece of evidence: J2EE has improved this problem substantially by using Java's relatively-new "annotation" feature, which, broadly speaking, has some of the same mojo as Lisp macros, when used properly.
On the other hand, you can do a lot better even in Java. Look at Spring and the other new post-J2EE frameworks. The experience with J2EE has led to learning and improvement in these new "dependency-injection" frameworks, which is definitely a good thing for the Java world.
Lisp Macros are boilerplate without the duplication. (And so without the drawbacks)
The idea that programming is not hard or that X will save us from the fact that it's hard, where X is OO, FP, structured programming, DSLs or even Lisp is an insult to the trade. Worse, it applies pressure to stop advancing - after all, X is going to make this easy any day now.
I suppose I should revise my question a bit. What I really meant to understand was what was it is about OO that makes it a focus of conversation as a hard technique, when compared to all these other programming techniques you hear little no arguments about.
For all its usefulness, you don't see too many blog posts vehemently defending or refuting recursion. It's simply another way to write some code. You don't hear joel or jeff going on about whether or not data structures are useful or on the whole a degradation to programming communities. These are all just different techniques used to accomplish the same thing; in some cases, those techniques make achieving a goal easier or the code more elegant.
I do hear these things about OO programming, and yet, OO is just another one of these techniques! So, my question now becomes - not what is so hard about OO - but instead - what is so arguable about OO, that people feel the need to call it hard, useless, or revolutionary, while leaving the other techniques like poor recursion out in the cold?
"Green field" programming is easy. Hell is other people's code.
On the other hand, if people start writing up and passing around CRC cards, they tend to produce better designs. My conclusion is that GUIs have too much baggage from documents. In some important way, they are much worse than 3x5 cards that you can pass around. Once we figure this out, some aspects of programming will improve.
It will probably always be hard.
I am probably spoiled because I did it for too long, but I find it really hard these days to get by without objects.
I took up Scheme programming again, and I like it. I tried to keep it simple and avoid objects, but I find myself missing them all the time. Writing complex getters and setters with car and cad into deeply nested lists does NOT seem simpler.
I wish some of the functional programming gurus would write tutorials on non-object-oriented programming, instead of ranting against OO programming.
var tableData = ... var listData = ... var textData = ...
and then add callback methods (all in the same source file?). But that runs out of hand real quick. It seems much nicer and cleaner to have a treeView object that knows it's own data and callback methods.
What if you have nested elements, like a window with those three elements, or something else. How would you hand that data around?
I have seen the make-struct macro of MzScheme, but once you start using inheritance with that, it starts getting really ugly, imo.
Good point. I missed this when I first left Java, because in Java you get these nice classes. Over time I've found I use class structures a lot less, or in some situations I'll hack them. The other day I was writing a tree builder where in java I would have had a different class for "node", "attribute", etc, all extending from element. In my python impl I just had a class "element" with a string field "e_type" which was "attribute", "node", etc and then I stuck other stuff into a dictionary in that object as I needed it. Over time I've found that the extreme brevity improvements combined with blocks of documentation to describe the purpose of a grouping of code more than make up for the loss of code-based structure.
Again on the topic of organisation of data - in databases you get a lot of automatic documentation because the schema abstraction we're used to is so widespread and accepted. This is one of the reasons I love to stick with relational databases even though I know that there are good arguments, particularly in FP communities, advocating more practical forms of datastore.
I don't think OOP is that hard to learn, but I do agree that learning to use it very effectively in the best and most tasteful way takes time and experience. But that's true of so many aspects of software design; I don't think OOP is all that different in this regard.
Absolutely correct.
and certainly not useful in many circumstances
This is what, infuriatingly, well over 90% of CS graduates are missing.
The OO patterns movement probably had something to do with it. It's easy sometimes to get the impression that with OO, you have to learn a bunch of weird patterns to produce good code, but with FP you don't have this conceptual overhead -- you just write the solution that feels the most compact and natural, almost in a mathematical sense.
Where OO does usually make life harder is in concurrent programming. Because objects have mutable state and hidden fields, it's difficult and often dangerous to send (and receive) them between processes. In Erlang, for example, all data is immutable and because it's made of simple (lists/tuples of) primitives, it's very easy to send it as messages (to processes on the local and/or remote VM) and to pattern match against it on the receiving end. This makes concurrent and distributed programming probably as easy as it can be.
I also find code written in function languages easier to read and debug because of data immutability. In code written in functional style, you know exactly where every variable is bound, so it's easy to track down bugs that cause it to have the wrong value. There's no mystery as to where a variable's data may be modified, which gives you confidence in the correctness of your solution.
I don't mean to suggest that FP is perfect and OO is broken. Sometimes when I write FP code I wish I could grab one or two concepts from OO. OO code can also be quite elegant as well.
OO is simply organizing your code before/as you write it. Combined with UML, it can be a kick-ass way of describing your general strategy for dealing with complex problems. Or it can take something simple and make it into a monster -- it's up to you.
Another way of looking at OO is that you are building your own language as you go along by starting with nouns (types) and adding all sorts of verb-clauses to hook them together. You can do this in a super-cool, easy-to-understand way, or you can get a bit carried away and try to recreate the dictionary when you only need a few nouns and a few verbs.
OO's goal is not tight, beautiful, concurrent code. You want to feel like Picasso or Spock, go write FP.
For all of those reasons, OO just isn't as sexy as FP. You're not writing something that scales to a zillion users right out of the bag. You're not doing a lot of meta-programming, recursion, lambda calculus and such. You're not writing anything sneaky, clever, or bound to impress the other nerds. Everything just looks plain Jane. Add to that all of the examples of bad pattern usage and other atrocities in the OO world and I can easily see why other ways of doing things can seem more attractive.
Some problems decompose naturally into functions.
Some problems decompose naturally into procedures.
Some problems decompose naturally into objects.
OO has extremely poor fit with functional programming at a fairly deep theoretical level. Procedural programming is often very useful for doing synthesis on a data set. Object programming is often very useful for people doing stuff to their virtual "things". The problem is when languages anchor to one abstraction or the other at a fundamental level (ruby) programmers tend to think they have to follow along.
In truth, it's about finding an abstraction (or set of abstractions) that fit the problem at hand. A lot of what makes OO hard is cases where one really needs to break down and say "but this bit? this bit is procedural" and having the experience to be confident of dropping the OO approach at those points.
One reason Lisp has survived so long because it is capable of absorbing so many of the good new programming ideas as they come along. (No, it's not perfect this way, but it's pretty darned good.)
I think there is a great need for a multi-language framework, because different languages have such disparate power depending on what you are working on. I keep on thinking back to Rob Pike's Google Tech Talk on Newsqueak. He spends 6 months developing a language that does concurrency at a high level. After that, he writes a windowing GUI system in two hours.
What we need is the ability to support disparate language semantics and glue them together easily. It would be great to be able to model your business logic in Smalltalk, but write the GUI in something like Newsqueak.
Maybe Richard Stallman's original idea with GUILE was on the right track?
With each level of indirection, you get one more level of sophistication, but at the cost of one more level of complexity. OO has tons of indirection (+cough+ polymorphism +cough+).
Back in the old C++ days, before decent IDE's, I remember tracking through multiple code files in several different directories, just to figure out if an add operator had been overloaded - and if so, how. Man, was that a complex pain!
Lately, with Java, I've found myself in the same situation, but with XML config files. Some Java developers just love XML config files and often use them to direct reflective code execution (dynamic language envy). Needless to say, my IDE's are failing me again.
I think the problem is further exacerbated by calling some languages "OOP" and others functional, or procedural, or whatever. Fundentally a programming language is none of these things. It is possible to program in a procedural style in java (make everything static or singleton) and it is possible to program functionally (but the syntax is very cumbersome). Similarly it is possible to program procedurally in lisp (do, and, etc) and CLOS. Some languages are more enjoyable to program in one style over another but if, like a lot of career programmers, you think programming in java means you are writing good OO code and because you don't care about the quality of the code you write, you just want to do a good enough job not to get fired, and because you know someone else is probably going to have to maintain it anyways, you write the code in the easiest most quickly completed way instead of the best overall way when considering debugging and maintainence times.
The only difficulty with the above approach is team working. In a team, you have to find common grounds to base your thinking on.
So, I never ever use OO, but maybe the only real reason is: I don't need to work in a team...