はじめに
京都リサーチパークにて開催のYAPC::Kyotoでノベルティを受け取りし参加者の1人、nikkieです。
読書会駆動でミノ駆動本(『良いコード/悪いコードで学ぶ設計入門』)を読んでいます。
7章ではファーストクラスコレクションが紹介されます。
Pythonで実装する上で参考になりそうな先駆者の例が見つかりました。
それを見て考えたことをアウトプットします。
目次
ファーストクラスコレクション
ファーストクラスコレクション(First Class Collection)とは、コレクションに関連するロジックをカプセル化する設計パターンです。(ミノ駆動本 Kindle版 p.233)
私がファーストクラスコレクションを知ったのは『ThoughtWorksアンソロジー』がきっかけです。
5章の「オブジェクト指向エクササイズ」の中で紹介されています。
実装としては、コレクションだけをメンバ変数に持つクラスです(『ThoughtWorksアンソロジー』 5.2.9)。
コレクションを操作するメソッドをファーストクラスコレクションのクラスに持たせます。
ミノ駆動本と相性がいいとされる増田本(『現場で役立つシステム設計の原則』)では、ファーストクラスコレクションはコレクションオブジェクトとも呼ばれます。
語感から値オブジェクト1のコレクション版というような印象を受けますね。
Pythonでファーストクラスコレクション、先駆者による例
増田本のコードをPythonで実装した例が見つかりました。
アウトプットしていただき、ありがとうございます!
今回考察するのは以下の箇所です。
from __future__ import annotations from dataclasses import dataclass @dataclass(frozen=True) class Customer: id: int @dataclass(frozen=True) class Customers: customers: list[Customer] def add(self, customer: Customer) -> Customers: return Customers(self.customers.append(customer)) def as_list(self) -> tuple[Customer]: return tuple(self.customers)
- Python 3.10.9 で動かしています
- Python 3.9以降、
typing.List
の代わりに組み込み型のlist
で型ヒントできるようになったので、そちらに置き換えました(tuple
も同様です)
なるほどと思った点
dataclassにする
@dataclass
はなるほどな〜と思いました- ファーストクラスコレクションには必須ではないと思うのですが、ファーストクラスコレクション同士の一致なども判定できるようになりますし、今の私には妥当そうに思えます
- 増田本では「不変」スタイルを勧めているので、
frozen=True
の指定も納得です- コレクションの要素(
Customer
)も不変にしておく
- コレクションの要素(
変更提案したい点
大きく2点です
add
メソッドの実装(コードの振る舞いについて)as_list
という命名(コードの構造について)
1-1. appendの返り値はNone
なのです
addメソッドについてです。
Customers(self.customers.append(customer))
self.customers
はリストなので、リストのappendメソッドを呼び出しています。
そしてappendメソッドの返り値をCustomersに渡していますね。
https://docs.python.org/ja/3/tutorial/datastructures.html#more-on-lists
実はappendメソッドの返り値はNone
です。
これはappendメソッドがリストをインプレースに変換するからです。
要素を追加した新しいリストを返すわけではありません。
対話モードで確認しましょう。
>>> l = [1, 2] >>> l.append(3) >>> print(l.append(4)) # appendの返り値を出力するとNone None >>> l [1, 2, 3, 4]
Customers
クラスのadd
ではCustomers(None)
が返っています。
1-2. ファーストクラスコレクションが持つコレクションは変更しないほうがいいのではないか
appendメソッドによって、customers
属性(リストを指す)が変更されています。
例を出します(コードをスクリプトに書き、python -i script.py
でスクリプトを読み込んで対話モードに入ります)
>>> empty = Customers([]) >>> empty Customers(customers=[])
空のCustomers
を作りました。
@dataclass
により、表示がわかりやすいですね!
Customer
インスタンスをadd
してみます
>>> c1 = Customer(1) >>> add_one = empty.add(c1) >>> add_one # appendの返り値がNoneのため Customers(customers=None) >>> empty Customers(customers=[Customer(id=1)])
空だったCustomers
(変数empty
が指す)が変わっています!
増田本が強調している不変から外れてしまっていますね。
2. as_listメソッドが返すのはtuple
ファーストクラスコレクションが持つコレクションを不変にして返す実装(asList
)が増田本にあります。
これをPythonに置き換えてas_list
でtuple
を返す実装です。
たしかにtuple
はappend
メソッドがないなど、ユーザは勝手に書き換えられません。
振る舞いとしてはこれでよさそうですが、as_list
という命名はユーザが誤解しそうなので変えたいなと思いました(as_tuple
でしょうか)。
なお、JavaのUnmodifiableList
がPythonではtuple
なのかという点には議論の余地があると思います。
FAQのなぜタプルとリストという別のデータ型が用意されているのですか?を参照すると
- タプルは「型が異なっても良い関連するデータの小さな集合」
- リストは「全て同じ型の可変数のオブジェクトを持ち、それらが一つ一つ演算される傾向にあ」る
タプルとリストでデザイン上の意図が異なるので、「変更できないlist
がtuple
である」とは私はみなしていないです
なお、依存が追加されるというデメリットはありますが、frozenlistを使うというのも1つの手かと思います。
提案実装
以上をまとめて、今の私は以下のファーストクラスコレクションの実装を提案します。
- ファーストクラスコレクション自体を不変にしたく、
add
メソッドでは新しいファーストクラスコレクションを作って返す - ファーストクラスコレクションが持つコレクションを返すメソッドは
as_tuple
とした(ただし暫定解)
終わりに
なかなかよさそうな例が見つかったと感じ、特に手を動かさずに読書会に持ち寄ったのですが、読書会でワイワイ読む中で変更提案したい点が見つかり、アウトプットしました。
インプレースなメソッドなど、私も最初は間違えましたね(手痛い失敗をしたから身に着いたというのもあります)
ファーストクラスコレクションの実装はもう少し考えてみようと思います