Update 2023.11.16 2017.05.05

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

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

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

GoFの言っていることは判りにくいが,「抽出されたクラス」とは機能のことで,「実装」とはサポート環境やバージョンなどのことです。(GoF本の和訳はいまいちです)

サポート環境やバージョンなどとは,
  Windows向け,Linux向け,MacOS向け
  プリンタ用,ディスプレイ用,通信用
  巨人向け,阪神向け,楽天向け
などいろいろ考えられます。

これら向けに機能もいろいろ追加したい場面では,機能ごとにすべての環境向けやバージョン向けに実装するのはたいへんな手間になってしまいます。

そこでGoFがいうように,機能側と環境側を独立に変更,拡張,再利用することができるように機能側と環境側の橋渡し(Bridgeという)を工夫するのがBridge パターンです。

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

【重要な注意】本プログラムはファイルを出力します。その使い方は不明です。

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

wrote C:\Users\yamak\AppData\Local\Temp\Forecast_6_8.xpm
wrote C:\Users\yamak\AppData\Local\Temp\Forecast_6_8.gif
wrote C:\Users\yamak\AppData\Local\Temp\Forecast_6_8.xpm

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

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

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

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


◆◆Bridge パターンとは◆◆

GoFによれば,Bridge パターンの目的は, 「抽出されたクラスと実装を分離して,それらを独立に変更できるようにする。」

GoFによれば,Bridge パターンの別名は,Handle/Body パターンだそうだ。

GoFによれば,次のような場合に,Bridge パターンを利用する。
・抽出されたクラスとその実装を永続的に結合することを避けたい場合。たとえば,実装を実行時に選択したり交換したりしなければならないときに,このような場合が起こり得る。
・抽出されたクラスとその実装の両方を,サブクラスの追加により拡張可能にすべき場合。この場合,Bridge パターンを用いることで,抽出されたクラスに異なる実装を結合したり,それぞれを独立に拡張することが可能になる。
・抽出されたクラスの実装における変更が,クライアントに影響を与えるべきではない場合。すなわち,クライアントのコードを再コンパイルしなくても済むようにすべき場合。
(以下省略)

GoFによれば,Bridge パターンの関連するパターンは次のようなものである。
Abstract Factory パターン: このパターンにより,Bridge パターンに基づくインスタンスの生成と構築を行うことができる。
Adapter パターン: このパターンは,関係のないクラス同士をつなぐことが目的である。このパターンは通常は設計が終わった後で適用される。それに対して,Bridge パターンは,抽出されたクラスと実装を独立に変更可能にするために設計の前段階で使われる。

◆◆barchart1.py のクラス構成◆◆

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。

    BarCharter                            BarRenderer
    (render)           (initialize, draw_caption, draw_bar, finalize)
       ↑                           ↑                   ↑
                               TextBarRenderer    ImageBarRenderer
  (追加機能はここに)               (do.)                (do.)
class BarCharter は,機能側の上位クラスです。 Bridge の役割をします。
        機能を追加するときは,class BarCharter のサブクラスを追加します。
class BarRenderer は,環境実装側の上位クラスであり,テンプレートであり抽象クラスです。
class TextBarRenderer, class ImageBarRenderer は,環境実装側の具象クラスです。
環境実装側のサブクラスを追加することもできます。

◆◆barchart1.py の実装◆◆

サンプルは,曜日ごとの天気予報のグラフです。降水確率なのか的中率なのか他のものかは不明です。サンプルは,このグラフの出力先をテキストにしたり,種々の画像にしたりします。

グラフの書き方が,機能側であり,グラフの出力先が,環境実装側であり,Bridge パターンを使えば,機能側と環境実装側が独立に追加・変更ができるようになります。

class TextBarRenderer の出力を先に見てみましょう。

【標準出力】

              Forecast 6/8
              ============
************************** Mon
**************************** Tue
******************************* Wed
************************************ Thu
**************************************** Fri
*********************************** Sat
******************************* Sun

class ImageBarRenderer の出力は,画像ファイル形式 XPM ですが,どのようなアプリで画像を見ることができるのか筆者は知りません(出力ファイルは添付しておきます)。

これらのクラスは,次の4つの共通のメソッドを持っています。
  initialize
  draw_caption
  draw_bar
  finalize

これらのクラスのスーパークラスはテンプレートの役割をする抽象クラスですが,抽象メソッドを持っていません。その代わり,32行目のようなデコレータが付いています。

Qtrac モジュールの has_methods は,特殊メソッド__subclasshook__を使って,対象クラスが引数のメソッドを持っているか確認するためのデコレータを生成するグローバル関数なのです。デコレータの作り方については,詳細は説明しませんが,いくつか比較すれば作り方は判ると思います。

環境実装側の4つのメソッドの働きは,出力サンプルを見れば想像することができますので,詳細は省略します。

これらの4つのメソッドを呼んでいるのは,機能側の
  class BarCharter
    def render
です。インスタンス化するときの引数に,環境実装側のインスタンスを渡されるので,機能側は環境実装側の使われるクラスを気にすることなく呼んで使うことができます。

23
24
25
26
27
28
29
def main():
    pairs = (("Mon", 16), ("Tue", 17), ("Wed", 19), ("Thu", 22),
            ("Fri", 24), ("Sat", 21), ("Sun", 19))
    textBarCharter = BarCharter(TextBarRenderer())
    textBarCharter.render("Forecast 6/8", pairs)
    imageBarCharter = BarCharter(ImageBarRenderer())
    imageBarCharter.render("Forecast 6/8", pairs)

それを可能にするのが,メインルーチンの26行目からです。機能側の class BarCharter をインスタンス化するときに,環境実装側の class TextBarRenderer, class ImageBarRenderer のいずれかを引数にしてインスタンス化してしまいます。
そのあと,機能側のメソッドを,環境実装側を気にしないで実行していますが,環境実装側が自動的に選択されています。言うまでもなく,class BarCharter が Bridge です。

このようなインスタンスの授受によって,if 文をまったく使わないで,場合分けができるようになのが,オブジェクト指向なのです。

◆◆barchart2.py のクラス構成◆◆

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。

barchart1.py と同じです

◆◆barchart2.py の実装◆◆

環境実装側の class BarRenderer 以外はほぼ同じです。

このクラスが出力するのは,GIF(ジフ)形式の画像であり(右図),GUI をつくるための tkinter でつくっています。tkinter の詳細は省略します。


◆◆barchart3.py のクラス構成◆◆

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。

barchart1.py と同じです

◆◆barchart3.py の実装◆◆

環境実装側のテンプレート class BarRenderer 以外はほぼ同じです。

barchart1.py, barchart2.py では,デコレータで,抽象メソッドの代わりに,具象メソッドの実装を確認していました。

barchart3.py では次のように,抽象スーパークラスを使います。このクラスも特殊メソッド__subclasshook__により実装メソッドが揃っているかを確認します。

【barchart3.py】
32
33
34
class BarRenderer(Qtrac.Requirer):
    required_methods = {"initialize", "draw_caption", "draw_bar",
            "finalize"}

◆◆ソースコード◆◆

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

ソースファイルは4つです。3つのサンプル出力も添付します。
・barchart1.py;Bridge パターンのPythonサンプル(その1)
・barchart2.py;Bridge パターンのPythonサンプル(その2)
・barchart3.py;Bridge パターンのPythonサンプル(その3)
・Qtrac.py;サンプルが import する

【barchart1.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
#!/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 abc
import os
import re
import tempfile
import Qtrac
try:
    import cyImage as Image
except ImportError:
    import Image


def main():
    pairs = (("Mon", 16), ("Tue", 17), ("Wed", 19), ("Thu", 22),
            ("Fri", 24), ("Sat", 21), ("Sun", 19))
    textBarCharter = BarCharter(TextBarRenderer())
    textBarCharter.render("Forecast 6/8", pairs)
    imageBarCharter = BarCharter(ImageBarRenderer())
    imageBarCharter.render("Forecast 6/8", pairs)


@Qtrac.has_methods("initialize", "draw_caption", "draw_bar", "finalize")
class BarRenderer(metaclass=abc.ABCMeta): pass


class BarCharter:

    def __init__(self, renderer):
        if not isinstance(renderer, BarRenderer):
            raise TypeError("Expected object of type BarRenderer, got {}".
                    format(type(renderer).__name__))
        self.__renderer = renderer


    def render(self, caption, pairs):
        maximum = max(value for _, value in pairs)
        self.__renderer.initialize(len(pairs), maximum)
        self.__renderer.draw_caption(caption)
        for name, value in pairs:
            self.__renderer.draw_bar(name, value)
        self.__renderer.finalize()


class TextBarRenderer:

    def __init__(self, scaleFactor=40):
        self.scaleFactor = scaleFactor


    def initialize(self, bars, maximum):
        assert bars > 0 and maximum > 0
        self.scale = self.scaleFactor / maximum


    def draw_caption(self, caption):
        print("{0:^{2}}\n{1:^{2}}".format(caption, "=" * len(caption),
                self.scaleFactor))


    def draw_bar(self, name, value):
        print("{} {}".format("*" * int(value * self.scale), name))


    def finalize(self):
        pass


class ImageBarRenderer:

    COLORS = [Image.color_for_name(name) for name in ("red", "green",
              "blue", "yellow", "magenta", "cyan")]


    def __init__(self, stepHeight=10, barWidth=30, barGap=2):
        self.stepHeight = stepHeight
        self.barWidth = barWidth
        self.barGap = barGap


    def initialize(self, bars, maximum):
        assert bars > 0 and maximum > 0
        self.index = 0
        color = Image.color_for_name("white")
        self.image = Image.Image(bars * (self.barWidth + self.barGap),
                maximum * self.stepHeight, background=color)


    def draw_caption(self, caption):
        self.filename = os.path.join(tempfile.gettempdir(),
                re.sub(r"\W+", "_", caption) + ".xpm")


    def draw_bar(self, name, value):
        color = ImageBarRenderer.COLORS[self.index %
                len(ImageBarRenderer.COLORS)]
        width, height = self.image.size
        x0 = self.index * (self.barWidth + self.barGap)
        x1 = x0 + self.barWidth
        y0 = height - (value * self.stepHeight)
        y1 = height - 1
        self.image.rectangle(x0, y0, x1, y1, fill=color)
        self.index += 1


    def finalize(self):
        self.image.save(self.filename)
        print("wrote", self.filename)


if __name__ == "__main__":
    main()

【barchart2.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
#!/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 abc
import os
import re
import tempfile
import tkinter as tk
import Qtrac


def main():
    pairs = (("Mon", 16), ("Tue", 17), ("Wed", 19), ("Thu", 22),
            ("Fri", 24), ("Sat", 21), ("Sun", 19))
    textBarCharter = BarCharter(TextBarRenderer())
    textBarCharter.render("Forecast 6/8", pairs)
    imageBarCharter = BarCharter(ImageBarRenderer())
    imageBarCharter.render("Forecast 6/8", pairs)


@Qtrac.has_methods("initialize", "draw_caption", "draw_bar", "finalize")
class BarRenderer(metaclass=abc.ABCMeta): pass


class BarCharter:

    def __init__(self, renderer):
        self.__renderer = renderer


    def render(self, caption, pairs):
        maximum = max(value for _, value in pairs)
        self.__renderer.initialize(len(pairs), maximum)
        self.__renderer.draw_caption(caption)
        for name, value in pairs:
            self.__renderer.draw_bar(name, value)
        self.__renderer.finalize()


class TextBarRenderer:

    def __init__(self, scaleFactor=40):
        self.scaleFactor = scaleFactor


    def initialize(self, bars, maximum):
        assert bars > 0 and maximum > 0
        self.scale = self.scaleFactor / maximum


    def draw_caption(self, caption):
        print("{0:^{2}}\n{1:^{2}}".format(caption, "=" * len(caption),
                self.scaleFactor))


    def draw_bar(self, name, value):
        print("{} {}".format("*" * int(value * self.scale), name))


    def finalize(self):
        pass


class ImageBarRenderer:

    COLORS = ("red", "green", "blue", "yellow", "magenta", "cyan")


    def __init__(self, stepHeight=10, barWidth=30, barGap=2):
        self.stepHeight = stepHeight
        self.barWidth = barWidth
        self.barGap = barGap


    def initialize(self, bars, maximum):
        assert bars > 0 and maximum > 0
        if tk._default_root is None:
            self.gui = tk.Tk()
            self.inGui = False
        else:
            self.gui = tk._default_root
            self.inGui = True
        self.index = 0
        self.width = bars * (self.barWidth + self.barGap)
        self.height = maximum * self.stepHeight
        self.image = tk.PhotoImage(width=self.width, height=self.height)
        self.image.put("white", (0, 0, self.width, self.height))


    def draw_caption(self, caption):
        self.filename = os.path.join(tempfile.gettempdir(),
                re.sub(r"\W+", "_", caption) + ".gif")


    def draw_bar(self, name, value):
        color = ImageBarRenderer.COLORS[self.index %
                len(ImageBarRenderer.COLORS)]
        x0 = self.index * (self.barWidth + self.barGap)
        x1 = x0 + self.barWidth
        y0 = self.height - (value * self.stepHeight)
        y1 = self.height - 1
        self.image.put(color, (x0, y0, x1, y1))
        self.index += 1


    def finalize(self):
        self.image.write(self.filename, "gif")
        print("wrote", self.filename)
        if not self.inGui:
            self.gui.quit()


if __name__ == "__main__":
    main()

【barchart3.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
#!/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 abc
import os
import re
import tempfile
import Qtrac
try:
    import cyImage as Image
except ImportError:
    import Image


def main():
    pairs = (("Mon", 16), ("Tue", 17), ("Wed", 19), ("Thu", 22),
            ("Fri", 24), ("Sat", 21), ("Sun", 19))
    textBarCharter = BarCharter(TextBarRenderer())
    textBarCharter.render("Forecast 6/8", pairs)
    imageBarCharter = BarCharter(ImageBarRenderer())
    imageBarCharter.render("Forecast 6/8", pairs)


class BarRenderer(Qtrac.Requirer):
    required_methods = {"initialize", "draw_caption", "draw_bar",
            "finalize"}


class BarCharter:

    def __init__(self, renderer):
        if not isinstance(renderer, BarRenderer):
            raise TypeError("Expected object of type BarRenderer, got {}".
                    format(type(renderer).__name__))
        self.__renderer = renderer


    def render(self, caption, pairs):
        maximum = max(value for _, value in pairs)
        self.__renderer.initialize(len(pairs), maximum)
        self.__renderer.draw_caption(caption)
        for name, value in pairs:
            self.__renderer.draw_bar(name, value)
        self.__renderer.finalize()


class TextBarRenderer:

    def __init__(self, scaleFactor=40):
        self.scaleFactor = scaleFactor


    def initialize(self, bars, maximum):
        assert bars > 0 and maximum > 0
        self.scale = self.scaleFactor / maximum


    def draw_caption(self, caption):
        print("{0:^{2}}\n{1:^{2}}".format(caption, "=" * len(caption),
                self.scaleFactor))


    def draw_bar(self, name, value):
        print("{} {}".format("*" * int(value * self.scale), name))


    def finalize(self):
        pass


class ImageBarRenderer:

    COLORS = [Image.color_for_name(name) for name in ("red", "green",
              "blue", "yellow", "magenta", "cyan")]


    def __init__(self, stepHeight=10, barWidth=30, barGap=2):
        self.stepHeight = stepHeight
        self.barWidth = barWidth
        self.barGap = barGap


    def initialize(self, bars, maximum):
        assert bars > 0 and maximum > 0
        self.index = 0
        color = Image.color_for_name("white")
        self.image = Image.Image(bars * (self.barWidth + self.barGap),
                maximum * self.stepHeight, background=color)


    def draw_caption(self, caption):
        self.filename = os.path.join(tempfile.gettempdir(),
                re.sub(r"\W+", "_", caption) + ".xpm")


    def draw_bar(self, name, value):
        color = ImageBarRenderer.COLORS[self.index %
                len(ImageBarRenderer.COLORS)]
        width, height = self.image.size
        x0 = self.index * (self.barWidth + self.barGap)
        x1 = x0 + self.barWidth
        y0 = height - (value * self.stepHeight)
        y1 = height - 1
        self.image.rectangle(x0, y0, x1, y1, fill=color)
        self.index += 1


    def finalize(self):
        self.image.save(self.filename)
        print("wrote", self.filename)


if __name__ == "__main__":
    main()

【Qtrac.py】(抜粋)
46
47
48
49
50
51
52
53
54
55
56
57

77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def has_methods(*methods):
    def decorator(Base):
        def __subclasshook__(Class, Subclass):
            if Class is Base:
                attributes = collections.ChainMap(*(Superclass.__dict__
                        for Superclass in Subclass.__mro__))
                if all(method in attributes for method in methods):
                    return True
            return NotImplemented
        Base.__subclasshook__ = classmethod(__subclasshook__)
        return Base
    return decorator

class Requirer(metaclass=abc.ABCMeta):

    # Since we have rules for adding new expected attributes, we *do*
    # perform the check for subclasses
    @classmethod
    def __subclasshook__(Class, Subclass):
        methods = set()
        for Superclass in Subclass.__mro__:
            if hasattr(Superclass, "required_methods"):
                methods |= set(Superclass.required_methods)
        attributes = collections.ChainMap(*(Superclass.__dict__
                for Superclass in Class.__mro__))
        if all(method in attributes for method in methods):
            return True
        return NotImplemented

以上

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