Some programmers actually hate OOP languages? WHAT?!

Started by
91 comments, last by Washu 9 years, 3 months ago


I'd love to see an example where an use of it has made the code more readable and easier to maintain by the way smile.png

I don't have any specific example for you at this time, but I will point you to some people who can expound on that subject. There's the points raised in http://www.dataorienteddesign.com/dodmain/, and the ones Mike Acton raises in his CppCon talk and "typical C++ bullshit" slides. I'll summarize what I took from both of those:

  • having your data declared in a simple, straightforward way instead of hiding it behind layers and layers of encapsulation and abstraction makes it easier to reason about what state the program has and what subsystem owns it - and I'd add that in a more data-oriented programming style, you might segregate your data by how often it is mutated, which can make it easier to understand where state changes happen
  • implementing functionality by composing transforms of collections of data allows you to more quickly find where a bug is actually happening because your state mutations occur in specific places meaning that you can quickly find where a corner case was missed or where an unintended side effect is happening
  • existence-based processing can cut down on the cyclomatic complexity of the code by reducing the number of checks for validity you need to do - and in my experience, checks for validity (ie. null pointers and such) negatively affect code readability in a huge way
  • concurrency becomes easier to implement and debug when you build your program by transforming sets of homogeneous data instead of structuring your program around transforming individual pieces of data in isolation

To me, the essence of being "readable" means that I can understand what a program is trying to accomplish and how it goes about accomplishing that with a minimum of effort. A program that is "easy to maintain" is one that is easy to debug and easy to change in such a way that I don't introduce new bugs in the process. My impression of data-oriented design is that while it might appear that these data-oriented systems take more effort to change, I think ultimately how long it takes me to make a change is more important than how much code I need to touch. Those two things are related - "effort" here means the time to make the change plus the time to debug it - but given the choice between making a larger number of small changes that I have high confidence won't break anything and a smaller number of changes that are more likely to break something elsewhere in ways that I need to think extra hard about (eg. if I fix a bug in a base class that breaks subclasses), I'll take the former.

My experience with my own code (which I've recently started writing in a more data-oriented way even for gameplay code) is that I tend to add new features by adding new subsystems instead of modifying big swathes of existing code to add the features to all the different classes that already exist as tends to happen with the (object-oriented) projects I've worked on and still work on at work. Most of the true object-orientation in my own code occurs at the feature subsystem level, not at the individual data object level. This not only means that it's easier to fix bugs in specific features (because the code and data for those features is all in the same place), but my confidence in the correctness of the existing codebase gets continually higher as I add new things. I already spent time to make it work, and I'm not changing it, after all.

It might be interesting to take a reasonably complex program and implement it in "typical OOP" and "typical DOD" and compare the two to see how easy it is to do different maintenance and implementation tasks in each system.

edit: And I think it's a misconception that data-oriented programming = focus on cache performance and avoiding branching. Those are things that can come out of taking a data-oriented approach, but I don't think you need to go all the way to using bitmasks instead of conditional branching for your code to be "data-oriented". Data orientation to me just implies that your code is built around how your data is laid out and your data is laid out according to what you actually need to accomplish instead of trying to model the "real world" with abstractions in code.

Advertisement

As a note, many paradigms are compatible with each other and work well together. Others are located on a continuum.

I already mentioned several earlier in the discussion. Object oriented works well with event oriented paradigms. Object oriented works well with state machines and automata. Object oriented is on a continuum with functional development, one preferring verbs the other preferring nouns.

Many people interpret "object oriented" as inheritance trees or deeply nested commands. While it is true that some systems do this, that is not what object oriented is about.

Object oriented is part of the continuum of preferring nouns or verbs. Languages tend to prefer objects, the nouns, or they tend to prefer verbs, the functions or actions. When you crate an opaque structure and operate on it like C's FILE*, that is a noun. When you work the opposite direction, SELECT whatever FROM whatever WHERE whatever, you are focusing on verbs, that is more functional.

Object oriented and data oriented can be orthogonal. You can have both, you can have either one, you can have neither. I can create an object that is composed of SoA or composed of AoS, both are still treated as a composed object that represents a bunch of items in a collection.

Edit: It looks from doing some reading like some people are treating 'data oriented' in the same bucket as 'functional'. If that is your interpretation of the term, and since there are no official definitions, then yes, they are both on the same continuum. But please don't use it that way, there is already a name for code that focuses on the manipulation of data as verbs through functions, it is called functional programming.

Object oriented is part of the continuum of preferring nouns or verbs. Languages tend to prefer objects, the nouns, or they tend to prefer verbs, the functions or actions. When you crate an opaque structure and operate on it like C's FILE*, that is a noun. When you work the opposite direction, SELECT whatever FROM whatever WHERE whatever, you are focusing on verbs, that is more functional.


This story of some languages putting emphasis on nouns or verbs doesn't make any sense to me. When you have an opaque structure like FILE * in C, you then `fopen' it, `fread' from it, `fwrite' to it and `fclose' it. If anything the emphasis seems to be on the verbs. When you have a language that can only be used to query a bunch of tables, it looks to me like the emphasis is on the nouns; or at least you could look at it that way.

The main difference between C and SQL is that in C you specify what steps to take, while in SQL you specify the results you want, and it's up to the RDBM to figure out a good plan for getting that information (I think this is called imperative -vs- declarative).

I find that when making games I love using OOP. Games just have so many objects that could be in the real world and it makes sense to me. When I write business applications I have a hard time using OOP because I find it harder to break concepts into objects that mean something to me. OOP in my mind just works better with actual objects you could touch and see vs concepts. A player, a coin, a bad guy, these are all very much physical things.

Part of the bad rap that OOP has can be tracked down to the prevalence of Java in Western college curriculum. Java, as a language, adheres to a particularly conservative orthodoxy of what "OOP" is -- everything is an object or part of one, the programmer can't be trusted to override operators in a sensible way, everything is garbage collected, don't think about the cost of abstractions, use lots of exceptions -- in other words, many of the things people call "typical C++ bullshit" or "typical OOP bullshit" is actually "typical Java bullshit" that's been drug into C++ by recent grads or Java refuges who don't know how to write idiomatic C++ and so write idiomatic Java in C++ instead.

Java's idioms are fine for Java, such as it is, but idiomatic Java is bad C++. In idiomatic C++, we prefer non-member functions over member functions (Java calls them Methods); in idiomatic C++, we trust the programmer to override operators and other such anti-social things; in idiomatic C++ we require that a programmer thinks about resource lifetimes and encourage them to think about the cost of their abstractions; in idiomatic C++, exceptions are to be used sparingly in truly exceptional (usually non-resumable) circumstances.

Now, C++ is by no means perfect as an OOP language or otherwise. Its got warts, it drags a lot of legacy along, its terribly complex, and its far corners are dimly lit for all but a select few programmers (usually the types who attend C++ standardization meetings, implement compilers, or are in the business of selling library implementations). Additionally, exceptions and manual resource management mostly go together like oil and water -- you can't just put the two in the same container and expect it to work out, they only combine well when you take great care in the combining.

Another part of the OOP-is-bad legacy, as relates to C++, is that C++ was the first really mainstream language to popularize OOP, and it did it in large part by dragging procedural (that is, C) programmers into OOP through C++. The existing OOP programmers at the time, the guys using Smalltalk or Simula, were not overly-impressed with C++ because it lacked alot of the OOP richness they were used to in the languages they already had -- those languages weren't commercially successful on the scale that C++ was, but there wasn't a mass exodus to the newer C++ nonetheless. Bad OOP/C++ practices that stem from this bloodline (as opposed to the Java bloodline) tend to manifest as C-with-classes-style programs, which look and act like a procedural program design that's been shoehorned into objects, usually with little regard at all for design proper.

But finally, be careful not to think of C++ as an "OOP language" because its not. C++ is a multi-paradigm language that supports and combines procedural, OOP, functional, and macro concepts into one language. You can use C++ in any of these ways, or all of them, the choice is yours.

As for Data-Oriented-Design, I submit to you that it is not in fact in opposition to OOP. Instead, I think of them as orthogonal and complimentary. Objects remain an important and solid way of organizing DoD code and representing its structures. You only have to change your perspective about what constitutes an object in your design language. DoD is effective and important for maximizing performance on today's machines -- when you need to unlock the full potential of performance, DoD is a necessity in any programming language or paradigm (In my view, DoD is more of an applicative pattern than a paradigm, because its a way of expressing how our program solutions map to hardware and not a way of expressing how our thoughts map to our program solutions).

throw table_exception("(? ???)? ? ???");

edit: And I think it's a misconception that data-oriented programming = focus on cache performance and avoiding branching. Those are things that can come out of taking a data-oriented approach, but I don't think you need to go all the way to using bitmasks instead of conditional branching for your code to be "data-oriented". Data orientation to me just implies that your code is built around how your data is laid out and your data is laid out according to what you actually need to accomplish instead of trying to model the "real world" with abstractions in code.

I think you are confusing data driven design and data oriented design smile.png Data oriented is all about performance (cache misses being a part of it), it's one of the first things it says on one of the pages of the link you posted (quoted below) smile.png. Although I think there are semantics to be discussed (My definition of data-driven is what the bitsquid engine guys use. They also mention data oriented, but only when talking about performance and optimizations). Semantics aside, what I was referring to in my original post was purely the "data oriented" design for high performance sake.

In recent years, there’s been a lot of discussion and interest in “data-oriented design”—a programming style that emphasizes thinking about how your data is laid out in memory, how you access it and how many cache misses it’s going to incur. With memory reads taking orders of magnitude longer for cache misses than hits, the number of misses is often the key metric to optimize. It’s not just about performance-sensitive code—data structures designed without sufficient attention to memory effects may be a big contributor to the general slowness and bloatiness of software.

OOP, DOD, procedural programming and whatnot all have their place in software engineering. I have a lot of trouble with programmers who pick one winner and then blindly apply it even when it is not appropriate. Personally, I love the C# programming language and I think it's the best designed language I have programmed with and that Anders Hejlsberg is a genius, but I wouldn't use it for absolutely everything because neither C# or pure OOP are not suited for everything. I used to code pure OOP, but doing system and driver programming on Linux in a OS class at university made me appreciate the C programming language. One of the great things about it is that I can look at C code and have a very good idea what the compiler will do with it because machine code is like that. OOP languages like C++ tend to hide things for you. For example, a function may look very simple and only a few lines of code, but then you look at the assembly and you see 5 virtual destructor calls, with each one doing a system call to close OS handles. In C there's no surprises like that, but on the other hand you have to do it yourself or you'll leak resources, so you can't be lazy. Personally, I believe in a balanced and context-sensitive approach to software engineering, not absolutes. I use the right tool for the job.

I enjoy "playing" with memory layout and I have fun trying to optimize its usage to avoid cache misses in a C/C++ application and squeeze as much performance as I can, so data-oriented design is an interesting concept for me, but I also like exposing APIs as a clean package of C++ classes because it (at least to me) feels more familiar, more structured and easier to maintain, so again I think balance is important. When I'm coding close to my engine I'm going to be paranoid about cache misses and might mangle my OO design a bit in the process if I realize something wastes more time than I expected, but when I'm writing code that only runs sporadically, I'll prefer a clean object-oriented design.

Somewhat related, but there seems to be some hate for the C++ programming language in the FSF community so I wonder if this thread was created after the original poster read something about OOP from a FSF guru. Linus Torvalds, for example, has a profound distaste for C++, and said he would even refuse to work with a C++ programmer. Now, Torvalds is pretty much the paragon of those anti-social hackers who know everything about a CPU but can't say 2 sentences in a row without insulting somebody, but if system/kernel programming was all I did, I doubt I'd find C++ very useful either since too many of the language's facilities are either off-topic or not very useful in kernel mode. Also, the GNU coding standard (assuming anyone calls that a coding standard tongue.png) says that you CAN use C++, but you basically can't do anything interesting with templates. (But this is the same "coding standards" that forbids using the term "WIN32" if you port to Windows because RMS doesn't want you to abbreviate Windows with WIN...)

I find that when making games I love using OOP. Games just have so many objects that could be in the real world and it makes sense to me.


And that's where OOP goes wrong and where people get ideas about it being universally bad. smile.png

Programmers are wizards. You're not programming the real world. You're programming a computer, using the magical powers of computer science and software engineering. The computer doesn't care in the least that in our real world we split Monsters and People into separate taxonomies nor does it care that your human muggle brain wants to organize all your asset type categories into a neat little hierarchy (which isn't even good for humans once you get to a large number of categories). When you start structuring code based on the real world, all kinds of abstractions and structures and relationships end up expressed in code that don't actually do a single thing to assist either the hardware nor your program's functionality and you end up enshrining both deep inefficiencies and crippling feature limitations into your entire codebase.

The popular example (I can never remember the source, and Google never helps) involves a coffee maker control program.

A novice programmer abuses OOP and makes a CoffeeMaker class that inherits from a Heater and a Pump class and the OnButton class and a PowerLight class.

An intermediate programmer realizes that Heater, Pump, and PowerLight are all just "things you can turn on" and OnButton is an event source and so goes on to make an IOnOff interface with On and Off methods and then implementations for OnOffHeater and so on.

A journeyman programmer realizes that On and Off can be parameterized into a single Enable function that takes a boolean argument, and then realizes that an interface consisting of a single function is a huge waste of effort and unnecessary coupling and just replaces the Heater and Pump classes and so on with functions.

The master programmer then applies the same thought process to the on/off button and transforms the novice programmer's pile of 7 different files with 10+ different classes into four little functions taking up 20 lines of code (not denser code, but clearer, simpler, and easier to maintain code). One might note that this can be written as a pure functional program using reactive programming.

--

There's not some big axis upon which the paradigms lie. OOP is not antithetical to data-oriented design, nor is it antithetical to functional programming. However, there are definitely elements of both OOP or functional programming that can be misused in ways that severely hard data-oriented design (or declarative programming, etc.). Part of being a truly exceptional programmer is being able to evaluate the specific context you're working in and what tool solves that problem.

An ECS probably should not have a bunch of polymorphic objects inside of it, for example, and that would be quite poor data-oriented design. However, all the things that bind together the component managers? The scripting interface bindings? Editing controls and serialization? Pretty good place to use OOP, since even super hip functional programming has a lot of weaknesses in those public layers (but functional programming might be the perfect tool to implement the stuff inside those layers!). You might also drop in evented programming, though you'd very likely run into maintenance issue at any large scale, or declarative programming if you're really into writing a ton of special tools and one-off DSLs just to avoid a little OOP.

--

OOP also gets hated on because people severely misunderstand type systems. People assume that Java and C++'s type systems are the definition of OOP, despite popular languages like Python or Ruby rather disproving that soundly (or even, say, the original OOP languages that way predate C++, but that's another story). Inheritance, even for interfaces, is not OOP. That mostly stems from a limitation in how C++ was implemented that itself is mostly due to the technical challenges of implementing highly efficient message (aka method) calling conventions.

Languages stemming as far back as Modula and offered alternatives to these type systems that allow efficient OOP without ever needing inheritance. Objective-C offered another appraoch based on Self. Dynamic and prototypical languages just threw the efficiency to the wind. Newer structurally-typed languages like Go brought back the Modula approach in a more pleasant modern packaging, and then a new breed of languages tweaked that approach a little and made a whole new category of structural typing now popularly known as a trait system (see Rust).

None of which has anything to do with OOP, but people confuse it all with OOP far too often.

Sean Middleditch – Game Systems Engineer – Join my team!

This topic is closed to new replies.

Advertisement