Troubles in OO design

Started by
33 comments, last by Rockoon1 15 years, 2 months ago
Quote:Original post by Antheus
Or something along these lines. There are no excess comparisons.


I realize that you probably would see them if you spent a little more time thinking about it, but you have approximately 50,000 excess comparisons there.

The object flags itself killed or does not, and then another object checks to see if it is or is not flagged.

That is more work than a system where the object simply kills itself, where other objects need not check for this state.

I know that it may seem minor right there with your example, but what happens when there are a lot more lists than just the alive list? What about the burning list or the waiting for orders list? I can easily envision a dozen such lists for a simple top-down shooter...

Advertisement
Quote:
Classic example: Object needs to destroy itself, because somebody just shot it in the head with a freakin' laser.


Classic mistake. Object needs destroyed as a result of some game rule. Game rules know full well about the components of their world.

Mis-assigning responsibility is a programmer error regardless of paradigm.
Sure. It's of course possible to get the same effects in OO programming as with old-school procedural programming. But, there is a huge inertia and intuitive pressure with OO (and C++ in particular) to use the STL, etc. Everything people are taught wrt OO will lead a person to think as follows:

Quote:
Hmm, I need a list of enemies in my game. Well, I'll make a kind of generic list which doesn't care what' in it. And, my enemies... they have no real need that I can think of to know that they're part of the list... etc.


It does make sense to the extent that the only real reason the enemies would need to know they are part of a list is for the sake of efficiency -- there is no *logical* reason they should know, given an infinitely fast CPU.

The problem I see with OO is that it takes the (generally good) idea that abstraction and APIs and black boxes are good to an extreme, and makes it kind of the "default" way of thinking about everything, even at the lowest levels, and you often find that there are unintended consequences. Much of the "refactoring" that is so popular and has become such a natural part of the development process is, I think, aimed at fixing these overly encapsulated interfaces which have led, unconsciously, and unforeseably in many cases to an inefficient design.

Of course I do think that profiling is an invaluable tool -- (and BTW... profiling a heavily STL using program is a real eye-opener if you haven't tried it, and if you can decipher the horrendous output). Premature optimization is the root of all evil or some such, it has been said.

As for "At some point, somewhere in there, you need to determine the /* oh, I'm dead */. " Yes, you do. But it doesn't have to be per-object. It can be in the code that detects, say, a laser beam intersecting a bad guy -- that is, it can be limited to comparisons between the laser beam's location and the subset of bad guys near the laser (as determined by the fact that the list is, say, sorted into zones) and the fact that the bad guys are on a "rarget list" which is not as big as the complete object list. Likewise, for objects which have a limited lifetime (e.g. say, sparks, they live say, 30 frames or so, then die. When they die -- which they notice while they're moving themselves -- they just remove themselves from the list.)

The difference is that you check these things when the associated objects are already doing something else, already sitting in the CPu's cache, and not in a separate step that's global to all objects.

Amd of upi DID have the separate global check, the very same tests I already described above would have to be done IN ADDITION, just to set the flag that the global check is doing. So -- 50000 comparisons are indeed eliminated, because your solution looks like this:

   /* oh, I'm dead */   dead = true;


and elsewhere:

  for each object     if it's dead, jim,       kill it with fire.


You essentially do the check *twice*. Which is my complaint.
Quote:Original post by Antheus
At some point, somewhere in there, you need to determine the /* oh, I'm dead */. Whether you then rewire the pointers or do something else, it's a consequence. This can be coded in many ways.


Yes, you do need /* oh, i'm dead */

..but you do not also need /* oh, it's dead */

I realize that this point is sort of tangent to this discussion, but you are making innaccurate observations so you shouldn't get to use them as leverage for your primary arguement without them being pointed out.

The methodology that forbids an object from knowing who contains it, in this case, is causing at least twice as many maintenance checks as is actualy necessary. And thats assuming an object always has to check if it should die, where the reality is that normally an object only has to make that check when it discovers a pending interaction with another object.
Quote:Original post by Rockoon1
The methodology that forbids an object from knowing who contains it, in this case, is causing at least twice as many maintenance checks as is actualy necessary. And thats assuming an object always has to check if it should die, where the reality is that normally an object only has to make that check when it discovers a pending interaction with another object.


Exactly, and well put. And since you mention that this point is a tangent, I'll bow out now. But nice to see that somebody gets what I'm saying. :)


Quote:Original post by smcameron
Exactly, and well put. And since you mention that this point is a tangent, I'll bow out now. But nice to see that somebody gets what I'm saying. :)


Its mainly tangent because the OP did ask for advice on a "clean" object oriented methodology. Essentialy, he is looking to draw on the experience of others, and in that vein, restricting to OO principles is exactly what he is after.



Quote:Original post by Rockoon1
Quote:Original post by smcameron
Exactly, and well put. And since you mention that this point is a tangent, I'll bow out now. But nice to see that somebody gets what I'm saying. :)


Its mainly tangent because the OP did ask for advice on a "clean" object oriented methodology. Essentialy, he is looking to draw on the experience of others, and in that vein, restricting to OO principles is exactly what he is after.



Right, and the classic OO principles, frankly, just plain suck.
Object-Oriented programming provides you with tools that help you achieve several things:

  • Open-Closed interfaces: you can alter the behavior of a certain piece of code without changing it, simply by providing a polymorphic object argument that does what you want. Other means of achieving this are functional programming (you would pass a function) and aspect-oriented programming (you would bind a behavior to an internal event).

  • Encapsulation: you can group together related pieces of functionality that operate on a certain piece of data, and hide the whole behind an interface to achieve polymorphism. Other paradigms have more difficulty achieving this, which leads them to designs where this is not required.

  • Dynamic designs: instead of having a design defined at compile-time, an object-oriented program can alter its design at runtime by adding or removing components and configuring object dependencies on the fly. This allows more freedom, but also makes program architecture more confusing to understand.


In addition to this, typical Object-Oriented programming also incorporates some more general concepts found in other approaches, such as separation of concerns (the Single Responsibility Principle and Interface Segregation Principle) which makes code more modular and therefore easier to extend, and the Dependency Inversion Principle, all of which can be commonly found in good procedural and functional code.

As smcameron points out, Object-Oriented programming also has its flaws. Optimization, for instance, relies on knowledge that helps improve performance, which can sometimes come at odds with the tendency of objects to hide their internal details (and therefore, useful knowledge) from the rest of the code. The example chosen here is silly: if performing a boolean check for every object were a significant amount of performance, then effort should be spent to avoid updating all objects every frame, for instance by using a priority queue based on the time of the next update. But there are more serious examples out there, such as the performance difference between performing SQL queries by hand and using objects and an ORM.

Be wary of premature optimization—there is never any point to optimize code that might be removed tomorrow. Only once you're fairly certain that a piece of code will not be changed anymore can you let tightly encapsulated objects leak information. To reuse smcameron's example, assume that instead of setting an isdead boolean, the object instead removes itself from a list of objects then deletes itself. This approach prevents us from storing the object in other lists (such as 'enemies that are about to appear in the game') or even be referenced by other objects (such as 'the target of a homing missile'): you have to find a way to notify all referencing objects that you're dead, which is harder to achieve if you just blast yourself out of a list as soon as possible.

As a final remark, don't plan your design. You don't have the experience required to think up a flexible design on your first try, because by definition you can't think of everything. The simplest way to create your first object-oriented design is to discover it: write your code as you normally would, and once you get stuck (because the design was bad), think of how you could have designed everything so that you wouldn't have gotten stuck. Keep things simple: don't just add complexity to your design, also think of ways to make it simpler. Once you have a good alternative design, refactor your code. Use unit tests to determine that you haven't broken anything.

After a while, you'll get used in advance to the various pitfalls of your designs, and you will be able to plan accordingly.
Quote:Original post by smcameron
Right, and the classic OO principles, frankly, just plain suck.


I wouldnt go that far. They suck in as much as it pertains to many optimization strategies, but that isnt in the scope of their intent.

OO is a solution for a development efficiency problem, not a runtime efficiency problem. It is no suprise that there will be sacrifices.

Quote:Original post by ToohrVyk
To reuse smcameron's example, assume that instead of setting an isdead boolean, the object instead removes itself from a list of objects then deletes itself. This approach prevents us from storing the object in other lists (such as 'enemies that are about to appear in the game') or even be referenced by other objects


smcameron would probably point out that if the space ship has a reference to the alive list, then why can't it also have a reference to things that are homing in on it? (Or, why can't the alive list have a reference to a list of homers)

The purely OO methodology which can accomplish this stuff is the Observer pattern.

Having a specific list of things homing in on it, the space ship is in effect implementing a subset of the observer pattern. In this point of view the difference is that the space ship knows that it has a list of homing missiles and what a homing missile is (what it needs to know), instead of the more generic concept of a list of arbitrary observers.
What the... What is going on in here o_O?

Optimise the code if you notice the game\application\utility\cheese maker is choppy. I don't really see why performing an boolean check would be a problem in these days, heck even a dozen.

if ( objects.at(i)->isAlive ){  //Do stuff  objects.at(i)->Logic();}else{  //Nuke it, then continue with next object.  delete objects.at(i);  objects.erase( objects.begin() + i );  i--;}


Keep it simple folks. :>

TL;DR: Over engineering is bad.

This topic is closed to new replies.

Advertisement