生成に関するデザインパターン

Last Change: 09-Mar-2016.
author : qh73xe

この章では オブジェクトの生成方法に関してのデザインパターンを紹介します。 一般に python でオブジェクトを生成するときには、クラスオブジェクトに引数を与えて呼ぶわけですが、 このオブジェクトの生成方法に柔軟性を与えたい時に使用していくデザインパターンです。

ここでは 5 つのデザインパターンを紹介しますが、 python を使いこなして行く上で すべてが全て全く異なっておりかつ、重要という意味ではありません。 そもそも デザインパターンというものは python 以前の産物で C++ を前提に考えられています。 だから C++ の制約のために生まれたパターンというものもあり python では関係のない問題も多々含んでいます。

Abstract Factory

まず第一に紹介するデザインパターンは Abstract Factory です。 これは「他のオブジェクトから複雑なオブジェクトを生成したい時、かつ、その構成オブジェクトがある特定のファミリーに属している場合」に使用します。

こうして言葉で説明すると面倒な話ですが、具体例で考えてみればよくある問題です。 例えば、何かの web アプリの出力を考えてみてください。 このアプリケーションは何かの API を叩いて結果をまとめてくれます。 一般的にはブラウザを通して HTML にして表示をすればよいです。 しかしある日、上司にこう言われます。

「この結果 CSV にしてまとめて?」

或いは別の日にこう言われます。

「さっきの PDF にしたいんだけど」

このような場合、 Abstract Factory を作成します。 このクラスはサブクラスとして HtmlFactory, CSVFactory, PdfFactory を持ちます(このように実際にある挙動を担うクラスを具象クラスといいます)。 これらのサブクラスは、例えば表として表示を行う make_table()、 リスト形式で表示を行う make_list() などが、それぞれの拡張子にあったスタイルで 実装されています。

このような状態にすれば、Abstract Factory クラスに、それぞれのインスタンスを引数にとる create_dialog() のような汎用的な関数が作成できるようになります。 このようなクラスの構成方法を Abstract Factory パターンといいます。

実戦 1

ともかく実例を見ていきましょう。 ここでは簡単な図形を作成するプログラムについて検討します。

このプログラムで見ていきたいものは、 Abstract Factory なので、その他の部分は適当に考えます。 とりあえず、簡単な図形として四角形を考えましょう。 そして四角形の中には文字が入っています。 四角形の各辺の長さは適当に決め打ちをしていく感じにします。

そして肝心の Abstract Factory の部分ですが、 今回は上記のような図を text 形式と svg 形式の二種類のアウトプットにすることを 考えましょう。

このスクリプトの実行時の挙動は以下のようにします。

if __name__ == "__main__":
    import os
    import tempfile

    textFilename = os.path.join(tempfile.gettempdir(), "diagram.txt")
    svgFilename = os.path.join(tempfile.gettempdir(), "diagram.svg")

    txtDiagram = create_diagram(DiagramFactory())
    txtDiagram.save(textFilename)
    print("wrote", textFilename)

    svgDiagram = create_diagram(SvgDiagramFactory())
    svgDiagram.save(svgFilename)
    print("wrote", svgFilename)

OS, tempfile ライブラリを読み込んでいるのはアウトプットファイルの出力先を決めるためだけです。 とりあえず、アウトプット先は実行した環境の一時ファイル置き場とし、 そこに txt, svg 拡張子でファイルをおきます。 これを適宜しているのが、 4, 5 行目です。 これは単純な文字列なので、これ以上の説明はいらないでしょう。

7, 8, 9 行目、 11, 12, 13 行目が今回の趣旨です(正確には 9, 13 行目は出力先を print しているだけなのでどうでもよいですが). ここで create_diagram() を使いアウトプット用のインスタンスを作成し、その save メソッドで任意の図を作成します。 つまり create_diagram() がここでいう抽象ファクトリーです。

この create_diagram は以下のように作成します。

def create_diagram(factory):
    diagram = factory.make_diagram(30, 7)
    rectangle = factory.make_rectangle(4, 1, 22, 5, "yellow")
    text = factory.make_text(7, 3, "Abstract Factory")

    diagram.add(rectangle)
    diagram.add(text)
    return diagram

この関数は factory を唯一の引数としています。 python の実装上、引数レベルでの制約をかけることはしていませんが、 この関数を成立させるためには引数で与えられるクラスは以下の3つの関数を持つ必要があります。

  • factory.make_diagram()
  • factory.make_rectangle()
  • factory.make_text()

まず make_diagram 関数ですが、これは描画領域そのものであると考えてください。 この関数は二つの引数、幅と高さを受け取り、実体化します。 この際生成されるクラスを create_diagram 関数は返します。 つまり、 main 部分の処理でおこなっていた save メソッドはここで生成されるクラスのものです。 また、このクラスは add メソッドを持ちます。 これは描画領域に何かをのせる作業であると思えばよいです。 一方残りの二つの関数は 描画される四角形と文字のオブジェクトを作成します。

とりあえずこの関数の挙動は上で説明できたと思うので、 この関数で引数となる factory はどのようなクラスを作成したのかという話を以下でしていきます。

class DiagramFactory:

    def make_diagram(self, width, height):
        return Diagram(width, height)

    def make_rectangle(self, x, y, width, height, fill="white", stroke="black"):
        return Rectangle(x, y, width, height, fill, stroke)

    def make_text(self, x, y, text, fontsize=12):
        return Text(x, y, text, fontsize)


class SvgDiagramFactory(DiagramFactory):

    def make_diagram(self, width, height):
        return SvgDiagram(width, height)

    def make_rectangle(self, x, y, width, height, fill="white", stroke="black"):
        return SvgRectangle(x, y, width, height, fill, stroke)

    def make_text(self, x, y, text, fontsize=12):
        return SvgText(x, y, text, fontsize)

問題の通り、 text 形式用と svg 用の二つのクラスを作成しています。 上記の通り、この二つのクラスは make_diagram, make_rectangle, make_text という同名の関数を持っています。 これらの関数の大きな差は返り値です。 DiagramFactory クラスでは Diagram, Rectangle, Text クラスを返しますが、 SvgDiagramFactory クラスでは SvgDiagramFactory, SvgRectangle, SvgText クラスを返します。

ここで SvgDiagramFactory は DiagramFactory を継承していることに注意してください。 つまり、この例では DiagramFactory は SvgDiagramFactory の基底クラスとしても利用しています。 このように Abstract Factory パターンは名前に Abstract と付きますが、一つのクラスをインターフェースを提供する基底クラスと、具象クラスの両方に利用するのが一般的です。

で、最後の実装は make_diagram(), make_rectangle(), make_text() が返すクラスを定義することです。 まずが text 形式のクラスを見ていきます。

BLANK = " "
CORNER = "+"
HORIZONTAL = "-"
VERTICAL = "|"


class Diagram:

    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.diagram = _create_rectangle(self.width, self.height, BLANK)

    def add(self, component):
        for y, row in enumerate(component.rows):
            for x, char in enumerate(row):
                self.diagram[y + component.y][x + component.x] = char

    def save(self, filenameOrFile):
        file = None if isinstance(filenameOrFile, str) else filenameOrFile
        try:
            if file is None:
                file = open(filenameOrFile, "w", encoding="utf-8")
            for row in self.diagram:
                print("".join(row), file=file)
        finally:
            if isinstance(filenameOrFile, str) and file is not None:
                file.close()


def _create_rectangle(width, height, fill):
    rows = [[fill for _ in range(width)] for _ in range(height)]
    for x in range(1, width - 1):
        rows[0][x] = HORIZONTAL
        rows[height - 1][x] = HORIZONTAL
    for y in range(1, height - 1):
        rows[y][0] = VERTICAL
        rows[y][width - 1] = VERTICAL
    for y, x in ((0, 0), (0, width - 1), (height - 1, 0), (height - 1, width - 1)):
        rows[y][x] = CORNER
    return rows


class Rectangle:

    def __init__(self, x, y, width, height, fill, stroke):
        self.x = x
        self.y = y
        self.rows = _create_rectangle(width, height, BLANK if fill == "white" else "%")

一方、SVG の方は以下のようなクラスを作成しました。

SVG_START = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
    "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
    width="{pxwidth}px" height="{pxheight}px">"""

SVG_END = "</svg>\n"

SVG_RECTANGLE = """<rect x="{x}" y="{y}" width="{width}" \
height="{height}" fill="{fill}" stroke="{stroke}"/>"""

SVG_TEXT = """<text x="{x}" y="{y}" text-anchor="left" \
font-family="sans-serif" font-size="{fontsize}">{text}</text>"""

SVG_SCALE = 20


class SvgDiagram:

    def __init__(self, width, height):
        pxwidth = width * SVG_SCALE
        pxheight = height * SVG_SCALE
        self.diagram = [SVG_START.format(**locals())]
        outline = SvgRectangle(0, 0, width, height, "lightgreen", "black")
        self.diagram.append(outline.svg)

    def add(self, component):
        self.diagram.append(component.svg)

    def save(self, filenameOrFile):
        file = None if isinstance(filenameOrFile, str) else filenameOrFile
        try:
            if file is None:
                file = open(filenameOrFile, "w", encoding="utf-8")
            file.write("\n".join(self.diagram))
            file.write("\n" + SVG_END)
        finally:
            if isinstance(filenameOrFile, str) and file is not None:
                file.close()


class SvgRectangle:

    def __init__(self, x, y, width, height, fill, stroke):
        x *= SVG_SCALE
        y *= SVG_SCALE
        width *= SVG_SCALE
        height *= SVG_SCALE
        self.svg = SVG_RECTANGLE.format(**locals())


class SvgText:

    def __init__(self, x, y, text, fontsize):
        x *= SVG_SCALE
        y *= SVG_SCALE
        fontsize *= SVG_SCALE // 10
        self.svg = SVG_TEXT.format(**locals())

最後のクラスの作成に関しては、 ようは、どのようにアウトプットをしたいのかを実装する部分であるので、 詳細な解説は省きます。

一応、すべてのソースを示すと以下のようになります。

# -*- coding: utf-8 -*

# Copyright © 2012-13 Qtrac Ltd. All rights reserved.
# This program or module is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. It is provided for
# educational purposes and is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.


def create_diagram(factory):
    diagram = factory.make_diagram(30, 7)
    rectangle = factory.make_rectangle(4, 1, 22, 5, "yellow")
    text = factory.make_text(7, 3, "Abstract Factory")

    diagram.add(rectangle)
    diagram.add(text)
    return diagram


class DiagramFactory:

    def make_diagram(self, width, height):
        return Diagram(width, height)

    def make_rectangle(self, x, y, width, height, fill="white", stroke="black"):
        return Rectangle(x, y, width, height, fill, stroke)

    def make_text(self, x, y, text, fontsize=12):
        return Text(x, y, text, fontsize)


class SvgDiagramFactory(DiagramFactory):

    def make_diagram(self, width, height):
        return SvgDiagram(width, height)

    def make_rectangle(self, x, y, width, height, fill="white", stroke="black"):
        return SvgRectangle(x, y, width, height, fill, stroke)

    def make_text(self, x, y, text, fontsize=12):
        return SvgText(x, y, text, fontsize)


BLANK = " "
CORNER = "+"
HORIZONTAL = "-"
VERTICAL = "|"


class Diagram:

    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.diagram = _create_rectangle(self.width, self.height, BLANK)

    def add(self, component):
        for y, row in enumerate(component.rows):
            for x, char in enumerate(row):
                self.diagram[y + component.y][x + component.x] = char

    def save(self, filenameOrFile):
        file = None if isinstance(filenameOrFile, str) else filenameOrFile
        try:
            if file is None:
                file = open(filenameOrFile, "w", encoding="utf-8")
            for row in self.diagram:
                print("".join(row), file=file)
        finally:
            if isinstance(filenameOrFile, str) and file is not None:
                file.close()


def _create_rectangle(width, height, fill):
    rows = [[fill for _ in range(width)] for _ in range(height)]
    for x in range(1, width - 1):
        rows[0][x] = HORIZONTAL
        rows[height - 1][x] = HORIZONTAL
    for y in range(1, height - 1):
        rows[y][0] = VERTICAL
        rows[y][width - 1] = VERTICAL
    for y, x in ((0, 0), (0, width - 1), (height - 1, 0), (height - 1, width - 1)):
        rows[y][x] = CORNER
    return rows


class Rectangle:

    def __init__(self, x, y, width, height, fill, stroke):
        self.x = x
        self.y = y
        self.rows = _create_rectangle(width, height, BLANK if fill == "white" else "%")


class Text:

    def __init__(self, x, y, text, fontsize):
        self.x = x
        self.y = y
        self.rows = [list(text)]


SVG_START = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
    "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
    width="{pxwidth}px" height="{pxheight}px">"""

SVG_END = "</svg>\n"

SVG_RECTANGLE = """<rect x="{x}" y="{y}" width="{width}" \
height="{height}" fill="{fill}" stroke="{stroke}"/>"""

SVG_TEXT = """<text x="{x}" y="{y}" text-anchor="left" \
font-family="sans-serif" font-size="{fontsize}">{text}</text>"""

SVG_SCALE = 20


class SvgDiagram:

    def __init__(self, width, height):
        pxwidth = width * SVG_SCALE
        pxheight = height * SVG_SCALE
        self.diagram = [SVG_START.format(**locals())]
        outline = SvgRectangle(0, 0, width, height, "lightgreen", "black")
        self.diagram.append(outline.svg)

    def add(self, component):
        self.diagram.append(component.svg)

    def save(self, filenameOrFile):
        file = None if isinstance(filenameOrFile, str) else filenameOrFile
        try:
            if file is None:
                file = open(filenameOrFile, "w", encoding="utf-8")
            file.write("\n".join(self.diagram))
            file.write("\n" + SVG_END)
        finally:
            if isinstance(filenameOrFile, str) and file is not None:
                file.close()


class SvgRectangle:

    def __init__(self, x, y, width, height, fill, stroke):
        x *= SVG_SCALE
        y *= SVG_SCALE
        width *= SVG_SCALE
        height *= SVG_SCALE
        self.svg = SVG_RECTANGLE.format(**locals())


class SvgText:

    def __init__(self, x, y, text, fontsize):
        x *= SVG_SCALE
        y *= SVG_SCALE
        fontsize *= SVG_SCALE // 10
        self.svg = SVG_TEXT.format(**locals())


if __name__ == "__main__":
    import os
    import tempfile

    textFilename = os.path.join(tempfile.gettempdir(), "diagram.txt")
    svgFilename = os.path.join(tempfile.gettempdir(), "diagram.svg")

    txtDiagram = create_diagram(DiagramFactory())
    txtDiagram.save(textFilename)
    print("wrote", textFilename)

    svgDiagram = create_diagram(SvgDiagramFactory())
    svgDiagram.save(svgFilename)
    print("wrote", svgFilename)

実例2

今迄の DiagramFactory とそのサブクラスである SvgDiagram は それぞれのファクトリーに相応しいクラスを作っており、上手く機能します。

ただし、python 的な記述として考えるといくつかの欠点があります。

  • ファクトリーについて実体化する必要がない
  • DiagramFactory と SvgDiagram は実態としてほぼ同じである
  • 最上位の名前空間にはすべてのクラスが含まれている

とくに最後の欠点について言えば我々がアクセスする必要があるクラスは 二つのファクトリーだけです。 svg 関連のクラスは名前の冒頭に svg を付けているだけでコードの重複が多いです。

これらの欠点を解消する方法として Diagram, Rectangle, Text の 3 つのクラスを Diagram Factory クラスの中にネスト化することです。