Update 2023.11.18 2017.05.05

Python デザインパターン サンプルコード Abstract Factory
結城 浩「Java言語で学ぶデザインパターン入門」をPython化
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

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

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

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

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


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

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

>python abstract_factory_main.py listfactory.ListFactory(tablefactory.TableFactory)

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

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

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

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

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


◆◆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の左上隅にありダウンロードできます。
Item                 Page                     Factory
(makeHTML)        (add, output, makeHTML)    (getFactory, createLink)
  ↑      ↑                                   (createTray, createPage)
Link    Tray
        (add)
  ↑      ↑              ↑                      ↑
ListLink  ListTray     ListPage                ListFactory
(makeHTML)(makeHTML)  (makeHTML)          (createLink, createTray, createPage)

上段と中段のモジュール「factory.py」は抽象工場であり,下段のモジュール「listfactory.py」は具象工場である。
具象工場である下段のモジュール「listfactory.py」はモジュール「tablefactory.py」と入れ替えが可能である。
Factoryが工場,Pageが製品,Item,Link,Trayが部品である。

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

◆Abstract Factory パターンのサンプルの動作

サンプルは HTMLファイルをつくるものです。ファイルの内容は HTMLコードです。HTMLコードはタグで構成されていてタグとタグの間の文字や画像がブラウザで見ることができます。タグは文字や画像の周囲に配置されて文字や画像の見え方を規定しています。

このように HTMLコードは表示される文字や画像とそれを修飾しているタグが一つの塊りになっているので部品化が簡単にできるのです。製品である HTMLファイルの HTMLコードは部品をただ繋げたものです。

◆Abstract Factory パターンの概念

GoF本や結城本を見てもすっきり理解できないのがAbstract Factory パターンであるが,大胆に整理しよう。

Abstract Factoryは文字通り抽象工場であり,抽象部品と抽象製品を持っています。そして工場,部品,製品をまとめて具象化するのです。別の製品をつくりたいときは,具象工場をそっくり取り換えるのです。

想像がつくと思うが,具象化すれば,工場,部品,製品は違うものだと言っても,部品の組み立て方法の構成は共通でなければなりません。ということは,部品を追加したりして組立の構成を替えるとすべての具象工場と抽象工場を更新しなければならなくなるのです。

このように整理するとサンプルのクラス構成が理解しやすくなります。

◆具象工場の入れ替え

サンプルは具象工場をモジュール listfactory.pyとモジュール tablefactory.pyにて実装しています。

メインルーチンのあるモジュール abstract_factory_main.pyをrunさせるとき,コマンドラインのパラメータ(引数)として
  listfactory.ListFactory
または
  tablefactory.TableFactory
を入力します。メインルーチンの冒頭で
  factory = Factory.getFactory(sys.argv[1])
がrunします。sys.argv[1]はコマンドラインの1つ目のパラメータのことです。因みにsys.argv[0]はrunするモジュール abstract_factory_main.pyのことです。Javaとは数え方が違うことに注意してほしい。

クラス Factoryのメソッド getFactoryは,デコレータ @classmethodによりクラスメソッドとなり,インスタンス化しなくても上のように直接呼ぶことができます。このメソッドはクラスをインポートすることによりモジュールつまり具象工場を入れ替える機能を有します。

class Factory(object):
    @classmethod
    def getFactory(cls, classname):
        module, kls = classname.rsplit(".", 1)
        return getattr(__import__(module), kls)()

第1引数 clsは classmethodの約束で自分のクラス名 Factoryであり,第2引数は sys.argv[1]であり,次の行でモジュール名とクラス名に分けられています。次の行でその名称を使い,importしています。これは
  import listfactory.ListFactory
と等価である。上のコードはコマンドラインのパラメータを importするときの決まり文句です。

◆たくさんのオブジェクトを呼ぶクライアント;メインルーチン

クライアント;メインルーチンはたくさんのオブジェクトを呼ぶわけだが,その呼び方を観察するとプログラミング技術が見えてきます。サンプルの出力を巻末に貼り付けてあるので Webページとして見ながら次の説明を解釈してください。

まず最小単位の部品をつくるのは,
  createLink
です。できた部品を,1番目の具象工場はインデントで区分けし,2番目の具象工場は区画で区切るようになっています。

部品を集めてサブアッシとして区分けするのが,
  createTray
です。コードを見ても出力をブラウザを見ても一部がネスト(入れ子)になっていて階層構造になっているのが判ります。

最後にサブアッシを集めて製品として完成させるのが,
  createPage
です。Webページとしての巻頭のタイトルと巻末のアドレスも追加されています。

このメインルーチンは,HTMLコードの知識がまったくなくても書けることが判ります。HTMLコードを生成するのは具象工場であり,そのテンプレートは抽象工場にあます。

◆Abstract Factory パターンのエッセンス

メインルーチンの冒頭でクラス Factoryをインタンス化してそのオブジェクトで3つのメソッドを呼んでいます。これらはすべて抽象工場にあります。

3つのメソッドは具象工場であるクラスListFactory/クラスTableFactoryにある同名のメソッドでオーバーライドされていてそこで部品,サブアッシ,製品をつくるクラスをインタンス化しています。このオブジェクトを使って具体的に部品,サブアッシ,製品がつくられるのです。

実際につくっているのは,メソッド makeHTMLである。HTMLコードの知識がないと具体的に何をやっているか理解できないと思う。HTMLコードの書き方を簡単に言うと,ブラウザから見れる文字や画像をタグで囲むのであるが,そのタグは「<」「>」で囲まれている。タグの内容によってブラウザから見れる文字や画像がいろいろに修飾されるわけです。

Abstract Factory パターンの Pythonコードは少し長いが整理してみると案外判り易いと思う

◆サンプルの出力

サンプルの出力は HTMLファイルです。ファイルの内容はもちろん HTMLコードです。それを巻末のソースコードの後ろに貼り付けてあります。この記事をWebページとして見ていると思うので,そのままブラウザで見ることができます。

サンプルの出力そのものを見たいときはこの Webページの HTMLコードを見ればよいです。
  ブラウザのメニュー → 表示 → ソース
とすれば表示されます。

サンプルの出力は Webページの完成形であるので,それを Webページに貼り付けたのでネスト(入れ子)になります。ブラウザによってはバグるかもしれません。IE11では大丈夫でした。

因みに,巻末のPythonソースコードはブラウザで表示されているがHTMLタグを一切使っていなく,ブラウザからもソースの HTMLコードからもドラッグしてコピペがやり易くなっています。ただし,HTMLコードの「<」「>」は記号に書き換えてあります。

◆◆ソースコード◆◆

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

ソースファイルは4つです。2つのサンプル出力と1つのコマンドライン引数を添付しました。
・Factory.py ; Abstract Factory パターンの抽象工場のモジュール
・listfactory.py ; Abstract Factory パターンの具象工場その1のモジュール
・tablefactory.py ; Abstract Factory パターンの具象工場その2のモジュール
・abstract_factory_main.py ; Abstract Factory パターンのメインルーチンのモジュール

【Factory.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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#factory.py
from abc import ABCMeta, abstractmethod

class Item(metaclass = ABCMeta):

    def __init__(self, caption):
        self.caption = caption
    @abstractmethod
    def makeHTML(self):
        pass

class Link(Item):
    def __init__(self, caption, url):
        super(Link, self).__init__(caption)
        self.url = url

class Tray(Item):
    def __init__(self, caption):
        super(Tray, self).__init__(caption)
        self.tray = []
    def add(self, item):
        self.tray.append(item)

class Page(metaclass = ABCMeta):

    def __init__(self, title, author):
        self.title = title
        self.author = author
        self.content = []
    def add(self, item):
        self.content.append(item)
    def output(self):
        self.filename = "{0}.html".format(self.title)
        writer = open(self.filename, "w")
        #writer.write(self.makeHTML().encode('shift-JIS'))
        writer.write(self.makeHTML())
        writer.close()
        print("{0}を作成しました。".format(self.filename))
    @abstractmethod
    def makeHTML(self):
        pass

class Factory(metaclass = ABCMeta):

    @classmethod
    def getFactory(cls, classname):
        module, kls = classname.rsplit(".", 1)
        return getattr(__import__(module), kls)()
        # import modulename.classnameと等価
    @abstractmethod
    def createLink(self, caption, url):
        pass
    @abstractmethod
    def createTray(self, caption):
        pass
    @abstractmethod
    def createPage(self, title, author):
        pass

【listfactory.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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#listfactory.py

import factory

class ListFactory(factory.Factory):
    def createLink(self, caption, url):
        return ListLink(caption, url)
    def createTray(self, caption):
        return ListTray(caption)
    def createPage(self, title, author):
        return ListPage(title, author)

class ListLink(factory.Link):
    def __init__(self, caption, url):
        super(ListLink, self).__init__(caption, url)
    def makeHTML(self):
        return ' <li><a href="{0}">{1}</a></li>\n'\
            .format(self.url, self.caption)

class ListTray(factory.Tray):
    def __init__(self, caption):
        super(ListTray, self).__init__(caption)
    def makeHTML(self):
        self.buffer = []
        self.buffer.append("<li>\n")
        self.buffer.append("{0}\n".format(self.caption))
        self.buffer.append("<ul>\n")
        for item in self.tray:
            self.buffer.append(item.makeHTML())
        self.buffer.append("</ul>\n")
        self.buffer.append("</li>\n")
        return "\n".join(self.buffer)

class ListPage(factory.Page):
    def __init__(self, title, author):
        super(ListPage, self).__init__(title, author)
    def makeHTML(self):
        self.buffer = []
        self.buffer.append(
            "<html><head><title>{0}</title></head>\n"
            .format(self.title))
        self.buffer.append("<body>\n")
        self.buffer.append("<h1>{0}</h1>\n".format(self.title))
        self.buffer.append("<ul>\n")
        for item in self.content:
            self.buffer.append(item.makeHTML())
        self.buffer.append("</ul>\n")
        self.buffer.append(
            "<hr><address>{0}</address>".format(self.author))
        self.buffer.append("</body></html>\n")
        return "\n".join(self.buffer)

【tablefactory.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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#tablefactory.py

import factory

class TableFactory(factory.Factory):
    def createLink(self, caption, url):
        return TableLink(caption, url)
    def createTray(self, caption):
        return TableTray(caption)
    def createPage(self, title, author):
        return TablePage(title, author)

class TableLink(factory.Link):
    def __init__(self, caption, url):
        super(TableLink, self).__init__(caption, url)
    def makeHTML(self):
        return '<td><a href="{0}">{1}</a></td>\n'\
            .format(self.url, self.caption)

class TableTray(factory.Tray):
    def __init__(self, caption):
        super(TableTray, self).__init__(caption)
    def makeHTML(self):
        self.buffer = []
        self.buffer.append("<td>")
        self.buffer.append(
            '<table width="100%" border="1"><tr>')
        self.buffer.append(
            '<td bgcolor="#cccccc" align="center"\
            colspan="{0}"><b>{1}</b></td>'
            .format(len(self.tray), self.caption))
        self.buffer.append("</tr>\n")
        self.buffer.append("<tr>\n")
        for item in self.tray:
            self.buffer.append(item.makeHTML())
        self.buffer.append("</tr></table>")
        self.buffer.append("</td>")
        return "\n".join(self.buffer)

class TablePage(factory.Page):
    def __init__(self, title, author):
        super(TablePage, self).__init__(title, author)
    def makeHTML(self):
        self.buffer = []
        self.buffer.append(
            "<html><head><title>{0}</title></head>\n"
            .format(self.title))
        self.buffer.append("<body>\n")
        self.buffer.append(
            "<h1>{0}</h1>\n".format(self.title))
        self.buffer.append(
            '<table width="80%" boder="3">\n')
        for item in self.content:
            self.buffer.append(
                "<tr>{0}</tr>".format(item.makeHTML()))
        self.buffer.append("</table>\n")
        self.buffer.append(
            "<hr><address>{0}</address>".format(self.author))
        self.buffer.append("</body></html>\n")
        return "\n".join(self.buffer)

【abstract_factory_main.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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#abstract_factory_main.py

from factory import *
import sys

def main():
    factory = Factory.getFactory(sys.argv[1])

    asahi = factory.createLink(
        "朝日新聞", "http://www.asahi.com/")
    yomiuri = factory.createLink(
        "読売新聞", "http://www.yomiuri.co.jp/")
    us_yahoo = factory.createLink(
        "Yahoo!", "http://www.yahoo.com/")
    jp_yahoo = factory.createLink(
        "Yahoo!Japan", "http://www.yahoo.co.jp/")
    excite = factory.createLink(
        "Exite", "http://www.excite.com/")
    google = factory.createLink(
        "Google", "http://www.google.com/")

    traynews = factory.createTray("新聞")
    traynews.add(asahi)
    traynews.add(yomiuri)

    trayyahoo = factory.createTray("Yahoo!")
    trayyahoo.add(us_yahoo)
    trayyahoo.add(jp_yahoo)

    traysearch = factory.createTray("サーチエンジン")
    traysearch.add(trayyahoo)
    traysearch.add(excite)
    traysearch.add(google)

    page = factory.createPage("LinkPage", "結城 浩")
    page.add(traynews)
    page.add(traysearch)
    page.output()

if __name__== '__main__':
    main()

サンプルの出力をWeBページに貼り付けたもの:ブラウザで見られる(今見てるはず)
コマンドライン・パラメータ:listfactory.ListFactory
LinkPage

LinkPage


結城 浩
inserted by FC2 system
サンプルの出力をWeBページに貼り付けたもの:ブラウザで見られる(今見てるはず)
コマンドライン・パラメータ:tablefactory.TableFactory
LinkPage

LinkPage

新聞
朝日新聞 読売新聞
サーチエンジン
Yahoo!
Yahoo! Yahoo!Japan
Exite Google

結城 浩
inserted by FC2 system
以上

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