コンポジションとミックスイン【Python】

315, 2020-03-11

目次

コンポジションとミックスイン

新たな知見が得られたので読者の皆さんに共有したいと思う。

私はYoutubeに上げる動画のためにCUIゲームのプロジェクトを開発していた。
このプロジェクトをもとに動画を撮影し、Youtubeにアップするのだ。

言語はPython, ライブラリはCursesを使った。
Cursesで力押しでゴリゴリ書いても良かったのだが、気が向いたのでちゃんと設計も考えて作ることにした。

そこで、やはり例の問題が出た。
それは「継承使いづらい」問題である。

世の中の優れたライブラリやフレームワークは、継承を使って巧みに設計をしている。
私からするとこれは、驚愕以外の何物でもない。
私は継承を使って、うまく設計する自信がいまだに持てない。それほど継承は私には難しく感じる。

例によって、継承を使わない場合はコンポジションを使う必要がある。
私は、継承を使うのをやめてコンポジションで開発を進めた。
しかし、コンポジションは移譲を書くのがめんどうくさいのだ。

私はまた困ってしまった。

継承は扱いづらく、コンポジションはめんどうくさい。いったいどうすればいいのだ。
しかし、このときふっと閃いたのだ。それは

「そうだ、ミックスインを使ってみよう」

である。
私はコンポジションとミックスインを併用して使うことにした。
するとどうだろう。私のプロジェクトは翼の生えたかのように前に進み、すべてが明快になり、美しさを内包するようになった。
私は今までミックスインを積極的に使ったことがなかったのだが、今回使ってみてミックスインに対する印象がまるで変わってしまった。

ミックスインはすばらしい。それも、コンポジションと併用する場合はさらに素晴らしい威力を発する。

どういうことか順を追って検証していきたいと思う。

コンポジションとは?

まずコンポジションとは何なのか。
コンポジションとは、クラスの機能を拡張する継承に代わる機能だ。

かなり前から、継承よりコンポジションを使ったほうが設計は良くなるというのは知られていた。
どれぐらい前からかはわからないが、手元のGoFのデザインパターン本にはすでにその事が書かれている。

継承よりコンポジションを多用せよ

具体的にPythonのコードでコンポジションを見てみる前に、まず継承からおさらいしよう。

class Animal:
    def __init__(self, name):
        self.name = name

    def say(self):
        print('my name is', self.name)


class Cat(Animal):
    def __init__(self, name):
        super().__init__(name)
        self.tail = 1

これが基本的な継承のコードだ。
Animalというベースクラス、そしてそれを継承するCatクラス。
Catのメンバ変数(Pythonでは属性と言う)とメソッドはAnimalによって拡張され、Catクラスはname変数やsayメソッドにアクセスすることが出来る。

次にコンポジションだ。

class Animal:
    def __init__(self, name):
        self.name = name

    def say(self):
        print('my name is', self.name)


class Cat:
    def __init__(self, name):
        self.animal = Animal(name)
        self.tail = 1

    def say(self):
        self.animal.say()

このように、コンポジションでは継承を使わずにクラスの機能を拡張することが出来る。
Catsayメソッドは、内部の処理をAnimalsayに委ねている。これを委譲と言う。

コンポジションの不便さ

さて、先程のコンポジションのコードで、Catからname変数にアクセスしたいとする。
ユーザーはCatをインスタンスにして外からnameを変更したい。
この時どうするか。

考えられるのは、animal変数に直接アクセスしてドット演算子を繋げる方法だ。

cat = Cat('Mike')
cat.animal.name = 'Tama'

これで用は足りるが、animal変数が丸見えなため、うかつに名前を変えるなどすると設計が壊れる可能性が高い。
継承を使う場合はanimal変数など無いので、直接変更すればそれでいい。
コンポジションでもanimal変数を隠しながら、nameを変更できるようにしたい。

そこで考えられるのは、セッターやゲッターだ。

class Cat:
    def __init__(self, name):
        self.animal = Animal(name)

    @property
    def name(self):
        return self.animal.name

    @name.setter
    def name(self, name):
        self.animal.name = name

このようにプロパティを使ってセッターとゲッターを定義すれば、コンポジションでも下のようにして変数にアクセスできる。

cat = Cat('Mike')
cat.name = 'Tama'

エクセレント! すばらしい!

だが、待ってほしい。
コンポジションはなにもCatクラスだけに限った話ではない。

DogクラスでもAnimalをコンポジションしたいとする。
そうなると、Dogクラスでもセッターやゲッターが必要になる。
つまり、↓のようになる。

class Cat:
    def __init__(self, name):
        self.animal = Animal(name)

    @property
    def name(self):
        return self.animal.name

    @name.setter
    def name(self, name):
        self.animal.name = name


class Dog:
    def __init__(self, name):
        self.animal = Animal(name)

    @property
    def name(self):
        return self.animal.name

    @name.setter
    def name(self, name):
        self.animal.name = name

このコードを見て読者の皆さんはどう思うだろうか?
私? 私は眉をしかめている。だってセッターとゲッターは同じコードじゃん。
こんなことになっては、明らかにコードを書くモチベーションが下がる。そうでしょう?

ミックスインとは?

さて、本題のミックスインだ。

さきほどのコンポジションの面倒臭さを解決するにはミックスインが使える。
ミックスインとは、それ単体では機能しない、クラスのメソッドを拡張するクラスだ。

どういうことかと言うと、↓の記事を見ていただくのが早い。

たとえば↓のようなコードだ。

class Animal:
    def __init__(self):
        self.name = 'Mike'


class Mixin:
    def say(self):
        print('my name is', self.name)


class Cat(Animal, Mixin):
    pass

CatAnimalMixinを継承している。
Animalはメンバ変数nameを提供し、Mixinsayメソッドを提供する。

AnimalMixinの違いは、Animalは単体でも動作するのに対し、Mixinは単体では動作しない点だ。
つまり、↓のようなことはできない。

mixin = Mixin()
mixin.say()

このようにミックスインはクラスにメソッドを加えることが出来る。

コンポジションとミックスインを合わせる

そこで、先程のコンポジションに加えて、ミックスインも作ってみる。

class Animal:
    def __init__(self, name):
        self.name = name


class AnimalMixin:
    @property
    def name(self):
        return self.animal.name    

    @name.setter
    def name(self, name):
        self.animal.name = name

Catに定義していたプロパティをAnimalMixinに移したわけだ。
あとはCatDogAnimalMixinを継承させれば……

class Cat(AnimalMixin):
    def __init__(self, name):
        self.animal = Animal(name)


class Dog(AnimalMixin):
    def __init__(self, name):
        self.animal = Animal(name)

このCatDogは↓のようにして使うことが出来る。

cat = Cat('Mike')
dog = Dog('Pochi')

cat.name = 'Tama'
print(dog.name)

どうだろう! すごい! 感動!
あのコンポジションの煩わしさが、ミックスインによって解消された瞬間である。
言ってみれば、この瞬間、我々はコンポジションの柔軟さと継承の便利さを同時に得ることができたことになる。

もちろんメンバ変数のないミックスインは、いくらクラスに継承させてもメンバ変数が込み入ることはない。
これは継承では実現できなかった点だ。継承は継承すれば継承するほど、メンバ変数が膨れ上がり、管理が難しくなる。

ということは、↓のようなコードも書けるわけである。

class WaterCat(AnimalMixin, WaterMixin):
    def __init__(self, name, amount):
        self.animal = Animal(name)
        self.water = Water(amount)

私達は動物の獰猛さと、水の柔軟性をあわせ持つ合成獣を手に入れることができた。
しかもこの合成獣は、継承の便利さとコンポジションの柔軟性が共存している!
すばらしい!

おわりに

コンポジションとミックスインは私の設計に光を与えることだろう。
それは長く闇に閉ざされた暗黒の時代の終焉を告げる聖なる鐘である。
ありがとう、イシドールス。我々に光を。

おしまい

投稿者名です。64字以内で入力してください。

必要な場合はEメールアドレスを入力してください(全体に公開されます)。

投稿する内容です。

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク