Here's an exception: the action depends on what each of two (or more, I suppose, but I haven't dealt with that) objects
are. This comes up doing collision detection and resolving the collisions. I'm writing a game, and I have different objects floating around in space- parts, bullets, ships, planets, etc. They can hit each other, and what happens depends on what the parts are. If a ship hits a part, it collects it. If a ship hits a bullet, the ship takes damage and the bullet is destroyed.
One option is to keep separate lists of each type of objects, and do:
for bullet in universe.bullets:
for ship in universe.ships:
//handle case
for part in universe.parts:
for ship in universe.ships:
//handle case
//etc. - O(n^2) cases
- This means having to maintain many more lists. If a new type of object is added, Universe needs to gain a new list.
- It will make collision optimizations (like quadtrees or sorting) much more complicated.
- It does avoid any RTTI, but Universe still needs to be aware of all the possibilities, so it isn't any more abstract.
Another option is to force everything into instance methods:
class Ship:
def collide(self, other):
if isinstance(other, bullet):
//handle case
else if isinstance(other, part):
//handle case
class Bullet:
def collide(self, other):
if isinstance(other, ship)
//etc.
- This is nasty.
- It means that everytime you add a new type of object, every other object has to gain a new case! This maintenance is the antitheses of encapsulation.
- You get the choice of doubling all the code (Bullet can handle Ships and Ship can handle Bullets) or creating a precedence list and ensuring that (Bullet, Ship) and (Ship, Bullet) both result in Ship.collide(Bullet)--never Bullet.collide(Ship), which will take more RTTI.
- It's also the way you end up doing things if you start by trying to follow the usual OOP solution to RTTI.
I believe that the best solution is to write one big, ugly function. The function should work like a table, with types on the top and side, and how to handle each type of collision in the cells:
def collide(a, b):
if isinstance(a, Ship):
if is instance(b, Part):
//handle case
else if isinstance(b, Bullet):
//handle case
else if isinstance(a, Part):
if isinstance(b, Ship):
//handle case
//etc.
- This is ugly, but all the ugliness is encapsulated in one function, so everything else can stay in blissful ignorance.
- When adding a new object type, you have to dig back into this function and add the necessary branches.
- This function makes extensive use of RTTI calls, but nothing outside it should have to.
- If you're careful and keep to the table analogy, the function will have an intuitive ordering and it will be clear what needs to added when.
- An optimization can remove the redundant triangle of the table:
if isinstance(b, Ship):
a, b = b, a
if isinstance(a, Ship):
if is instance(b, Part):
//handle case
else if isinstance(b, Bullet):
//handle case
//now nothing below will have to handle cases where b is a Ship.if isinstance(b, Part):
a, b = b, a
if isinstance(a, Part):
if isinstance(b, Bullet):
//handle case
//etc.