1
\$\begingroup\$

Imagine I have code like this in (it's about the concept; this is example code):

# models.py
from django.db import models

class Monster(models.Model):
    x = models.FloatField()
    y = models.FloatField()
    def use_unique_ability(self, target):
        ''' different monsters types will implement this differently '''
    def check_attack(self, player): # should I attack this player?
        ''' different monsters types will implement this differently '''
    class Meta:
        abstract = True

class SlimyMonster(Monster):
    def __init__(self):
        self.attack = 3
        self.hit_points = 10
    def use_unique_ability(self, target):
        target.hit_points -= 2 * target.attack
    def check_attack(self, player): # should I attack this player?
        return player.hit_points < self.hit_points

class CaerbannogRabbit(Monster):
    ''' well, you get the idea '''

# many more monsters

Each monster has the same attributes and method names, but different values and implementation. Monster would probably be an interface in Java. Each monster type will override only some of the many methods.

This technically works, but it has some significant downsides:

  • Each monster is a new database table with the same columns
  • Because of that, I need a lot of queries to check for all monsters
  • I need GenericForeignKey to reference individual monsters (instances of any of the model classes)

The other extreme isn't exactly pretty either. This is based on dynamically mixin a base class to an instance (+this) and how to import a module given the full path?:

# models.py
from django.db import models
from os.path import join
from inspect import isclass
from imp import load_source
from settings import PATH_TO_MONSTER_FILES

class InconsistentMonsterError(Exception): pass

class MonsterClass(models.Model):
    name = models.CharField(max_length = 32)
    attack = models.PositiveIntegerField(default = 1)
    defense = models.PositiveIntegerField(default = 10)
    method_file = models.CharField(max_length = 128) # it's a source file, not media file, so I'm using CharField
    def __init__(self, *args, **kwargs):
        super(MonsterClass, self).__init__(*args, **kwargs)
        try:
            modul = load_source(self.method_file, join(PATH_TO_MONSTER_FILES, self.method_file))
        except IOError:
            raise InconsistentMonsterError('no file named %s' % join(PATH_TO_MONSTER_FILES, self.method_file))
        classes = [cls for cls in [getattr(modul, x) for x in dir(modul)] if isclass(cls)]
        if not len(classes):
            raise InconsistentMonsterError('file %s doesn\'t contain classes' % join(PATH_TO_MONSTER_FILES, self.method_file))
        bases = self.__class__.__bases__ + (classes[-1], )
        self.__class__ = type('NewClassName', bases, {
            '__module__': MonsterClass.__module__,
        })

class MonsterInstance(models.Model):
    cls = models.ForeignKey(MonsterClass)
    x = models.FloatField()
    y = models.FloatField()

and for each monster a file like this:

# slimy_monster.py
class SlimyMonster(): # not a Model
    def use_unique_ability(self, target):
        ''' different monsters types will implement this differently '''
    def check_attack(self, player): # should I attack this player?
        ''' different monsters types will implement this differently '''
  • The code seems... inellegant. It's complex, and it's like using eval (execute code based on an 'external' string)
  • Every monster must be changed in two places: the database and a file (although I suppose I could define database values in the file)

Given these two options, should I use one of them, or something in between? I tend towards the second option, but I'm not quite satisfied.

\$\endgroup\$
3
  • \$\begingroup\$ I remove the notice about being off-topic. IMO, having two version of the same code tend to be opiniated, reviewing one version could be better (in fact you could probably have two questions). have you read the faq before posting ? This can help to decide if your question is on-topic or not. (I'm still hesitant to consider the on-topicness of the question) \$\endgroup\$
    – Marc-Andre
    Apr 11, 2014 at 13:50
  • \$\begingroup\$ Well, it's best practise for specific code so that seems okay. Can't find the two different versions things. Splitting it in two questions seems wasteful though; the answers to each are just doing to complement each other. \$\endgroup\$
    – Mark
    Apr 11, 2014 at 14:30
  • \$\begingroup\$ The version things is my personal preference, nothing off-topic about it, just they have a tendency to being opinionated (again this is my opinion). I have difficulty to judge the topicness of v1 vs v2 questions. Indeed your question seems okay, there are no vote-to-close. \$\endgroup\$
    – Marc-Andre
    Apr 11, 2014 at 14:37

1 Answer 1

3
\$\begingroup\$

What you want is inheritance as a proxy model.

In your subclasses, use the following Meta option:

class SlimyMonster(Monster):
    class Meta:
        proxy = True

    def some_method_to_override(self):
        ...

This way the subclassing has no effect on the database side, only on the Python side.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.