I don't accept your breakdown of what classes are "nothing but". You've omitted at least one critical thing: the name. But even if the list were complete, it's a fallacy to say "if you dislike the combination, you must be opposed to one of its components".
(The name is critical because the human mind leaps irresistibly from names to things, something you appear not to be considering. But I already wrote about that.)
Organizing code, making it nicer, reducing complexity? all I can say is that my own programs got far better when I stopped organizing them into classes. They became shorter, easier to write, easier to test, easier to change, and more fun to produce. But we're in YMMV territory here. I do feel like stating, though, that I worked for years in the style you describe. I even taught it. And I said and believed many of the same things. Should that count for something? Maybe not. Maybe I just got bored and went off.
Encapsulation? The most grossly overrated allegedly simplifying mechanism ever. But "any code could be modifying blah"? Any code can do a lot of horrible things. Trying to rely on technical constructs to prevent it just adds weight and bloat and impediments. Let's have flexible languages and be good programmers.
Difficult to reason about? The hardest code I've ever tried to understand has been complex object models where Thingy depends on Fooey which needs a Bingy and a Batty, and to construct a Batty you need a... Compared to this sort of conceptual glob, bad procedural code has always, in my experience, been easier to understand.
Inheritance vs. composition? Not relevant here. Maybe I should have said "graph" instead of "hierarchy". Whether A "is" or "has" a B, that's still an edge in a graph, and it's those object graphs I'm talking about. They are much harder to rework than functions. The trouble is that when we believe that programming is making object graphs, we assume that difficulty to be part of the problem and don't notice it.
Rewriting? You rewrote something to be clearer and more intuitive to you and maybe to others who share your beliefs. That it happened to come out in objects is a consequence, not a cause, of what you find clear. It's a lot harder to judge clarity across assumption sets. For example, I work a lot in JS and defining JS objects is the one thing I never do. Maybe if each of us put our code in front of the other we'd recoil to exactly the same distance :)
I'm glad to see this discussion on HN, because I've always wondered if there was something I was really missing regarding OO programming and whether or not coding in that style exclusively would make me a better programmer. Obviously it's all subjective and somewhat dependent on the task at hand, but my concern is probably more with whether that "style" of thinking is beneficial.
Then, fairly recently, I dabbled in some OO programming when I was making a mod for Unreal Tournament (I've also coded in C++ in the past, but not extensively); and I realized that I had already been using the same line of thinking with my functional programming in the way I was using structs, tuples, etc. When you really get down to it, it's really all just syntax and semantics.
For whatever reason, my brain has always naturally leaned towards the functional style; and it's just my opinion (of which gruseom seems to agree) and my opinion is probably only the result of how my brain works, but something about OO programming always seemed unnecessary for me. It seems to me like it's just another way for people with different thought processes than my own to represent their program logic. To me it seems a little too verbose and entirely too repetitive, and I always felt like the overhead required to "classify" everything wasn't worth it; but it's all in how you look at it, how your brain works or what you're used to. So what seems unnecessary to me (and makes it harder for me personally to trace the program's logic) might be perfectly comprehensible code for someone else; and some of the relatively minimalistic code I attempt to write might seem perfect to me, while an OO-minded programmer might feel like it's scattered about and hard to understand.
Good code is good code, regardless of the paradigm.
> I dabbled in some OO programming when I was making a mod for Unreal Tournament
> but something about OO programming always seemed unnecessary for me.
I actually learned to program making mods for Unreal Tournament (1999). I'd love to hear more about your feelings about how OO seems unnecessary.
For me, UT was a prime example of OOP (both the general bundling data and functions together as well as inheritance) being used to simplify things. Key is that any instance of 'actor' is a single physical object rendered in the world. A subclass of 'actor' is "inventory" - items that can be picked up and held by players. Below that is the "weapon" class; which a player can switch between, fire, and are rendered in 1st and 3rd person views.
Obviously you don't need to be OO to pull off a game; many FPS games are not. But it makes things so easy to handle. If you build a new weapon, all of the functions to handle picking it up, rendering it, etc. are written in super-classes. But if you want, you can over-ride those. Furthermore, the weapon interface is well-defined to external entities; e.g. the 'playerpawn' knows his weapon has a 'fire' method.
I'm not sure how you could build an equally powerful and customizable engine without using OOP techniques at some level. Sure a weapon could be a struct, but how would you know what associated fire() method you would call? Unless you want a bunch of non-extensible case statements, you'd need a function pointer in the struct itself.. and once you couple the data and methods, you are getting close to OOP.
One thing I loved about UT compared to Quake-engine games was its extensibility. OOP allowed reflection; just 'spawn weaponpackage.weapon' and you can use a custom weapon. This also allowed gameplay "mutators" to blend together. Quake games in the day required a custom -game argument at startup; and consequently custom content could not easily be merged together.
Of course, this is how I learned to program so my brain may be overly-wired that way.
As someone who works in games, I should point out that the industry has rejected the hierarchical OO model in new engines because it doesn't remain maintainable at scale.
The influence that is driving the new stuff - component systems and composable entities - can be seen as derivative of the actor model. Actor models go beyond what is present in most game engines, which tend to stay well on the imperative side of the line. It takes a heavy dose of purity to grok actors in full, but it dovetails well with functional style as an alternative view of computation. (I also recommend the Tim Sweeney POPL06 "Next Mainstream Language" talk to see exactly what problems are motivating the changes)
First, aim to never store values on objects - prefer instead listeners that return values independent of the objects themselves. Suddenly you start to enjoy "code=data" just as in Lisp, because the semantics of getting data and processing data are identical and interchangeable - you can take a getter on one object and copy it to a different one, and they'll return the same data, even if their other methods are completely unrelated!
Decoupling occurs naturally in such a system - instances can be as similar or different as desired. The usage of classes shackles these abilities into monolithic, heavily-coupled data structures again, which is the core point of difference from "OO" as we know it in industry.
If you also stay purely asynchronous at all times, there is no longer a difference between "now" and "later" to the callee, so order-dependent operations can naturally transition from one-to-one mappings into arbitrary ordering and queuing. Pure asynchronicity was lost when actor-style systems first tried to enter industry(e.g. Smalltalk-80 opted for a synchonous system - a concession to single-threaded performance), but it's come back into vogue in this heavily-parallelized era.
Fortuitously, it's straightforward to achieve these properties in languages that support closures and reflection. And you don't have to write entirely in this style; raw data structures can be written imperatively, and then tied together using actors. The listeners can access a common database to look up values. You can mix and match.
State-of-the-art game entity systems aim to cut down this level of power into a palette of performance/power options for composition, but if you can live with high overhead, taking all the power is pretty simple to do. I wrote a prototype for it yesterday - 169 lines of Haxe.
As a person who has written games, I think games are one of those weird cases where OO really shines, but I don't think it's representative of most general programming problems. In my experience OO makes a ton of sense when you have a lot of mutable state that actually represents a tangible real world object or metaphor. So that works great for games, which are all about simulation, and where you actually have a lot of things that really do map to an "object".
But, outside of simulations, a lot of programming problems come down to things that don't have a convenient real world mapping. A lot of it is just data transformations, and data transformations are awkward in an OO setting, because the focus is on the object rather than the process. This is where more functional styles really shine, (for instance, in compilers and such), where the existance of an object would be incredibly transient and short lived, because the goal is to transform one set of data into another representation. OOP basically sucks at this.
Objects are certainly useful, but I think they've been fetishized to a weird degree.
OOP works very well for UI code. I got introduced introduced to it in that way, back with Turbo Pascal. When you bought a compiler you got this wall-filling poster with the class hierarchy. Great fun. Nowadays, this is still reflected in toolkits such as Qt.
It also tends to work for things that can be represented as opaque handles (such as files, sockets, ...). Polymorphism helps there to be be able to treat different but similar objects in the same way. No need for deep, nested hierarchies there though.
But I agree that outside the domain of UIs it quickly diminishes in value.
I agree, I think where we went wrong is in these one size fits all type designs. I like OO and events for UI, I feel that it is more powerful than other paradigms like say controller / template. I think black box widgets that respond and emit events and that can be inherited work in a manner that reflects my thought process. For example I always use a select box, where you have the standard select box and via extension or mixing in, you can create a filtering select box that adds the behavior of filtering type ahead. To me this works very well and promoted loose coupling of the interface as well as reusability of components.
Once I move past the UI though I really start to feel the burden of OO start to drag me down as far as productivity goes. It requires a lot of organizational overhead to force overtly service based functionality into OO structures, something like getUserList is functional at it's simplest form and should remain so. When working with Java I tend to discard OO by creating static functional classes that acts as a service and util layer. I find that doing so simplifies the architecture of the system and eliminates a lot of spaghetti code that has to be written to deal with making functional logic OO.
For that reason I tend to prefer languages that can do both, or that don't force a pattern on you at all, and that leave it up to the library developers to work out patterns. JavaScript despite it warts is a good example of the library developers building out that layer of the language. I also find myself writing more and more back end code in Clojure for that reason, while it is a functional language it does not preclude the ability to write OO code if it is the right pattern for a particular problem set. I like not having to fight the language to break free of the constraint.
> the focus is on the object rather than the process
From a business apps point of view, this is exactly it. You might find that methods are subordinate to objects, but in the business domain itself, the objects themselves are manipulated by processes. It's a whole other layer above the OO layer, and failure to realise this means you either end up with lots of FooManager classes, or you push overarching responsiblities back down into low-level objects that don't really want them, and find you have too much coupling and not enough Demeter
(Of course, you might be writing FooManager classes with this upper layer explicitly in mind - in which case fair enough and you'll probably like DCI. But I think there's still a lot of mileage in plain ordinary functions that don't have to belong to anything)
I learned a lot from Quake 1. QuakeC [1] was not object oriented, although it had one struct type: an entity. The entity struct had function pointers that were used to attach handlers for various events (touch, think, etc.), but there weren't methods per say, or a class hierarchy. The code was very procedural. You could not define new types, and could only make limited modifications to the entity struct's definition.
This restriction was often frustrating, but limited types meant the language was very approachable for beginners. Once a programmer understood the entity and its attributes, they knew what they had to work with. They only needed to find interesting ways to transform data, not interesting ways to organize data.
While sometimes frustrating, the simple type system rarely limited the creativity of developers. This is evident in the wealth of mods that were created for the game. People built Diablo style RPGs [2] with it, racing games [3], and much more.
In Quake's sequels, the modding system grew more flexible (and also more complex). The number of mods seemed to decrease. The amount of mod content for Quake 2 was less than 1, and Quake 3's was far less than 2. The mods that were released were often far more polished, but I think this is representative of the skill level and determination that was required to actually build mods for those games.
Garry's Mod [4] is only modern game I've seen with a similar level of successful modding activity. It also uses a scripting language (Lua). Garry's Mod defines a strict set of structs that you use to interact with the host game engine (effects, entities, tools, and weapons). It feels very Quake-like.
I personally feel like these kinds of restrictions create flatter code, and structure that is easier to hold in my head. I find myself concentrating more on features, rather than on code organization.
I don't accept your breakdown of what classes are "nothing but". You've omitted at least one critical thing: the name. But even if the list were complete, it's a fallacy to say "if you dislike the combination, you must be opposed to one of its components".
(The name is critical because the human mind leaps irresistibly from names to things, something you appear not to be considering. But I already wrote about that.)
Organizing code, making it nicer, reducing complexity? all I can say is that my own programs got far better when I stopped organizing them into classes. They became shorter, easier to write, easier to test, easier to change, and more fun to produce. But we're in YMMV territory here. I do feel like stating, though, that I worked for years in the style you describe. I even taught it. And I said and believed many of the same things. Should that count for something? Maybe not. Maybe I just got bored and went off.
Encapsulation? The most grossly overrated allegedly simplifying mechanism ever. But "any code could be modifying blah"? Any code can do a lot of horrible things. Trying to rely on technical constructs to prevent it just adds weight and bloat and impediments. Let's have flexible languages and be good programmers.
Difficult to reason about? The hardest code I've ever tried to understand has been complex object models where Thingy depends on Fooey which needs a Bingy and a Batty, and to construct a Batty you need a... Compared to this sort of conceptual glob, bad procedural code has always, in my experience, been easier to understand.
Inheritance vs. composition? Not relevant here. Maybe I should have said "graph" instead of "hierarchy". Whether A "is" or "has" a B, that's still an edge in a graph, and it's those object graphs I'm talking about. They are much harder to rework than functions. The trouble is that when we believe that programming is making object graphs, we assume that difficulty to be part of the problem and don't notice it.
Rewriting? You rewrote something to be clearer and more intuitive to you and maybe to others who share your beliefs. That it happened to come out in objects is a consequence, not a cause, of what you find clear. It's a lot harder to judge clarity across assumption sets. For example, I work a lot in JS and defining JS objects is the one thing I never do. Maybe if each of us put our code in front of the other we'd recoil to exactly the same distance :)