Abstract base class vs Multi-table Inheritance

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


# TL;DR
Django ModelのAbstrat base classってぶっちゃけいらなくね？:bomb: 


# Django: モデル継承問題
例えば、次のようなモデル継承。

```py3: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`(防具)か２種類が存在する。


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

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

    class Meta:
        abstract = True
# ...
```

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

```pycon: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)。

```pycon: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

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

# ...
```

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

```pycon: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](https://qiita-image-store.s3.amazonaws.com/0/153694/b647f0d3-df12-4a80-a176-a3b80d33d600.jpeg)


# References
- [オブジェクト指向と10年戦ってわかったこと](https://qiita.com/tutinoco/items/6952b01e5fc38914ec4e)
- [How to query abstract-class-based objects in Django?](https://stackoverflow.com/questions/3797982/how-to-query-abstract-class-based-objects-in-django)