週末!プログラミング部

ソフトウェア開発ネタを中心に自分でいろいろ調べた内容を自分の勝手な解釈で思うがままに書いくためのブログ。サンプルソースコード、API、プラットフォーム、プログラミング言語、開発環境などを調査、分析して追求いく予定です。

Pythonでデザインパターン 漆ノ型「Builder」

今回はPythonの勉強しながらデザインパターンのBuilderをやってみました。
他のパターンにつきましては、以下の目次記事をご参照ください。

Builderとは

直訳すると「建築者」という意味になります。
wikiには「 オブジェクトの生成過程を抽象化することによって、動的なオブジェクトの生成を可能にする」とあります。

Builderパターンは「オブジェクトを生成する過程は同じだけど、性質の異なるオブジェクトを作成する」ためのパターンのようです。

・・・(´・ω・`)?

Builderパターンについて考えてみる

いい例かどうかわかりませんが、RESTful Web APIのレスポンスを題材に考えてみたいと思います。

よくあるRESTful Web APIは、なにかリクエストを送るとXMLJSONなどのフォーマットは異なるけど同じ内容のレスポンスを得ることができますね。
例えば以下のような感じです。これは人物のプロフィールを取ってくるようなAPIのつもりです^^;

{
  "name": "hoge",
  "age": 15
  "job":"student"
}
<xml>
    <name>hoge</name>
    <age>15</age>
    <job>student</job>
</xml>

このレスポンスを作るためのクラスを単純に書いてみるとこんな感じでしょうか?

class Response(object) :
    def __init__(self, name="AAA", age=15, job="student" ):
        self.__name = name
        self.__age  = age
        self.__job  = job

    def getXML(self) -> str :
        respnse = "<xml>\n"
        respnse += "\t<name>" + self.__name     + "</name>\n"
        respnse += "\t<age>"  + str(self.__age) + "</age>\n"
        respnse += "\t<job>"  + self.__job      + "</job>\n"
        respnse += "</xml>"
        return respnse

    def getJSON(self) -> str :
        respnse = "{\n"
        respnse += "\t\"name\":\"" + self.__name     + "\"\n"
        respnse += "\t\"age\":\""  + str(self.__age) + "\"\n"
        respnse += "\t\"job\":\""  + self.__job      + "\"\n"
        respnse += "}"
        return respnse

if __name__ == "__main__":
    obj = Response("AAA", 15, "student")
    
    resXML = obj.getXML()
    resJSON = obj.getJSON()
    
    print(resXML)
    print(resJSON)

いまはデータが少ないのでそのままでもいいのですが、将来的に扱うデータが増えたりするとフォーマットも修正しないといけなくなり、 どんどん複雑になっていく可能性があります。 できればXMLJSONでクラスを分けたいところです。そこで以下のようにしてみました。

class Response(metaclass=ABCMeta) :
    def Name(self, name) :
        self._name = name
        
    def Age(self, age) :
        self._age = age
        
    def Job(self, job) :
        self._job = job
        
    @abstractmethod
    def getResponse() -> str :
        pass

class XMLResponse(Response) :
    def __init__(self, name="AAA", age=15, job="student" ):
        self.Name(name)
        self.Age(age)
        self.Job(job)

    def getResponse(self) -> str :
        # 処理内容は割愛
        return respnse

class JSONResponse(Response) :
    def __init__(self, name="AAA", age=15, job="student" ):
        self.Name(name)
        self.Age(age)
        self.Job(job)

    def getResponse(self) -> str :
        # 処理内容は割愛
        return respnse

if __name__ == "__main__":
    objXML = XMLResponse(name = "AAA", age = 15, job = "student")
    objJSON = JSONResponse(name = "AAA", age = 15, job = "student")
    
    resXML = objXML.getResponse()
    resJSON = objJSON.getResponse()
    
    print(resXML)
    print(resJSON)

これで少しだけオブジェクト指向に近づいてきましたね。
でも、ユーザ視点から見るとインスタンスを作成するときに毎回引数を指定しないといけないのは大変。。。 もしかすると間違えてnameにjobを代入しちゃうかもしれないし、XMLResponseとJSONResponseで別々の値を入れてしまうかもしれません。
ここでXMLResponseとJSONResponseのコンストラクタを見るとやっていることは同じなので何とかまとめたいところです。 そこで以下のようなResponseBuilderクラスを作ってクッションにしてました!

class Response(metaclass=ABCMeta) :
    # 処理内容は割愛

class XMLResponse(Response) :
    # 処理内容は割愛

class JSONResponse(Response) :
    # 処理内容は割愛

class ResponseBuilder(object):
    def build(self, response: Response) -> Response:
        response.Name("hoge")
        response.Age(15)
        response.Job("student")
        return response

if __name__ == "__main__":
    objXML  = ResponseBuilder().build(XMLResponse())
    objJSON = ResponseBuilder().build(JSONResponse())
    
    resXML = objXML.getResponse()
    resJSON = objJSON.getResponse()
    
    print(resXML)
    print(resJSON)

これでResponseBuilderクラスのbuildメソッドにオブジェクトを入れれば、XMLResponseでもJSONResponseでも同じように初期化されます。
mainもだいぶシンプルにユーザに優しくなりました!
こんな感じでオブジェクトの初期化にワンクッション挟むのがBuilderパターンなのかな(´・ω・`)?

サンプルコード

以下、上記のコードの全体像です。

# coding: utf-8

from abc import ABC, ABCMeta, abstractmethod

class Response(metaclass=ABCMeta) :
    def Name(self, name) :
        self._name = name
        
    def Age(self, age) :
        self._age = age
        
    def Job(self, job) :
        self._job = job
        
    @abstractmethod
    def getResponse() -> str :
        pass

class XMLResponse(Response) :
    def getResponse(self) -> str :
        respnse = "<xml>\n"
        respnse += "\t<name>" + self._name     + "</name>\n"
        respnse += "\t<age>"  + str(self._age) + "</age>\n"
        respnse += "\t<job>"  + self._job      + "</job>\n"
        respnse += "</xml>"
        return respnse

class JSONResponse(Response) :
    def getResponse(self) -> str :
        respnse = "{\n"
        respnse += "\t\"name\":\"" + self._name     + "\"\n"
        respnse += "\t\"age\":\""  + str(self._age) + "\"\n"
        respnse += "\t\"job\":\""  + self._job      + "\"\n"
        respnse += "}"
        return respnse

class ResponseBuilder(object):
    def build(self, response: Response) -> Response:
        response.Name("hoge")
        response.Age(15)
        response.Job("student")
        return response

if __name__ == "__main__":

    objXML  = ResponseBuilder().build(XMLResponse())
    objJSON = ResponseBuilder().build(JSONResponse())
    
    resXML = objXML.getResponse()
    resJSON = objJSON.getResponse()
    
    print(resXML)
    print(resJSON)

実行結果は以下のとおりです!

$ python Builder.py
<xml>
        <name>hoge</name>
        <age>15</age>
        <job>student</job>
</xml>
{
        "name":"hoge"
        "age":"15"
        "job":"student"
}