読者です 読者をやめる 読者になる 読者になる

ゆくゆくは有へと

おかゆ/オカ∃/大鹿有生/彼ノ∅有生 の雑記

「実践Python3」の Factory method がもやもやする

題のごとし。

ググって理解したところによれば、factory method って「具象クラスごとに、同種手続き(たいてい抽象クラスで定義されてるメソッド)時に呼び出されるクラス可変的なコンストラクタをメソッド化したもの(で、具象クラス実装時に中身が書かれるもの)」っぽい。

で、「同種手続き」というのを抽象クラスのレベルで書いちゃうというのがどうやら template method パティーンらしい。ググった感じだと、factory method パティーンは template method パティーンに更に組み込んで使うっぽい。

んだけど、「実践Python3」の場合、この「同種手続き」というのに共通性がほとんどなくて、「えぇ…」ってなっちゃった。

たぶん、Board の上に駒を置いていく操作、populate_board が同種手続き(factory method パティーンでいうanOperation?)に相当しそうなんだけど、そもそも anOperation は具象クラスでオーバーライドしない(したら意味ないし)から、「???」って感じだ。

でも、create_piece は「ファクトリー関数」らしくて、これのおかげで、populate_board は「「ファクトリーメソッド」と呼ぶことができる」らしい。そうすると、同書においては、__init__ が anOperation に相当して、populate_board がファクトリーメソッドということになるのだろうか…。

Factory Method パターン - Wikipedia 見てたら、

factoryMethodは、[...] パラメータを取り、それによって生成するクラスを変えることもある。

ともあるので、それならむしろ create_piecefactoryMethod では……?

そう考えると、AbstractBoard はこうしたくなる(ちなみに個人的な趣味で ABC にしておいた)

from abc import ABCMeta, abstractmethod

class AbstractBoard(metaclass=ABCMeta):

    def __init__(self, rows, columns):
        self.rows = rows
        self.columns = columns
        self.board = [[None for _ in range(columns)] for _ in range(rows)]
        self.populate_board()

    def populate_board(self):
        for row in range(self.rows):
            for column in range(self.columns):
                self.board[row][column] = self.create_piece(row, column)

    @abstractmethod
    def create_piece(row, column):
        pass

    def __str__(self):
        ...

populate_board は各マスに対して、サブクラスで実装されたcreate_piece(row, column) を呼び出して相応しいインスタンスをもらえる。populate_boardanOperation に相当するはずで、個別の具象ボードにおいて共通のプロセス。

ほんで、ChessBoard 具象クラスでは

class ChessBoard(AbstractBoard):

    def __init__(self):
        super().__init__(8, 8)

    def create_piece(self, row, column):
        piece_dict = self._init_piece_map()
        name_dict = {PAWN: "ChessPawn", ROOK: "ChessRook",
                     KNIGHT: "ChessKnight", BISHOP: "ChessBishop",
                     KING: "ChessKing", QUEEN: "ChessQueen"}
        if (row, column) in piece_dict:
            kind, color = piece_dict[(row, column)]
            name = name_dict[kind]
            return globals()[color + name]()
        else:
            return None

    def _init_piece_map(self):
        colors = (((0, 1), BLACK), ((6, 7), WHITE))
        kinds = (((0, 7), ROOK), ((1, 6), KNIGHT), ((2, 5), BISHOP),
                 ((3,), QUEEN), ((4,), KING), ((0, 1, 2, 3, 4, 5, 6, 7), PAWN))
        piece_dict = {(row, column): (kind, color)
                      for row, color in colors for column, kind in kinds}
        return piece_dict

ヘルパーメソッドとして _init_piece_map を用意した。_init_piece_map で初期状態における各コマの座標の辞書を返してもらって、今の位置がその辞書にあれば相応のインスタンスを渡してあげるという形。

んん…でもこれならもういっそこうした方が…

from abc import ABCMeta, abstractmethod

class AbstractBoard(metaclass=ABCMeta):

    def __init__(self, rows, columns):
        self.rows = rows
        self.columns = columns
        self.board = [[None for _ in range(columns)] for _ in range(rows)]
        self.populate_board()

    def populate_board(self):
        for row in range(self.rows):
            for column in range(self.columns):
                self.board[row][column] = self.create_piece(row, column)

    def create_piece(self, row, column):
        piece_dict = self.initial_piece_map()
        name_dict = self.get_name_dict()
        if (row, column) in piece_dict:
            kind, color = piece_dict[(row, column)]
            name = name_dict[kind]
            return globals()[color + name]()
        else:
            return None

    @abstractmethod
    @staticmethod
    def initial_piece_map():
        pass

    @abstractmethod
    @staticmethod
    def get_name_dict():
        pass

    def __str__(self):
        ...


class ChessBoard(AbstractBoard):

    def __init__(self):
        super().__init__(8, 8)

    @staticmethod
    def get_name_dict():
        name_dict = {PAWN: "ChessPawn", ROOK: "ChessRook",
                     KNIGHT: "ChessKnight", BISHOP: "ChessBishop",
                     KING: "ChessKing", QUEEN: "ChessQueen"}
        return name_dict

    @staticmethod
    def initial_piece_map():
        colors = (((0, 1), BLACK), ((6, 7), WHITE))
        kinds = (((0, 7), ROOK), ((1, 6), KNIGHT), ((2, 5), BISHOP),
                 ((3,), QUEEN), ((4,), KING), ((0, 1, 2, 3, 4, 5, 6, 7), PAWN))
        piece_dict = {(row, column): (kind, color)
                      for row, color in colors for column, kind in kinds}
        return piece_dict

今回のテーマだと本質的に差異が生じるところって「どこにどんな駒を置くのか」なので、それを具象クラスで決めるという風にしたほうが簡潔そう…。特に、駒の位置情報を司るinitial_piece_map が分離するおかげで、外部ファイルからの読み込みに変更するのも簡単。

ただ、これが factory method パティーンなのかと言われると……。そもそもこのテーマ設定がファクトリーメソッドパティーンに向いてないのかしら。

しかし考えてみれば、population の仕方を抽象メソッドで固定してしまうというのはハードコーディングな気もする。population の仕方は同書のやり方もあるし、おかゆのやり方もあるわけで(たとえばおかゆのやり方は、列真ん中付近の無駄なところまで走査してるので、パフォーマンスが悪い)。そうなると、やはり @abstractmethod にするのは populate_board にしておくべきなのだろうか…。

いや、でも、仮にそうだったとしても、populate_board が factory method とは思えない。こいつは別に anOperation (この状況でいえば__init__) に対してクラス差異的にオブジェクトを生成してるわけではなくて、個別クラス特有的にインスタンスの内部状態を変更してるだけ。単なる素朴なオーバーライドにしか見えない。

もやもやする

追記

2016-11-29-15:20

えーっと。factory パターンというのがあるのを知った。理解によれば、「factoryクラスに指示(引数)を与えて、それに見合ったオブジェクトを持ってきてくれるというクールなやつをつくる」パティーンのことらしい。言い方としては「指示に見合ったクラスのインスタンスを作り出す」といったほうがいいか(関数でよくない…?シンプルなファクトリなら関数でもできる(create_pieceがまさにそう)けど、ファクトリの内部状態によって指示への従い方を変えていく…みたいなことをするなら確かにクラスである必要がありそう(でもファクトリってそんな複雑なことする(していい)のかな))。

個人的には router みも感じる。

これを踏まえると、なるほど、create_piece は確かに「ファクトリー関数」ではあっても factory method ではない気がしてきた。

じゃあ、問題はどれがanOperation で、factory method なのか。

サルでもわかる 逆引きデザインパターン 第2章 逆引きカタログ ロジック編 Factory/Factory Method(ファクトリ/ファクトリメソッド) をみる。

それでは、ファクトリパターンとファクトリメソッドパターンの違いは何でしょうか?

ファクトリパターンは、オブジェクトの生成処理だけでなく、どのオブジェクトを生成するかの判断もオブジェクトェクトの使用者から隠してくれるパターンです。 ファクトリパターンでは、生成するオブジェクトの種類の変更をファクトリの処理の中で動的に行います。

しかし、ファクトリパターンでは、生成するオブジェクトの種類が増えたり、生成処理手順が複雑化した場合、ファクトリ内の処理が冗長で複雑になってしまいます。 そこで登場するのがファクトリメソッドパターンです。

せやな。続き:

ファクトリメソッドパターンでは、生成するオブジェクトごとにファクトリを用意し、ファクトリに対して共通のスーパークラスを設けることで、オブジェクトの生成処理を柔軟に行います。 スーパークラスでは、オブジェクト生成に共通な処理の実装と、オブジェクトのnewを行うメソッドやオブジェクト固有の生成手順を抽象メソッドとして定義します。 サブクラスでは、抽象メソッドの実装(オブジェクトのnew)を行うだけです。 オブジェクトの生成はサブクラスで行い、サブクラスでは生成処理の差分のみを実装すればよいので、生成処理を簡略化できます(注4)。

ファクトリメソッドパターンは、1つのファクトリは1つのオブジェクトの生成のみを行うため、生成するオブジェクトの種類の変更を行う場合、ファクトリクラスを切り替える必要があります。 ファクトリメソッドパターンでは多くの場合、オブジェクトの使用者はファクトリのスーパークラスを使用します。 そしてファクトリ指定は、オブジェクト使用者の生成時にコンストラクタで渡したり、ファクトリ設定用メソッドを設けるなどの手段が必要になります。

やっぱりとにかくファクトリメソッドパターンというのは

  • 共通処理は抽象クラスで
  • 差分処理(たいていコンストラクタの指定)は具象クラスで

だと思うんだよな。で、ファクトリである以上、多分、クライアントに渡されるのはプロダクト(を加工したもの)であるべきで、さらにそのプロダクトというのはファクトリーメソッドによって可変的に持ってきたもの。

と考えたときに、やっぱり「実践Python3」のゲームボードの例は気持ちが悪い…。

なんか、もしかして「ファクトリ関数を中に含んだメソッド」をファクトリメソッドと呼んでるんじゃないかコイツ?という感がしてきた。

あ~ もやもやする

追記

16:50

これを Factory method パターンと理解しようとするのを諦めた。

で、こういうの見つけた

doloopwhile.hatenablog.com

あまりにも自由度が高くなってきたらクラスを引数で渡してやれっていう…えぇ…いやそりゃそうするけど……

なんか基本的に批判的な目で読んだほうがいいんかも分からんね(まあ元々がC++デザインパターンだし仕方ないけども)