Facadeパターン

概要

facadeパターンは「複雑なシステムに対し、シンプルなインターフェースを提供する」パターンです。

facadeパターンは多くの人が意識して使っていることで、pythonを叩く親シェルを作るのもある種facadeパターンです。


例題

ファイル解凍プログラムを作ります。

  • 圧縮されたファイル(gzip, tarなど)を解凍するには、Pythonでは異なるメソッドを使用する必要がある。

  • ここでは、圧縮されたファイルに対し、共通のインターフェース(unpack()メソッド等)を作成する。

今回は、zip,tar,gz形式のファイルを作成し、それぞれ解凍したいと思います。
ファイルはtemp.zipからダウンロードしzipを解凍、または以下のコマンドで作成してください。

#!/bin/bash
mkdir -p temp

# make test.zip
mkdir -p _temp/test1
echo "bag1" > "temp/test1/bag1.txt"
echo "bag2" > "temp/test1/bag2.txt"
echo "bag3" > "temp/test1/bag3.txt"
cd "temp/"
zip -r "test.zip" "test1/"
cd ..

# make test.tar.gz
mkdir -p _temp/test2
echo "genome1" > "temp/test2/genome1.txt"
echo "genome2" > "temp/test2/genome2.txt"
echo "genome3" > "temp/test2/genome3.txt"

cd "temp/"
tar -zcvf "test.tar.gz" "test2/"
cd ..

# make test.gz
echo "test" > _temp/test
gzip "temp/test"

rm -r temp/test1
rm -r temp/test2

なお、例題およびサンプルコードは実践Python3を一部改変しています。

サンプルコード

import os
import gzip
import tarfile
import zipfile


def main():
    fname_zip = "_temp/test.zip"
    fname_tar = "_temp/test.tar.gz"
    fname_gz = "_temp/test.gz"

    for fname in [fname_zip, fname_tar, fname_gz]:
        with Archive(fname) as archive:
            print(archive.names())
            archive.unpack()


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()

    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: {}".format(self.filename))

    def _prepare_tarball_or_zip(self):
        def ext_all():
            self._file.extractall()

        if self.filename.endswith(".zip"):
            self._file = zipfile.ZipFile(self.filename)
            self._names = self._file.namelist
            self._unpack = ext_all
        else:
            suffix = os.path.splitext(self.filename)[1]
            self._file = tarfile.open(self.filename, "r:" + suffix[1:])
            self._names = self._file.getnames
            self._unpack = ext_all

    def _prepare_gzip(self):
        self._file = gzip.open(self.filename)
        filename = os.path.splitext(self.filename)[0]
        self._names = lambda: [filename]

        def extractall():
            with open(filename, "wb") as f:
                f.write(self._file.read())
        self._unpack = extractall

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

    # 以下2つは、コンテキストマネジャーに必要なメソッド。
    # 詳細は補足解説で説明しています
    def __enter__(self):
        return self

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


if __name__ == "__main__":
    main()

# 出力

(特になし)

実行すると、実行したディレクトリにtest1とtest2のフォルダが、temp下にtestファイルができます。


特徴

サンプルコードはArchiveクラスだけな分、処理が若干複雑になっています。
以下にフローを書いてみました。

処理の流れ(Facadeパターン)

委譲を用いることにより、ある程度柔軟にソースコードが変更できるようにしています。

Pythonらしく書くためのコツ

特になし

クラス図

# 一般的なクラス図

一般的なクラス図(Facadeパターン)

あまりこのクラス図に意味はありませんが、

  • ユーザーはFacadeクラスを使う
  • Module達がFacadeクラスを使用することはない

ということを把握しておけば問題ないです。

# サンプルコードにおけるクラス図

省略


補足(コンテキストマネジャーについて)

main() 文の中でコンテキストマネジャーというものが使用されています。

def main():
    # ...
    with Archive(fname) as archive:
        print(archive.names())
        archive.unpack()

with文で呼び出されているのがコンテキストマネジャーです。

コンテキストマネジャーが呼び出されると、前処理(__enter__)と後処理(__exit__)を必ず行われます。

コンテキストマネジャーの動きを理解するために、以下のようなコードを書いてみました。

class Hoge:

    def __init__(self):
        pass

    def huga(self):
        print("huga")

    def __enter__(self):
        print("enter")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("exit")


if __name__ == "__main__":
    with Hoge() as h:
        h.huga()
        print("hoge")

実行結果は以下

enter
huga
hoge
exit

with文の前にenter, 最後に exit関数が動いているのがわかります。

今回は、enterでは特に何もしませんが、exitで必ずファイルをcloseしています。

参考URL

Wikipedia