Update 2023.11.17 2017.05.05

Python デザインパターン サンプルコード Abstract Factory
Mark Summerfield『実践 Python 3』デザインパターンのサンプルコード
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

著作権の問題があるので,本に書いてないことだけを解説します。つまり,視点を変えて解説します。

◆◆Abstract Factory パターンの使われる場面◆◆

このサンプルのように部品を組み合わせて製品を作る場合,製品が違っても部品を替えるだけで組み立て方法が同じであれば,Abstract Factory パターンが適用できる。

巻末のサンプルの出力(製品と言われるもの)を見てもらえば,違う製品で組み立て方法が同じであるという意味が判っていただけると考えます。

Webページは,構成が完全に同じでも部品を替えるとまったく違ったものが出来上がるということはよく見られます。その他の応用では,"Kit"という名前を付けたものが Abstract Factory パターンを使っているそうです。

(2023-11-17)Python3.11で動作確認済み

【重要な注意】本ソースコードファイルを起動するには第2引数が必要です。ターミナルからPythonを起動するとき,普通はプロンプト「'>'」の後に次のようにタイプします。

>python diagram1.py -P(ケースバイケースで)

ターミナルによっては'python'は'python3'になります。ソースコードファイル(スクリプトファイル)名称が第1引数です。第2引数は本プログラムに読み込むオプションです。

「PyCharm」では,次の枠に第2引数を設定します。
  メニュー→run→Edit Configurations...→左下矢印と右上矢印の枠
  ファイル名→Modify Run Configuration...→左下矢印と右上矢印の枠
  画面によっては,Interpreter options: という枠です(左下矢印と右上矢印の枠)

「Jupyter Notebook」では,自己テストの1行目に「sys.argv[1]=」を入れます。「PyCharm」では,この行はありません。「Jupyter Notebook」にこの行を入れるのは,ハードコードと言い,固有名詞や固有な数値をコマンドラインに埋め込むことは本来避けるべきですが,この場合はやむを得ないとしましょう。ちなみにこのコードでは第1引数のような扱いになっていますが,ターミナルのコマンドラインでは第2引数なので混乱しないように。

「sys.argv[1]=」の前かまたは冒頭に「import sys」を忘れないように。

.pyではターミナルから実行されたとき自己テストが実行され,他のファイルから呼ばれたときは自己テストは無視されることも忘れないように。「Jupyter Notebook」では呼ばれる側を上側に置き,下側に参照されるようにします(本記事に関係ないかも)。


本プログラムはファイルを出力します。その使い方は不明です。

「PyCharm」上に「Jupyter Notebook」上に出力されるのは出力ファイルのディレクトリだけです。そのディレクトリは「os.path」で指定しています。

wrote C:\Users\yamak\AppData\Local\Temp\diagram.txt
wrote C:\Users\yamak\AppData\Local\Temp\diagram.svg

「AppData」は隠しフォルダであり,

エクスプローラ(ユーザーアカウント)→表示→表示→隠しファイル

により,エクスプローラに他のフォルダのように表示されます。

上のディレクりのフォルダの中にはたくさんのファイルが有りますが,更新日時でソートすれば1番目に見つかります。


◆◆Abstract Factory パターンとは◆◆

GoFによれば,Abstract Factory パターンの目的は, 「互いに関連したり依存し合うオブジェクト群を,その具象クラスを明確にせずに生成するためのインタフェースを提供する。」

GoFによれば,Abstract Factory パターンの別名は,Kit パターンである。

GoFによれば,Abstract Factory パターンは,以下のような場合に有効である。
・システムを部品の生成,組み合わせ,表現の方法から独立にすべき場合。
・部品の集合が複数存在して,その中の1つを選んでシステムを構築する場合。
・一群の関連する部品を常に使用しなければならないように設計する場合。
・部品のクラスライブラリを提供する際に,インタフェースだけを公開して,実装は非公開にしたい場合。

GoFによれば,Abstract Factory パターンの関連するパターンは,
Abstract Factory クラスは,しばしば Factory Method パターンを使って実装されるが,Prototype パターンを使って実装することも可能である。
生成されるオブジェクトは,しばしば,Singleton オブジェクトである。

◆◆Abstract Factory パターンのサンプルのクラス構成◆◆

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
サンプルは,diagram1.py, diagram2.py の2つありますが,著者が言うには,前者は実装には欠点があるそうなので,後者のみ解説します。

【diagram2.py】
    def create_diagram

          ↑(参照)
    DiagramFactory     ( Diagram     Rectangle     Text )
   (make_diagram,      (add, save)
    make_rectangle,
    make_text)
          ↑(継承)
    SvgDiagramFactory   ( Diagram     Rectangle     Text )
                        (add, save)
class DiagramFactory, class SvgDiagramFactory が工場である。
class Diagram が製品である。
class Rectangle, class Text が部品である。

サンプル diagram2.py は,典型的な Abstract Factory パターンのクラス構成と少し違うようである。何よりも Abstract Factory がない。

典型的な Abstract Factory パターンのクラス構成はこちらを参照してほしい。
https://yamakatsusan.web.fc2.com/hp_software/pythonpattern08.html
『Abstract Factory パターン 結城 浩「Java言語で学ぶデザインパターン入門」をPython化』

◆◆Abstract Factory パターンの実装◆◆

冒頭にも書きましたが,Abstract Factory パターンとは,同じ製作工程を持つ工場が複数ありまして,工場ごと入れ替えれば,同じ製作工程で違う製品を作ることができます。

サンプルの出力を見てみましょう。前者は,例によって,プレーンテキストで絵を描画したものです。後者は,HTMLファイルに,ベクターグラフィクスを埋め込むもので,ドットではなく SVG コードでできています。例えば,動物の絵などがコードで作れるのです。コードは,このWebのソースを見てください。

【class DiagramFactoryの出力】
+----------------------------+
|   +--------------------+   |
|   |%%%%%%%%%%%%%%%%%%%%|   |
|   |%%Abstract Factory%%|   |
|   |%%%%%%%%%%%%%%%%%%%%|   |
|   +--------------------+   |
+----------------------------+
【class SvgDiagramFactoryの出力】
Abstract Factory

さて,Abstract Factory パターンでは,工場をどのように作るのでしょうか。

典型的な方法は,1工場1モジュールにして,さらに,Abstract Factory モジュールも作ります。

サンプルの方法は,1工場1クラスにして,製品や部品は,クラス内クラスとしています。さらに,
  class DiagramFactory
  class SvgDiagramFactory
が,親子関係になっています。

別にこれでも良いのですが,スーパークラス Abstract Factory を作り,上の2つを具象工場として,入れ替えが可能になるようにすると,構成がすっきりすると考えられます。ただ,親クラスの方を debug 用と割り切れば,この構成も役に立ちます。

クラス内クラスの
  class Rectangle
  class Text
は,プレーンテキスト/SVGコードで,周りの囲いと中心の文字列を作っています。これらは部品です。

クラス内クラスの
  class Diagram
は,プレーンテキスト/SVGコードのファイルを出力します。製品に相当します。

class DiagramFactoryには,
  def make_diagram
  def make_rectangle
  def make_text
の3つのクラスメソッドがあり,クラス内クラスの製品や部品をインスタンス化します(正確には,インスタンス化の戻り値を提供)。もちろん,その外クラスの
  class DiagramFactory
  class SvgDiagramFactory
のどちらにも対応できます。

インデントが0のグローバル関数
  def create_diagram
は,クラス内クラスの製品や部品をインスタンス化しています。工場である外クラスは引数として渡されるので,どちらの工場であるかを気にしないでインスタンス化しています。Python では,インスタンスをつくるのをファクトリと呼びます。このグローバル関数とクラスメソッドがそれです。

このように,オブジェクト指向では,if 文を使わないで場合分けができるようになります。

インデントが0のグローバル関数
  def main
が,メインルーチンであり,工場に指示しているところです。ここで,工場の入れ替えなどができるのです。

コマンドラインの第2引数を"-P"とすることによって,製品が標準出力に出力されます。debugに使えます。

Abstract Factory パターンの注意事項として,すべての工場の製作工程は同じでなければならないということです。製作工程を変更すると,すべての工場に影響を及ぼすので注意が必要です。

◆◆ソースコード◆◆

このWebの左上隅からダウンロードできます。

ソースファイルは2つです。2つのサンプル出力を添付しました。
・diagram1.py ; Abstract Factory パターンのサンプル(その1)
・diagram2.py ; Abstract Factory パターンのサンプル(その2)

【diagram2.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#!/usr/bin/env python3
# Copyright c 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.

import os
import sys
import tempfile


def main():
    if len(sys.argv) > 1 and sys.argv[1] == "-P": # For regression testing
        create_diagram(DiagramFactory).save(sys.stdout)
        create_diagram(SvgDiagramFactory).save(sys.stdout)
        return
    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)


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:

    @classmethod
    def make_diagram(Class, width, height):
        return Class.Diagram(width, height)


    @classmethod
    def make_rectangle(Class, x, y, width, height, fill="white",
            stroke="black"):
        return Class.Rectangle(x, y, width, height, fill, stroke)

    @classmethod
    def make_text(Class, x, y, text, fontsize=12):
        return Class.Text(x, y, text, fontsize)


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


    class Diagram:

        def __init__(self, width, height):
            self.width = width
            self.height = height
            self.diagram = DiagramFactory._create_rectangle(self.width,
                    self.height, DiagramFactory.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()


    class Rectangle:

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


    class Text:

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


    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] = DiagramFactory.HORIZONTAL
            rows[height - 1][x] = DiagramFactory.HORIZONTAL
        for y in range(1, height - 1):
            rows[y][0] = DiagramFactory.VERTICAL
            rows[y][width - 1] = DiagramFactory.VERTICAL
        for y, x in ((0, 0), (0, width - 1), (height - 1, 0),
                (height - 1, width -1)):
            rows[y][x] = DiagramFactory.CORNER
        return rows


class SvgDiagramFactory(DiagramFactory):

    # The make_* class methods are inherited

    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 Diagram:

        def __init__(self, width, height):
            pxwidth = width * SvgDiagramFactory.SVG_SCALE
            pxheight = height * SvgDiagramFactory.SVG_SCALE
            self.diagram = [SvgDiagramFactory.SVG_START.format(**locals())]
            outline = SvgDiagramFactory.Rectangle(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" + SvgDiagramFactory.SVG_END)
            finally:
                if isinstance(filenameOrFile, str) and file is not None:
                    file.close()


    class Rectangle:

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


    class Text:

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


if __name__ == "__main__":
    main()

以上

トップページに戻る
inserted by FC2 system