Python
Django
オブジェクト指向
設計
DjangoDay 22

Abstract base class vs Multi-table Inheritance

はじめに

この記事は作者があるSlackで設計問題のヒステリーを起こしながらダラダラ愚痴った暴言チャットをまとめたものです。

TL;DR

Django ModelのAbstrat base classってぶっちゃけいらなくね?:bomb:

Django: モデル継承問題

例えば、次のようなモデル継承。

models.py
class Player(models.Model):
    name = models.CharField(max_length=128)
    level = models.PositiveIntegerField(default=1)

    class Meta:
        # ...

    # ...


class Equipment(models.Model):
    name = models.CharField(max_length=128)
    owner = models.ForeignKey(Player,
                              null=True,
                              on_delete=models.SET_NULL)

    class Meta:
        # ...

    # ...


class Weapon(Equipment):
    damage = models.PositiveIntegerField()


class Armor(Equipment):
    defense = models.PositiveIntegerField()

わかりやすくRPG風に例えました。Player(プレイヤ)が所有するEquipment(装備)があり、Weapon(武器)かArmor(防具)か2種類が存在する。

Abstract base class

本来なら、EquipmentをAbstract base class(以下、ABC)にしてインスタンス化を禁止すべきです。

models.py
# ...
class Equipment(models.Model):
    # ...

    class Meta:
        abstract = True
# ...

だがこのやり方じゃ、次のようにPlayerの装備を一つのQuerySetにまとめることが出来ません。

InteractiveConsole
>>> tirion = Player.objects.create(name='Tirion Fordring', level=120)
>>> tirion
<Player: Tirion Fordring>
>>> ashbringer = Weapon.objects.create(name="Ashbringer", owner=tirion, damage=160)
>>> lightbringer = Armor.objects.create(name="Lightbringer", owner=tirion, defense=69)
>>>
>>> tirion.equipment_set
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'Player' object has no attribute 'equipment_set'
>>>
>>> tirion.weapon_set.all()
<QuerySet [<Weapon: Ashbringer>]>
>>> tirion.armor_set.all()
<QuerySet [<Armor: Lightbringer>]>

ABCを使うと継承したConcrete Model以外はテーブルとManagerを作らないので当たり前ではあります。

Multi-table Inheritance

一方、上記models.pyでMetaオプションの特別な指定がなければ、Multi-table Inheritance(以下、MTI)になります(裏ではもちろんOneToOne Relation)。

InteractiveConsole
>>> tirion = Player.objects.create(name='Tirion Fordring', level=120)
>>> tirion
<Player: Tirion Fordring>
>>>
>>> ashbringer = Weapon.objects.create(name="Ashbringer", owner=tirion, damage=160)
>>> frostmourne = Weapon.objects.create(name="Frostmourne", owner=tirion, damage=896)
>>> lightbringer = Armor.objects.create(name="Lightbringer", owner=tirion, defense=69)
>>> robe = Armor.objects.create(name="Devout Robe", owner=tirion, defense=89)
>>>
>>> tirion.equipment_set.all()
<QuerySet [<Equipment: Ashbringer>, <Equipment: Frostmourne>, <Equipment: Lightbringer>, <Equipment: Devout Robe>]>
>>> tirion.equipment_set.filter(weapon__isnull=False)
<QuerySet [<Equipment: Ashbringer>, <Equipment: Frostmourne>]>
>>> tirion.equipment_set.filter(armor__isnull=False)
<QuerySet [<Equipment: Lightbringer>, <Equipment: Devout Robe>]>
>>>
>>> tirion.weapon_set
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'Player' object has no attribute 'weapon_set'

綺麗に装備をまとめることが出来ましたが、abstractにすべき装備モデルが単独でインスタンス化してしまいます。
あ、SQL文も増えます…

django-model-utils

ここで、django-model-utils: InheritanceManagerの登場です。
https://github.com/jazzband/django-model-utils
https://django-model-utils.readthedocs.io/en/latest/managers.html#inheritancemanager

models.py
from model_utils.managers import InheritanceManager
# ...
class Equipment(models.Model):
    # ...
    objects = InheritanceManager()

# ...

すると、以下のことが出来ます。

InteractiveConsole
>>> tirion.equipment_set.select_subclasses()
<InheritanceQuerySet [<Weapon: Ashbringer>, <Weapon: Frostmourne>, <Armor: Lightbringer>, <Armor: Devout Robe>]>
>>> tirion.equipment_set.get_subclass(pk=1)
<Weapon: Ashbringer>

mixed iterableだけど同じEquipmentインスタンスだしいいんじゃね?
Equipmentのインスタンス化問題はvalidationやassertionなどで補うとなんとか…なところでしょうか。

あと気になるのは武器と防具の更なるsubclassを作ったらどうなるんでしょう…

ABCの存在意義とは

ここから愚痴です。

  • Django ModelのABCってただ、フィールド・メソッドのコード共通化以外意味なくね?動的型付けだからPolymorphismの意味もないし…
  • Interface作るならmetaclass=abc.ABCMetaして@abstractmethod使って多重継承(Multiple-inheritance)すればいいよね?さっぱりして衝突もなさそうだしProxyってなんか変だし…

まとめ

  • Multi-table Inheritanceで行こう
  • import abcしてInterfaceはいかが?
  • Django2.0でInheritanceManagerがまだ使えん…
  • 経験豊富な方のマサカリ、お待ちしております。

etc.

Human.objects.all()にして人種・国・性嗜好関係なくmixedされたら世界平和なのに、
Human._meta.abstract = Trueなのが諸悪の根源。

DQ8rAo6X0AE04BE.jpg

References