Update 2023.11.17 2017.05.05

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

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

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

Facade ファサードは仏語で建物の正面という意味です。Facade パターンは,複雑な手順の作業を統一した手段でできるようにいわば「入口(窓口)を1つにする」ということを可能にします。

また,Facade みかけ・外見を統一すると解釈することもできます。


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


◆◆Facade パターンとは◆◆

GoFによれば,Facade パターンの目的は, 「サブシステム内に存在する複数のインタフェースに1つの統一インタフェースを与える。Facade パターンはサブシステムの利用を容易にするための高レベルインタフェースを定義する。」

GoFによれば,次のような場合に Facade パターンを利用する。
・複雑なサブシステムに単純なインタフェースを提供したい場合。サブシステムは発展するにつれて,より複雑になっていく。たいていのパターンは,適用するとたくさんの小さなクラスが導入されることになる。それにより,サブシステムの再利用性が増し,カスタマイズも容易になる。しかしその一方で,サブシステムをカスタマイズする必要のないクライアントにとっては,そのサブシステムの利用が難しくなる。このような場合に,facade はサブシステムの単純なデフォルトのビューを提供してくれる。ほとんどのクライアントにとってはこのデフォルトのビューだけで十分である。サブシステムをカスタマイズする必要のあるクライアントだけが,facade を越えてサブシステムの内部まで見ることになる。
・ある抽象を実装しているクラスとクライアントの間に多くの依存関係がある場合。あるサブシステムをクライアントや他のサブシステムから切り離して,独立性や移植性を高めるために facade を導入する。
・サブシステムを階層化したい場合。各階層の各サブシステムへの入り口を定義するために facade を使う。複数のサブシステムが依存し合っている場合,それらのサブシステムが互いに facade を通してのみやりとりを行うようにすれば,それらの依存関係を単純にすることができる。

GoFによれば,Facade パターンの関連するパターンは次のようなものである。
Abstract Factory パターン:サブシステムとは独立した方法でサブシステム内のオブジェクトを生成するインタフェースを提供するために,Facade パターンと一緒に利用することができる。また,プラットフォームに特化したクラスを隠ぺいするという点で,Facade パターンに対する代替案として利用することもできる。
Mediator パターン:既存のクラスの機能を抽出しているという点で Facade パターンと似ている。しかし,Mediator パターンの目的は Colleague オブジェクト間のやりとりを抽出することであり,それらの機能を1か所に集中させて,Colleague オブジェクトには機能を持たせないようにする。Colleague オブジェクトは mediator の存在を知っており,Colleague オブジェクト同士で直接やりとりをする代わりに mediator とやりとりをする。一方 facade は,サブシステム内のオブジェクトの利用を容易にするために,そのインタフェースを抽出するだけである。facade は新たな機能を定義しないし,サブシステム内のクラスは facade の存在を知らない。
Singleton パターン:通常,facade には唯一性が要求される。したがって,facade にはしばしば Singleton パターンが適用される。

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

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
    Archive
    (内容省略)
class Archive は,圧縮・解凍に関わる標準ライブラリにアクセスするインターフェイス

◆◆Facade パターンの実装◆◆

サンプルは,3種類の圧縮と解凍をするだけの簡単な例です。

圧縮と解凍には標準ライブラリを使うのですが,3種類の圧縮と解凍にはそれぞれの使い方があるので,そのインターフェイスを統一するのが Facade パターンです。

インターフェイスの統一には,圧縮と解凍に関わる標準ライブラリの使い方を知らなければならないのですが,それは,class Archive に書かれています。

それの詳細は,天下り的に考えてもらって十分です。

まず,統一インターフェイスは,23行目のコンストラクタに凝縮されています。これらの使い方はメインルーチンに書かれています。

コンストラクタで受け取るのは,filename だけですが,これだけに getter, setter があります。それが30行目と35行目の同じ名前のメソッド
  def filename
です。同じ名前ですが,違うデコレータ
  @property
  @filename.setter
で区別しています。前者が getter であり,後者が setter です。

デコレータを使わなければ,
  def get_filename
  def set_filename
と書くのが普通です。

3種類の圧縮と解凍の具体的な作業は,
  def _prepare...
に書かれています。詳細な説明は省略しますので,天下り的に考えてください。

サンプルは,まず最初に,ソースファイル
  Unpack.py
と同じディレクトリにある7つのファイルを圧縮し,Tempファイルに保存します(127行目~)。保存される場所は,178行目をコメントアウトを外せば判ります(「#」を削除)。

圧縮対象ファイルと圧縮ファイルのファイル名称は,134行目から139行目までに書かれています。拡張子が「.zip」「.tar.gz」「.pyw.gz」が圧縮ファイルです。

実際に圧縮するのは,141行目から163行目までで,try:finally:文で例外を投げないようにしています。

次に,解凍については,166行目の with ブロックに書かれています。この with ブロックをコンテキストマネージャーと言います。圧縮と違って,try:finally:文を使っていません。

コンテキストマネージャーのメリットは,ただ簡単に書けるというしかないです。

その代わり,106行目と110行目のように,特殊メソッド
  __enter__
  __exit__
を定義する必要があります。これらは,
  with ブロック
の最初と最後に必ず実行されます。

159行目の with とは違うので注意してください。

179行目は,標準ライブラリを使って,ディレクトリとファイルを削除しようとしています。が,エラーが出てファイルしか削除できませんでした。標準ライブラリなのにバグるとは,原因が判りません。Tempフォルダが特殊なディレクトリなのかも?

◆◆ソースコード◆◆

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

1つのソースファイル,7つの圧縮対象ファイルがあります(名称を指定すれば何でもよい)。
・Unpack.py;Facade パターンのPythonサンプル

【Unpack.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
#!/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 contextlib
import gzip
import os
import re
import string
import tarfile
import zipfile


class Archive:

    def __init__(self, filename):
        self._names = None
        self._unpack = None
        self._file = None
        self.filename = filename


    @property
    def filename(self):
        return self.__filename


    @filename.setter
    def filename(self, name):
        self.close()
        self.__filename = name


    def close(self):
        if self._file is not None:
            self._file.close()
        self._names = self._unpack = self._file = None


    def names(self):
        if self._file is None:
            self._prepare()
        return self._names()


    def unpack(self):
        if self._file is None:
            self._prepare()
        self._unpack()


    def _prepare(self):
        if self.filename.endswith((".tar.gz", ".tar.bz2", ".tar.xz",
                ".zip")):
            self._prepare_tarball_or_zip()
        elif self.filename.endswith(".gz"):
            self._prepare_gzip()
        else:
            raise ValueError("unreadable: {}".format(self.filename))


    def _prepare_tarball_or_zip(self):
        def safe_extractall():
            unsafe = []
            for name in self.names():
                if not self.is_safe(name):
                    unsafe.append(name)
            if unsafe:
                raise ValueError("unsafe to unpack: {}".format(unsafe))
            self._file.extractall()
        if self.filename.endswith(".zip"):
            self._file = zipfile.ZipFile(self.filename)
            self._names = self._file.namelist
            self._unpack = safe_extractall
        else: # Ends with .tar.gz, .tar.bz2, or .tar.xz
            suffix = os.path.splitext(self.filename)[1]
            self._file = tarfile.open(self.filename, "r:" + suffix[1:])
            self._names = self._file.getnames
            self._unpack = safe_extractall


    def _prepare_gzip(self):
        self._file = gzip.open(self.filename)
        filename = self.filename[:-3]
        self._names = lambda: [filename]
        def extractall():
            with open(filename, "wb") as file:
                file.write(self._file.read())
        self._unpack = extractall


    def is_safe(self, filename):
        return not (filename.startswith(("/", "\\")) or
            (len(filename) > 1 and filename[1] == ":" and
             filename[0] in string.ascii_letter) or
            re.search(r"[.][.][/\\]", filename))


    def __enter__(self):
        return self


    def __exit__(self, exc_type, exc_value, traceback):
        self.close()


    def __str__(self):
        return "{}({})".format(self.filename, self._file is not None)


if __name__ == "__main__":
    # This code is designed to work with 3.1+, so can't use
    # os.makedirs()'s exist_ok keyword argument, can't use
    # zipfile.ZipFile as a context manager, and can't use text mode for
    # gzip.
    import errno
    import os
    import shutil
    import tempfile
    fromPath = os.path.dirname(os.path.abspath(__file__))
    toPath = os.path.join(tempfile.gettempdir(), "unpack")
    try:
        os.makedirs(toPath)
    except OSError as err:
        if err.errno != errno.EEXIST:
            raise
    zipFilename = os.path.join(toPath, "test.zip")
    zipNames = ["Bag1.py", "Bag2.py", "Bag3.py"]
    tarFilename = os.path.join(toPath, "test.tar.gz")
    tarNames = ["genome1.py", "genome2.py", "genome3.py"]
    gzFilename = os.path.join(toPath, "hello.pyw.gz")
    gzName = "hello.pyw"
    file = None
    try:
        file = zipfile.ZipFile(zipFilename, "w", zipfile.ZIP_DEFLATED)
        for name in zipNames:
            file.write(name)
    finally:
        if file is not None:
            file.close()
    file = None
    try:
        file = tarfile.open(tarFilename, "w:gz")
        for name in tarNames:
            file.add(name)
    finally:
        if file is not None:
            file.close()
    file = None
    try:
        file = gzip.open(gzFilename, "w")
        with open(gzName, "rb") as infile:
            file.write(infile.read())
    finally:
        if file is not None:
            file.close()

    os.chdir(toPath)
    with Archive(zipFilename) as archive:
        print(archive.names())
        assert archive.names() == zipNames
        archive.unpack()

        archive.filename = tarFilename
        print(archive.names())
        assert archive.names() == tarNames
        archive.unpack()

        archive.filename = gzFilename
        archive.unpack()
    # print(toPath)
    # shutil.rmtree(toPath)

以上

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