MixinについてAdapterについて

pyramidでmixinを使ってコードを書いていた。これは適切なコードの書き方では無かったそう。
もう少し具体的に言うと、component architectureの上では、mixinを使わない方が望ましいらしい。
代わりにadapter*1という機能を使うそう。
adapterのことをちょっとだけ調べてみたらおもしろかったので日記にまとめてみる。

実は。。

adapterの機能を詳しく知らなかった。動きは大まかにはあくしていたのだけれど、何に使えるのか分かっていなかったような感じ。

ここでは、adapterという言葉をZCA(zope component architecture)の中でのadapterに意味を限って使うことにする。
調べる前のadapterの認識は以下の程度の認識だった。
「何か、渡したオブジェクトをメンバーとして持つオブジェクト返してくれる機能」。。

良く見かける例はだいたいこんな感じで終わってる。

  • Groupに所属するMemberがいるような感じ
  • それぞれIGroup,IMemberを実装している。
  • GroupはMemberをオブジェクトのメンバーとして持つ。

adapterは、オブジェクトを渡してあげると、それをオブジェクトのメンバーとしてもつオブジェクトを返してくれるもの。という感じの認識だった。

from zope.interface import implements
from zope.interface import Interface
from zope.interface import Attribute

class IGroup(Interface):
    pass

class Group(object):
    implements(IGroup)

    def __init__(self, member):
        self.member = member

class IMember(Interface):
    name = Attribute("member name")

class Member(object):
    implements(IMember)
    def __init__(self, name):
        self.name = name

foo = Member("foo")

##
from pyramid.registry import global_registry as registry
registry.registerAdapter(Group, (IMember, ), IGroup,)
print registry.queryAdapter(foo, IGroup)
# <__main__.Group object at 0x1240490>

Memberをメンバに持つGroupをAdapterを通して生成できた。
GroupはIGroupの実装でIMemberが必要。

動きはおえるのだけど、使い道が分からない。。

mixinについて

一方で、mixinはわかりやすい。複数の機能を持ったオブジェクトを作成するときに、機能ごとにブロックで区切って実装すること。
ただし、コンストラクタを作成しない。
だいたいこんな感じ。

class MeasureSkillMixin(object):
    def has_empty_area(self, target):
        return True

class SaveSkillMixin(object):
    def save(self, target):
        return "ok"

class DownLoader(MeasureSkillMixin, 
                 SaveSkillMixin):
    def download(self, target):
        if self.has_empty_area(target):
            return self.save(target)
        
dlr =  DownLoader()
print dlr.download("anything") # => ok

余裕があるか調べる機能と保存する機能を分けて定義。Downloaderでmixinしてる。

mixinが解決してくれること

mixinに分けることで、細かく機能を追加できるようになった。
そして各mixinが提供する機能が直交してるなら、それぞれを個別につなぎあわせて便利なオブジェクトを作ることができる。

また、ポリシー毎にmixinを用意して、そのポリシーを利用するオブジェクトには、mixinで機能を付加させることで、
綺麗な形で複数のオブジェクト間の関係を保つことができる。

mixinが解決してくれないもの
  • 異なるポリシーを持つオブジェクト間の機能の共有。

どういうことか少しだけ説明。

今自分たちが作っているプロジェクトでは、容量に余裕が計測が必要クラスについて、
実際に作業(saveなど)を行う前に、has_empty_areaを実行して、作業可能かどうか判断する
というのポリシーとして持っているとする。

そのために

  • MeasureableみたいなInterfaceを用意している
  • MeasureSkillMixinみたいなその機能を提供してくれるMixinを用意している。
  • 提供されているMixinを利用して各開発者は機能を実装していく。

ここで、このポリシーが守られている場合は、一定の秩序を保つことができる。

ここで、別のプロジェクトで使われていた巨大なライブラリ/システムを組込むことになった。
別のプロジェクトでは、上述したポリシーに基づいて設計されていることは期待できない。

例えば、これら複数のポリシーを持ついろいろなオブジェクトに対して、作業の実施コストを尋ねるような処理を書きたいとする。
こういう状況下におかれてるとする、さてどうしよう?

mixinの場合は、結局、実装をどちらかに寄せるということになると思う。
現在のプロジェクトに参加する全てのオブジェクトは、統一的なインターフェイスになるように新たにラッパーがたくさん作られることになると思う。

各オブジェクトに統一的な振る舞いを要求する感じなので、トップダウンの父権的な感じ。

adapterの方法

ZCAでのadapterの方法はこれとは少し違う。
こちらでは、各オブジェクトに統一的なインターフェイスを課すのではなく、興味を持った側面(価値基準)で切り分ける。
そして切り分けた時の関係を登録しておくという感じにする。

切り分けた側面から、それを手にするAPIがあるかのようにmappingを行い欲しいものを取り出すというような感じ。

具体例

もう少し具体的に書いてみる。
例えば、大きさ(size)を計算する必要があるものについて、2つのポリシーを持つもの同士を混ぜ合わせて利用したいとき。

  • 一方は大きさをsizeとして扱っている
  • 一方は大きさをlengthとして扱っている。
Mixinの例

例えば、全てをsizeとして扱うようにして、obj.lengthでアクセスするオブジェクトをラップするようなクラスを作って使う。
でも、どこかで明示的にラッパークラスで包んだりしないといけない。

#Aproject

from zope.interface import Interface
from zope.interface import Attribute

class IAProjectObject(Interface):
    size = Attribute("size of object")

class IBProjectObject(Interface):
    length = Attribute("length of object")

class HasSizeDecorator(object):
    """ a object that obj.length object treats as obj.size object.
    """
	implements(IAProjectObject)
    def __init__(self, has_length):
        self.obj = has_length
        
    @property
    def size(self):
        return self.obj.length

    @property
    def __getattr__(self, k):
        return getattr(self, k)

## とかUtilityをつくる.(条件が複雑になると死ねる?かも?)
def size(o):
    return o.size if hasattr(o, "size") else o.length

結局、どちらがわに実装を寄せるとか、実際大きさを調べたい時適宜使い分けるなどする。

Adapterを使った場合はどうするか。

サイズが欲しいので、sizeという概念で区切る。ここではISizeということにする。
そういうインターフェイスを実装するクラスを作ってあげる。

作ったクラスを登録してあげる。元のクラスにはまったく手を加えない。

実際に利用する際には、オブジェクトがAprojectのものなのか。Bprojectのものなのか考える必要はなくなる。
利用する際に考えるのは「サイズが欲しい」だけなので。

from zope.interface.registry import Components
component = Components("my")

class ASize(object):
    implements(ISize)
    def __init__(self, a):
        self.size = a.size
        self.obj = a

class BSize(object):
    implements(ISize)
    def __init__(self, b):
        self.size = b.length
        self.obj = b

component.registerAdapter(BSize, (IBProjectObject, ), ISize)
component.registerAdapter(ASize, (IAProjectObject, ), ISize)

print component.queryAdapter(B(10), ISize).size #10
print component.queryAdapter(A(10), ISize).size #10

おもしろい。
機能を合成したり一ヶ所にまとめる瞬間が存在しなくて、緩やかな関係が裏側に存在するような感じ。

*1:ZCAの中の話。adapterパターンとかと関連があるかは良く分かんない。ごめんなさい。