Pythonで、デザインパターン「Prototype」を学ぶ


GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。ただ、取り上げられている実例は、JAVAベースのため、自分の理解を深めるためにも、Pythonで同等のプラクティスに挑んでみました。

■ Prototype(プロトタイプ・パターン)

Prototypeパターン(プロトタイプ・パターン)とは、ソフトウェア開発で用いられる、生成に関するデザインパターンの1つである。生成されるオブジェクトの種別がプロトタイプ(典型)的なインスタンスであるときに使用され、このプロトタイプを複製して新しいオブジェクトを生成する。

このパターンは、以下の目的で用いられる。

  • Abstract Factory パターンでなされるように、クライアント・アプリケーションにおいてオブジェクトの生成者をサブクラスにすることを回避する
  • 標準的な方法(例えば'new')で新しいオブジェクトを作ることによる固有のコストが所与のアプリケーションにとって高すぎる時にそれを回避する

このパターンを実装するには、純粋仮想 (pure virtual method) の clone()メソッドを指定する抽象的(abstract)な基底クラスを宣言する。「多態性を持つコンストラクタ」の能力を必要とする全てのクラスは抽象的な基底クラスから自身を派生させ、clone()の操作を実装する。

UML class and sequence diagram

UML class diagram

(以上、ウィキペディア(Wikipedia)より引用)

□ 備忘録

Prototypeパターンは、クラスからインスタンスをつくるのではなく、インスタンスをコピーすることで、インスタンスから別のインスタンスをつくるというものです。
Prototypeパターンを活用するメリットについては、書籍「増補改訂版Java言語で学ぶデザインパターン入門」P.66に以下の記述があります。
(1) 種類が多すぎてクラスにまとめられない場合
(2) クラスからインスタンス作成が難しい場合
(3) フレームワークと生成するインスタンスを分けたい場合

まあ、大量に、インスタンスを生成するようなユースケースがあるならば、インスタンス生成に関わるオーバヘッド処理の削減につながるんですかねぇー

■ "Prototype"のサンプルプログラム

実際に、Prototypeパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。

  • 文字列を、下線を引いて表示する
  • 文字列を、枠線で囲って表示する
$ python Main.py 
"Hello World"
-------------

***************
* Hello World *
***************

///////////////
/ Hello World /
///////////////

■ サンプルプログラムの詳細

Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern/tree/master/Prototype

  • ディレクトリ構成
.
├── Main.py
└── framework
    ├── __init__.py
    ├── decoprototype
    │   ├── __init__.py
    │   ├── message_box_prototype.py
    │   └── underline_pen_prototype.py
    ├── manager.py
    └── prototype.py

(1) Prototype(原型)の役

Prototype役は、インスタンスをコピー(複製)して新しいインスタンスを作るためのインタフェースを定めます。
サンプルプログラムでは、Prototypeクラスが、この役を努めます。

framework/prototype.py
from abc import ABCMeta, abstractmethod

class Prototype(metaclass=ABCMeta):
    @abstractmethod
    def use(self, s):
        pass

    @abstractmethod
    def createClone(self):
        pass

(2) ConcretePrototype(具体的な原型)の役

ConcretePrototype役は、インスタンスをコピーして新しいインスタンスを作るメソッドを実際に実装します。
サンプルプログラムでは、UnderlinePenクラスやMessageBoxクラスが、この役を努めます。

framework/decoprototype/underline_pen_prototype.py
import copy
from framework.prototype import Prototype

class UnderlinePen(Prototype):
    def __init__(self, ulchar):
        self.__ulchar = ulchar

    def use(self, s):
        length = len(s)
        line = self.__ulchar * (length + 2)

        print("\"{0}\"".format(s))
        print("{0}\n".format(line))

    def createClone(self):
        clone = copy.deepcopy(self)
        return clone
framework/decoprototype/message_box_prototype.py
import copy
from framework.prototype import Prototype

class MessageBox(Prototype):
    def __init__(self, decochar):
        self.__decochar = decochar

    def use(self, s):
        length = len(s)
        line = self.__decochar * (length + 4)

        print("{0}".format(line))
        print("{0} {1} {2}".format(self.__decochar, s, self.__decochar))
        print("{0}\n".format(line))

    def createClone(self):
        clone = copy.deepcopy(self)
        return clone

(3) Client(利用者)の役

Client役は、インスタンスをコピーするメソッドを利用して、新しいインスタンスを作成します。
サンプルプログラムでは、ManagerクラスやstartMainメソッドが、この役を努めます。

framework/manager.py
class Manager(object):
    def __init__(self):
        self.__showcase = {}

    def register(self, name, proto):
        self.__showcase[name] = proto

    def create(self, protoname):
        p = self.__showcase[protoname]
        return p.createClone()
Main.py
from framework.manager import Manager
from framework.decoprototype.underline_pen_prototype import UnderlinePen
from framework.decoprototype.message_box_prototype import MessageBox

def startMain(managerObject):
    upen = UnderlinePen("-")
    mbox = MessageBox("*")
    sbox = MessageBox("/")
    managerObject.register("strong message", upen)
    managerObject.register("warning box", mbox)
    managerObject.register("slash box", sbox)

    p1 = managerObject.create("strong message")
    p2 = managerObject.create("warning box")
    p3 = managerObject.create("slash box")
    p1.use("Hello World")
    p2.use("Hello World")
    p3.use("Hello World")

if __name__ == "__main__":
    startMain(Manager())

■ 参考URL