FactoryメソッドパターンとPythonでの実装

FactoryメソッドパターンとPythonでの実装

この記事では、Factory Methodの設計パターンとPythonでの実装について説明します。 いわゆるGangof Four(GoF:Gamma、Helm、Johson、Vlissides)が本Design Patterns: Elements of Reusable Object-Oriented Softwareを出版した後、デザインパターンは90年代後半に人気のトピックになりました。

この本では、ソフトウェアで繰り返し発生する問題のコアデザインソリューションとしてデザインパターンについて説明し、問題の性質に応じて各デザインパターンをcategoriesに分類しています。 各パターンには、名前、問題の説明、設計ソリューション、およびそれを使用した結果の説明が与えられます。

GoFブックは、ファクトリメソッドを創造的なデザインパターンとして説明しています。 創造的なデザインパターンはオブジェクトの作成に関連し、ファクトリメソッドは共通のインターフェイスを持つオブジェクトを作成するデザインパターンです。

これはmakes Factory Method one of the most widely used design patternsで繰り返し発生する問題であり、それを理解し、その適用方法を知ることが非常に重要です。

By the end of this article, you will

  • Factory Methodのコンポーネントを理解する

  • アプリケーションでFactory Methodを使用する機会を認識する

  • パターンを使用して既存のコードを変更し、その設計を改善することを学ぶ

  • Factory Methodが適切な設計パターンである機会を特定することを学ぶ

  • ファクトリメソッドの適切な実装を選択する

  • Factory Methodの再利用可能な汎用ソリューションを実装する方法を知っている

Free Bonus:5 Thoughts On Python Masteryは、Python開発者向けの無料コースで、Pythonスキルを次のレベルに引き上げるために必要なロードマップと考え方を示しています。

ファクトリーメソッドの紹介

ファクトリメソッドは、共通のインターフェイスの具体的な実装を作成するために使用される創造的なデザインパターンです。

オブジェクトの作成プロセスを、オブジェクトのインターフェースに依存するコードから分離します。

たとえば、アプリケーションでは、タスクを実行するために特定のインターフェイスを持つオブジェクトが必要です。 インターフェイスの具体的な実装は、いくつかのパラメーターによって識別されます。

複雑なif/elif/else条件構造を使用して具体的な実装を決定する代わりに、アプリケーションはその決定を具体的なオブジェクトを作成する別のコンポーネントに委任します。 このアプローチにより、アプリケーションコードが簡素化され、再利用性と保守が容易になります。

指定された形式を使用して、Songオブジェクトをそのstring表現に変換する必要があるアプリケーションを想像してみてください。 オブジェクトを別の表現に変換することは、多くの場合、シリアル化と呼ばれます。 多くの場合、これらの要件は、次のコードのように、すべてのロジックと実装を含む単一の関数またはメソッドで実装されます。

# In serializer_demo.py

import json
import xml.etree.ElementTree as et

class Song:
    def __init__(self, song_id, title, artist):
        self.song_id = song_id
        self.title = title
        self.artist = artist


class SongSerializer:
    def serialize(self, song, format):
        if format == 'JSON':
            song_info = {
                'id': song.song_id,
                'title': song.title,
                'artist': song.artist
            }
            return json.dumps(song_info)
        elif format == 'XML':
            song_info = et.Element('song', attrib={'id': song.song_id})
            title = et.SubElement(song_info, 'title')
            title.text = song.title
            artist = et.SubElement(song_info, 'artist')
            artist.text = song.artist
            return et.tostring(song_info, encoding='unicode')
        else:
            raise ValueError(format)

上記の例では、曲を表す基本的なSongクラスと、songオブジェクトをそのstring表現に変換できるSongSerializerクラスがあります。 formatパラメータ。

.serialize()メソッドは、JSONXMLの2つの異なる形式をサポートします。 指定された他のformatはサポートされていないため、ValueError例外が発生します。

Pythonインタラクティブシェルを使用して、コードの動作を確認しましょう。

>>>

>>> import serializer_demo as sd
>>> song = sd.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = sd.SongSerializer()

>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'

>>> serializer.serialize(song, 'XML')
'Water of LoveDire Straits'

>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "", line 1, in 
  File "./serializer_demo.py", line 30, in serialize
    raise ValueError(format)
ValueError: YAML

songオブジェクトとserializerを作成し、.serialize()メソッドを使用して曲を文字列表現に変換します。 このメソッドは、songオブジェクトをパラメーターとして受け取り、必要な形式を表す文字列値を受け取ります。 最後の呼び出しでは、フォーマットとしてYAMLが使用されますが、これはserializerでサポートされていないため、ValueError例外が発生します。

この例は短く単純化されていますが、依然として非常に複雑です。 formatパラメータの値に応じて、3つの論理パスまたは実行パスがあります。 これは大したことではないように思えるかもしれません。おそらくこれよりも複雑なコードを見たことがあるかもしれませんが、上記の例を維持するのはまだかなり困難です。

複雑な条件付きコードの問題

上記の例は、複雑な論理コードで見つかるすべての問題を示しています。 複雑な論理コードは、if/elif/else構造体を使用してアプリケーションの動作を変更します。 if/elif/elseの条件構造を使用すると、コードが読みにくくなり、理解しにくくなり、保守しにくくなります。

上記のコードは読みにくい、または理解しにくいように思えるかもしれませんが、このセクションの最終コードが表示されるまでお待ちください!

それでも、上記のコードはあまりにも多くの作業を行っているため、維持するのが困難です。 single responsibility principleは、モジュール、クラス、またはメソッドでさえ、明確に定義された単一の責任を持つ必要があることを示しています。 それはただ一つのことをし、変更する理由を一つだけ持つべきです。

SongSerializer.serialize()メソッドは、さまざまな理由で変更が必要になります。 これにより、変更が行われたときに新しい欠陥が導入されたり、既存の機能が破損したりするリスクが高まります。 実装の変更が必要になるすべての状況を見てみましょう。

  • When a new format is introduced:その形式へのシリアル化を実装するには、メソッドを変更する必要があります。

  • When the Song object changes:Songクラスにプロパティを追加または削除するには、新しい構造に対応するために実装を変更する必要があります。

  • When the string representation for a format changes (plain JSON vs JSON API):.serialize()メソッドの実装では表現がハードコーディングされているため、フォーマットに必要な文字列表現が変更された場合、.serialize()メソッドを変更する必要があります。

理想的な状況は、.serialize()メソッドを変更せずに、要件のこれらの変更のいずれかを実装できる場合です。 次のセクションで、その方法を見てみましょう。

共通のインターフェースを探す

アプリケーションに複雑な条件付きコードが表示される最初のステップは、各実行パス(または論理パス)の共通の目標を特定することです。

if/elif/elseを使用するコードには通常、各論理パスでさまざまな方法で実装される共通の目標があります。 上記のコードは、各論理パスで異なる形式を使用して、songオブジェクトをそのstring表現に変換します。

目標に基づいて、各パスを置き換えるために使用できる共通のインターフェイスを探します。 上記の例では、songオブジェクトを受け取り、stringを返すインターフェイスが必要です。

共通のインターフェイスを作成したら、各論理パスに個別の実装を提供します。 上記の例では、JSONとXML用にシリアル化する実装を提供します。

次に、指定されたformatに基づいて、使用する具体的な実装を決定する別のコンポーネントを提供します。 このコンポーネントは、formatの値を評価し、その値によって識別される具体的な実装を返します。

次のセクションでは、動作を変更せずに既存のコードを変更する方法を学習します。 これは、refactoringコードと呼ばれます。

Martin Fowlerは、彼の著書Refactoring: Improving the Design of Existing Codeで、リファクタリングを「コードの外部動作を変更せずに内部構造を改善するような方法でソフトウェアシステムを変更するプロセス」と定義しています。

Factory Methodデザインパターンを使用する目的の構造を実現するために、コードのリファクタリングを始めましょう。

目的のインターフェイスへのコードのリファクタリング

必要なインターフェイスは、Songオブジェクトを受け取り、string表現を返すオブジェクトまたは関数です。

最初のステップは、論理パスの1つをこのインターフェースにリファクタリングすることです。 これを行うには、新しいメソッド._serialize_to_json()を追加し、それにJSONシリアル化コードを移動します。 次に、ifステートメントの本体に実装を含める代わりに、クライアントを変更して呼び出します。

class SongSerializer:
    def serialize(self, song, format):
        if format == 'JSON':
            return self._serialize_to_json(song)
        # The rest of the code remains the same

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

この変更を行ったら、動作が変更されていないことを確認できます。 次に、新しいメソッド._serialize_to_xml()を導入し、実装をそのメソッドに移動し、elifパスを変更して呼び出すことにより、XMLオプションに対して同じことを行います。

次の例は、リファクタリングされたコードを示しています。

class SongSerializer:
    def serialize(self, song, format):
        if format == 'JSON':
            return self._serialize_to_json(song)
        elif format == 'XML':
            return self._serialize_to_xml(song)
        else:
            raise ValueError(format)

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

    def _serialize_to_xml(self, song):
        song_element = et.Element('song', attrib={'id': song.song_id})
        title = et.SubElement(song_element, 'title')
        title.text = song.title
        artist = et.SubElement(song_element, 'artist')
        artist.text = song.artist
        return et.tostring(song_element, encoding='unicode')

新しいバージョンのコードは読みやすく、理解しやすいですが、Factory Methodの基本的な実装で改善できます。

ファクトリーメソッドの基本的な実装

Factory Methodの中心的な考え方は、特定のパラメーターに基づいて、どの具体的な実装を使用するかを決定する責任を別のコンポーネントに提供することです。 この例のそのパラメーターはformatです。

ファクトリメソッドの実装を完了するには、目的のformatを取る新しいメソッド._get_serializer()を追加します。 このメソッドは、formatの値を評価し、一致するシリアル化関数を返します。

class SongSerializer:
    def _get_serializer(self, format):
        if format == 'JSON':
            return self._serialize_to_json
        elif format == 'XML':
            return self._serialize_to_xml
        else:
            raise ValueError(format)

Note:._get_serializer()メソッドは具象実装を呼び出さず、関数オブジェクト自体を返すだけです。

これで、SongSerializer.serialize()メソッドを変更して、._get_serializer()を使用してファクトリメソッドの実装を完了することができます。 次の例は、完全なコードを示しています。

class SongSerializer:
    def serialize(self, song, format):
        serializer = self._get_serializer(format)
        return serializer(song)

    def _get_serializer(self, format):
        if format == 'JSON':
            return self._serialize_to_json
        elif format == 'XML':
            return self._serialize_to_xml
        else:
            raise ValueError(format)

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

    def _serialize_to_xml(self, song):
        song_element = et.Element('song', attrib={'id': song.song_id})
        title = et.SubElement(song_element, 'title')
        title.text = song.title
        artist = et.SubElement(song_element, 'artist')
        artist.text = song.artist
        return et.tostring(song_element, encoding='unicode')

最終的な実装では、ファクトリメソッドのさまざまなコンポーネントを示しています。 .serialize()メソッドは、タスクを完了するためにインターフェイスに依存するアプリケーションコードです。

これは、パターンのclientコンポーネントと呼ばれます。 定義されたインターフェイスは、productコンポーネントと呼ばれます。 この場合、積はSongを受け取り、文字列表現を返す関数です。

._serialize_to_json()および._serialize_to_xml()メソッドは、製品の具体的な実装です。 最後に、._get_serializer()メソッドはcreatorコンポーネントです。 作成者は、使用する具体的な実装を決定します。

既存のコードから始めたため、ファクトリメソッドのすべてのコンポーネントは同じクラスSongSerializerのメンバーです。

通常、これは当てはまらず、ご覧のとおり、追加されたメソッドはいずれもselfパラメータを使用しません。 これは、それらがSongSerializerクラスのメソッドであってはならず、外部関数になる可能性があることを示す良い兆候です。

class SongSerializer:
    def serialize(self, song, format):
        serializer = get_serializer(format)
        return serializer(song)


def get_serializer(format):
    if format == 'JSON':
        return _serialize_to_json
    elif format == 'XML':
        return _serialize_to_xml
    else:
        raise ValueError(format)


def _serialize_to_json(song):
    payload = {
        'id': song.song_id,
        'title': song.title,
        'artist': song.artist
    }
    return json.dumps(payload)


def _serialize_to_xml(song):
    song_element = et.Element('song', attrib={'id': song.song_id})
    title = et.SubElement(song_element, 'title')
    title.text = song.title
    artist = et.SubElement(song_element, 'artist')
    artist.text = song.artist
    return et.tostring(song_element, encoding='unicode')

Note:SongSerializer.serialize()メソッドは、selfパラメータを使用しません。

上記のルールは、クラスの一部であってはならないことを示しています。 これは正しいですが、既存のコードを扱っています。

SongSerializerを削除し、.serialize()メソッドを関数に変更した場合、SongSerializerを使用するアプリケーション内のすべての場所を変更し、新しい関数の呼び出しを置き換える必要があります。

単体テストでコードカバレッジの割合が非常に高い場合を除き、これは変更すべきではありません。

Factory Methodの仕組みは常に同じです。 クライアント(SongSerializer.serialize())は、インターフェイスの具体的な実装に依存します。 ある種の識別子(format)を使用して、作成者コンポーネント(get_serializer())に実装を要求します。

作成者はパラメータの値に応じて具体的な実装をクライアントに返し、クライアントは提供されたオブジェクトを使用してタスクを完了します。

Pythonインタラクティブインタープリターで同じ一連の命令を実行して、アプリケーションの動作が変更されていないことを確認できます。

>>>

>>> import serializer_demo as sd
>>> song = sd.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = sd.SongSerializer()

>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'

>>> serializer.serialize(song, 'XML')
'Water of LoveDire Straits'

>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "", line 1, in 
  File "./serializer_demo.py", line 13, in serialize
    serializer = get_serializer(format)
  File "./serializer_demo.py", line 23, in get_serializer
    raise ValueError(format)
ValueError: YAML

songserializerを作成し、serializerを使用して、曲をformatを指定するstring表現に変換します。 YAMLはサポートされている形式ではないため、ValueErrorが発生します。

ファクトリメソッドを使用する機会を認識する

ファクトリメソッドは、アプリケーション(クライアント)がタスクを実行するためにインターフェイス(製品)に依存し、そのインターフェイスの具体的な実装が複数あるすべての状況で使用する必要があります。 具体的な実装を識別し、作成者でそれを使用して具体的な実装を決定できるパラメータを提供する必要があります。

この説明に当てはまるさまざまな問題があるので、いくつかの具体例を見てみましょう。

Replacing complex logical code:要件の変化に応じて新しい論理パスが必要になるため、if/elif/else形式の複雑な論理構造を維持するのは困難です。

ファクトリメソッドは、各論理パスの本体を共通のインターフェイスを使用して個別の関数またはクラスに配置でき、作成者が具体的な実装を提供できるため、優れた代替手段です。

条件で評価されたパラメーターは、具体的な実装を識別するパラメーターになります。 上記の例はこの状況を表しています。

Constructing related objects from external data:データベースまたはその他の外部ソースから従業員情報を取得する必要があるアプリケーションを想像してみてください。

レコードは、さまざまなロールまたはタイプ(管理者、事務員、販売員など)を持つ従業員を表します。 アプリケーションは、従業員のタイプを表す識別子をレコードに格納し、ファクトリメソッドを使用して、レコードの残りの情報から各具象Employeeオブジェクトを作成できます。

Supporting multiple implementations of the same feature:画像処理アプリケーションは、衛星画像をある座標系から別の座標系に変換する必要がありますが、変換を実行するには、精度のレベルが異なる複数のアルゴリズムがあります。

アプリケーションは、具体的なアルゴリズムを識別するオプションをユーザーが選択できるようにします。 Factory Methodは、このオプションに基づいてアルゴリズムの具体的な実装を提供できます。

Combining similar features under a common interface:画像処理の例に従って、アプリケーションは画像にフィルターを適用する必要があります。 使用する特定のフィルターは、いくつかのユーザー入力によって識別でき、Factory Methodは具体的なフィルター実装を提供できます。

Integrating related external services:音楽プレーヤーアプリケーションは、複数の外部サービスと統合し、ユーザーが自分の音楽の出所を選択できるようにしたいと考えています。 アプリケーションは、音楽サービスの共通インターフェースを定義し、Factory Methodを使用して、ユーザーの好みに基づいて正しい統合を作成できます。

これらの状況はすべて類似しています。 これらはすべて、製品として知られる共通インターフェースに依存するクライアントを定義します。 これらはすべて、製品の具体的な実装を識別する手段を提供するため、設計でFactory Methodを使用できます。

これで、前の例のシリアル化の問題を見て、Factory Methodの設計パターンを考慮することで、より良い設計を提供できます。

オブジェクトのシリアル化の例

上記の例の基本的な要件は、Songオブジェクトをそれらのstring表現にシリアル化することです。 アプリケーションは音楽に関連する機能を提供しているようです。そのため、アプリケーションがPlaylistAlbumなどの他のタイプのオブジェクトをシリアル化する必要があると考えられます。

理想的には、既存の実装を変更せずに新しいクラスを実装することで、新しいオブジェクトのシリアル化の追加をサポートする設計が必要です。 このアプリケーションでは、オブジェクトをJSONやXMLなどの複数の形式にシリアル化する必要があるため、形式ごとに1つずつ、複数の実装を持つことができるインターフェイスSerializerを定義するのが自然なようです。

インターフェイスの実装は次のようになります。

# In serializers.py

import json
import xml.etree.ElementTree as et

class JsonSerializer:
    def __init__(self):
        self._current_object = None

    def start_object(self, object_name, object_id):
        self._current_object = {
            'id': object_id
        }

    def add_property(self, name, value):
        self._current_object[name] = value

    def to_str(self):
        return json.dumps(self._current_object)


class XmlSerializer:
    def __init__(self):
        self._element = None

    def start_object(self, object_name, object_id):
        self._element = et.Element(object_name, attrib={'id': object_id})

    def add_property(self, name, value):
        prop = et.SubElement(self._element, name)
        prop.text = value

    def to_str(self):
        return et.tostring(self._element, encoding='unicode')

Note:上記の例は完全なSerializerインターフェースを実装していませんが、私たちの目的とファクトリメソッドのデモンストレーションには十分なはずです。

Serializerインターフェースは、Python言語の動的な性質により、抽象的な概念です。 JavaやC#などの静的言語では、インターフェイスを明示的に定義する必要があります。 Pythonでは、目的のメソッドまたは機能を提供するオブジェクトは、インターフェースを実装すると言われています。 この例では、Serializerインターフェイスを、次のメソッドまたは関数を実装するオブジェクトとして定義しています。

  • .start_object(object_name, object_id)

  • .add_property(name, value)

  • .to_str()

このインターフェースは、具象クラスJsonSerializerおよびXmlSerializerによって実装されます。

元の例では、SongSerializerクラスを使用しました。 新しいアプリケーションでは、ObjectSerializerなどのより一般的なものを実装します。

# In serializers.py

class ObjectSerializer:
    def serialize(self, serializable, format):
        serializer = factory.get_serializer(format)
        serializable.serialize(serializer)
        return serializer.to_str()

ObjectSerializerの実装は完全に汎用的であり、パラメーターとしてserializableformatについてのみ言及しています。

formatは、Serializerの具体的な実装を識別するために使用され、factoryオブジェクトによって解決されます。 serializableパラメータは、シリアル化するオブジェクトタイプに実装する必要がある別の抽象インターフェイスを参照します。

Songクラスのserializableインターフェースの具体的な実装を見てみましょう。

# In songs.py

class Song:
    def __init__(self, song_id, title, artist):
        self.song_id = song_id
        self.title = title
        self.artist = artist

    def serialize(self, serializer):
        serializer.start_object('song', self.song_id)
        serializer.add_property('title', self.title)
        serializer.add_property('artist', self.artist)

Songクラスは、.serialize(serializer)メソッドを提供することにより、Serializableインターフェースを実装します。 このメソッドでは、Songクラスはserializerオブジェクトを使用して、形式を知らなくても独自の情報を書き込みます。

実際のところ、Songクラスは、データを文字列に変換することが目標であることさえ知りません。 このインターフェイスを使用して、必要に応じてSong情報を完全に異なる表現に変換する別の種類のserializerを提供できるため、これは重要です。 たとえば、アプリケーションで将来、Songオブジェクトをバイナリ形式に変換する必要がある場合があります。

これまで、クライアント(ObjectSerializer)と製品(serializer)の実装を見てきました。 Factoryメソッドの実装を完了し、作成者を提供するときが来ました。 この例の作成者は、ObjectSerializer.serialize()の変数factoryです。

オブジェクトファクトリとしてのファクトリメソッド

元の例では、作成者を関数として実装しました。 関数は非常に単純な例では問題ありませんが、要件が変わっても柔軟性はあまりありません。

クラスは、機能を追加するための追加のインターフェイスを提供でき、動作をカスタマイズするために派生できます。 将来変わらない基本的な作成者がいない限り、関数ではなくクラスとして実装する必要があります。 これらのタイプのクラスは、オブジェクトファクトリと呼ばれます。

ObjectSerializer.serialize()の実装では、SerializerFactoryの基本的なインターフェイスを確認できます。 このメソッドは、factory.get_serializer(format)を使用して、オブジェクトファクトリからserializerを取得します。

ここで、このインターフェイスを満たすためにSerializerFactoryを実装します。

# In serializers.py

class SerializerFactory:
    def get_serializer(self, format):
        if format == 'JSON':
            return JsonSerializer()
        elif format == 'XML':
            return XmlSerializer()
        else:
            raise ValueError(format)


factory = SerializerFactory()

.get_serializer()の現在の実装は、元の例で使用したものと同じです。 このメソッドは、formatの値を評価し、作成して返す具体的な実装を決定します。 これは、すべてのFactory Methodコンポーネントの機能を検証できる比較的単純なソリューションです。

Pythonインタラクティブインタープリターにアクセスして、その仕組みを見てみましょう。

>>>

>>> import songs
>>> import serializers
>>> song = songs.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = serializers.ObjectSerializer()

>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'

>>> serializer.serialize(song, 'XML')
'Water of LoveDire Straits'

>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "", line 1, in 
  File "./serializers.py", line 39, in serialize
    serializer = factory.get_serializer(format)
  File "./serializers.py", line 52, in get_serializer
    raise ValueError(format)
ValueError: YAML

Factory Methodの新しい設計により、アプリケーションは、既存のクラスを変更するのではなく、新しいクラスを追加して新しい機能を導入できます。 Serializableインターフェースを実装することにより、他のオブジェクトをシリアル化できます。 別のクラスにSerializerインターフェースを実装することにより、新しいフォーマットをサポートできます。

欠けている部分は、新しいフォーマットのサポートを含めるためにSerializerFactoryを変更する必要があるということです。 SerializerFactoryはクラスであるため、この問題は新しい設計で簡単に解決できます。

追加フォーマットのサポート

SerializerFactoryの現在の実装は、新しい形式が導入されたときに変更する必要があります。 アプリケーションは追加のフォーマットをサポートする必要はないかもしれませんが、あなたは決して知りません。

デザインに柔軟性を持たせたいと考えています。ご覧のとおり、SerializerFactoryを変更せずに追加のフォーマットをサポートするのは比較的簡単です。

アイデアは、サポートしたい形式の新しいSerializer実装を登録するメソッドをSerializerFactoryで提供することです。

# In serializers.py

class SerializerFactory:

    def __init__(self):
        self._creators = {}

    def register_format(self, format, creator):
        self._creators[format] = creator

    def get_serializer(self, format):
        creator = self._creators.get(format)
        if not creator:
            raise ValueError(format)
        return creator()


factory = SerializerFactory()
factory.register_format('JSON', JsonSerializer)
factory.register_format('XML', XmlSerializer)

.register_format(format, creator)メソッドでは、フォーマットとcreatorオブジェクトを識別するために使用されるformat値を指定することにより、新しいフォーマットを登録できます。 作成者オブジェクトは、たまたま具象Serializerのクラス名です。 これが可能なのは、すべてのSerializerクラスがインスタンスを初期化するためのデフォルトの.__init__()を提供しているためです。

登録情報は_creators辞書に保存されます。 .get_serializer()メソッドは、登録された作成者を取得し、目的のオブジェクトを作成します。 要求されたformatが登録されていない場合、ValueErrorが発生します。

これで、YamlSerializerを実装して設計の柔軟性を検証し、以前に見た煩わしいValueErrorを取り除くことができます。

# In yaml_serializer.py

import yaml
import serializers

class YamlSerializer(serializers.JsonSerializer):
    def to_str(self):
        return yaml.dump(self._current_object)


serializers.factory.register_format('YAML', YamlSerializer)

Note:この例を実装するには、pip install PyYAMLを使用して環境にPyYAMLをインストールする必要があります。

JSONとYAMLは非常によく似た形式であるため、JsonSerializerの実装のほとんどを再利用し、.to_str()を上書きして実装を完了することができます。 次に、フォーマットがfactoryオブジェクトに登録され、使用可能になります。

Pythonインタラクティブインタープリターを使用して結果を見てみましょう。

>>>

>>> import serializers
>>> import songs
>>> import yaml_serializer
>>> song = songs.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = serializers.ObjectSerializer()

>>> print(serializer.serialize(song, 'JSON'))
{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}

>>> print(serializer.serialize(song, 'XML'))
Water of LoveDire Straits

>>> print(serializer.serialize(song, 'YAML'))
{artist: Dire Straits, id: '1', title: Water of Love}

オブジェクトファクトリを使用してファクトリメソッドを実装し、登録インターフェイスを提供することにより、既存のアプリケーションコードを変更せずに新しい形式をサポートできます。 これにより、既存の機能を破壊したり、微妙なバグを導入したりするリスクが最小限に抑えられます。

汎用オブジェクトファクトリ

SerializerFactoryの実装は、元の例から大幅に改善されています。 新しいフォーマットをサポートする優れた柔軟性を提供し、既存のコードの変更を回避します。

それでも、現在の実装は上記のシリアル化の問題を特に対象としており、他のコンテキストでは再利用できません。

Factory Methodを使用すると、さまざまな問題を解決できます。 オブジェクトファクトリを使用すると、要件が変更されたときに設計の柔軟性が向上します。 理想的には、実装を複製せずにあらゆる状況で再利用できるObject Factoryの実装が必要です。

Object Factoryの汎用実装を提供するにはいくつかの課題があります。次のセクションでは、これらの課題を見て、あらゆる状況で再利用できるソリューションを実装します。

すべてのオブジェクトを均等に作成できるわけではありません

汎用オブジェクトファクトリを実装するための最大の課題は、すべてのオブジェクトが同じ方法で作成されるわけではないことです。

すべての状況で、デフォルトの.__init__()を使用してオブジェクトを作成および初期化できるわけではありません。 作成者、この場合はオブジェクトファクトリが完全に初期化されたオブジェクトを返すことが重要です。

そうでない場合、クライアントは初期化を完了し、複雑な条件コードを使用して提供されたオブジェクトを完全に初期化する必要があるため、これは重要です。 これは、ファクトリメソッドのデザインパターンの目的に反します。

汎用ソリューションの複雑さを理解するために、別の問題を見てみましょう。 アプリケーションがさまざまな音楽サービスと統合したいとします。 これらのサービスは、ローカルの音楽コレクションをサポートするために、アプリケーションの外部または内部に配置できます。 各サービスには、さまざまな要件があります。

Note:この例で定義する要件は説明を目的としたものであり、PandoraSpotifyなどのサービスと統合するために実装する必要がある実際の要件を反映していません。

その目的は、汎用オブジェクトファクトリを実装する際の課題を示すさまざまな要件を提供することです。

アプリケーションがSpotifyが提供するサービスと統合したいと考えていることを想像してください。 このサービスでは、認証のためにクライアントキーとシークレットが提供される認証プロセスが必要です。

サービスは、以降の通信で使用されるアクセスコードを返します。 この認証プロセスは非常に遅く、一度だけ実行する必要があるため、アプリケーションは初期化されたサービスオブジェクトを保持し、Spotifyと通信する必要があるたびにそれを使用する必要があります。

同時に、他のユーザーはPandoraとの統合を望んでいます。 Pandoraは完全に異なる認証プロセスを使用する場合があります。 また、クライアントキーとシークレットが必要ですが、他の通信に使用する必要があるコンシューマキーとシークレットを返します。 Spotifyと同様に、認証プロセスは遅く、一度だけ実行する必要があります。

最後に、アプリケーションは、音楽コレクションがローカルに保存されるローカル音楽サービスの概念を実装します。 このサービスでは、ローカルシステムの音楽コレクションの場所を指定する必要があります。 新しいサービスインスタンスの作成は非常に迅速に行われるため、ユーザーが音楽コレクションにアクセスするたびに新しいインスタンスを作成できます。

この例にはいくつかの課題があります。 各サービスは、異なるパラメーターセットで初期化されます。 また、SpotifyとPandoraでは、サービスインスタンスを作成する前に承認プロセスが必要です。

また、そのインスタンスを再利用して、アプリケーションを複数回認証しないようにします。 ローカルサービスはよりシンプルですが、他のサービスの初期化インターフェイスとは一致しません。

次のセクションでは、作成インターフェイスを一般化し、汎用オブジェクトファクトリを実装することにより、この問題を解決します。

共通のインターフェースを提供するための個別のオブジェクト作成

各具体的な音楽サービスの作成には、独自の要件があります。 これは、各サービス実装の共通の初期化インターフェースが不可能または推奨されないことを意味します。

最善のアプローチは、一般的なインターフェイスを提供し、具体的なサービスの作成を担当する新しいタイプのオブジェクトを定義することです。 この新しいタイプのオブジェクトは、Builderと呼ばれます。 Builderオブジェクトには、サービスインスタンスを作成および初期化するためのすべてのロジックがあります。 サポートされているサービスごとにBuilderオブジェクトを実装します。

まず、アプリケーションの構成を見てみましょう。

# In program.py

config = {
    'spotify_client_key': 'THE_SPOTIFY_CLIENT_KEY',
    'spotify_client_secret': 'THE_SPOTIFY_CLIENT_SECRET',
    'pandora_client_key': 'THE_PANDORA_CLIENT_KEY',
    'pandora_client_secret': 'THE_PANDORA_CLIENT_SECRET',
    'local_music_location': '/usr/data/music'
}

configディクショナリには、各サービスを初期化するために必要なすべての値が含まれています。 次のステップでは、これらの値を使用して音楽サービスの具体的な実装を作成するインターフェースを定義します。 そのインターフェースはBuilderで実装されます。

SpotifyServiceSpotifyServiceBuilderの実装を見てみましょう。

# In music.py

class SpotifyService:
    def __init__(self, access_code):
        self._access_code = access_code

    def test_connection(self):
        print(f'Accessing Spotify with {self._access_code}')


class SpotifyServiceBuilder:
    def __init__(self):
        self._instance = None

    def __call__(self, spotify_client_key, spotify_client_secret, **_ignored):
        if not self._instance:
            access_code = self.authorize(
                spotify_client_key, spotify_client_secret)
            self._instance = SpotifyService(access_code)
        return self._instance

    def authorize(self, key, secret):
        return 'SPOTIFY_ACCESS_CODE'

Note:音楽サービスインターフェイスは.test_connection()メソッドを定義します。これは、デモンストレーションの目的には十分なはずです。

この例は、.__call__(spotify_client_key, spotify_client_secret, **_ignored)を実装するSpotifyServiceBuilderを示しています。

このメソッドは、具象SpotifyServiceを作成および初期化するために使用されます。 必要なパラメーターを指定し、**_ignoredを介して提供される追加のパラメーターを無視します。 access_codeが取得されると、SpotifyServiceインスタンスが作成されて返されます。

SpotifyServiceBuilderはサービスインスタンスを保持し、サービスが最初に要求されたときにのみ新しいインスタンスを作成することに注意してください。 これにより、要件で指定されているように承認プロセスを何度も繰り返す必要がなくなります。

Pandoraについても同じことをしましょう。

# In music.py

class PandoraService:
    def __init__(self, consumer_key, consumer_secret):
        self._key = consumer_key
        self._secret = consumer_secret

    def test_connection(self):
        print(f'Accessing Pandora with {self._key} and {self._secret}')


class PandoraServiceBuilder:
    def __init__(self):
        self._instance = None

    def __call__(self, pandora_client_key, pandora_client_secret, **_ignored):
        if not self._instance:
            consumer_key, consumer_secret = self.authorize(
                pandora_client_key, pandora_client_secret)
            self._instance = PandoraService(consumer_key, consumer_secret)
        return self._instance

    def authorize(self, key, secret):
        return 'PANDORA_CONSUMER_KEY', 'PANDORA_CONSUMER_SECRET'

PandoraServiceBuilderは同じインターフェースを実装しますが、異なるパラメーターとプロセスを使用してPandoraServiceを作成および初期化します。 また、サービスインスタンスを保持するため、認証は1回だけ行われます。

最後に、ローカルサービスの実装を見てみましょう。

# In music.py

class LocalService:
    def __init__(self, location):
        self._location = location

    def test_connection(self):
        print(f'Accessing Local music at {self._location}')


def create_local_music_service(local_music_location, **_ignored):
    return LocalService(local_music_location)

LocalServiceは、LocalServiceを初期化するために、コレクションが格納されている場所を必要とします。

遅い認証プロセスがないため、サービスが要求されるたびに新しいインスタンスが作成されます。 要件はより単純なので、Builderクラスは必要ありません。 代わりに、初期化されたLocalServiceを返す関数が使用されます。 この関数は、ビルダークラスに実装されている.__call__()メソッドのインターフェイスと一致します。

オブジェクトファクトリへの汎用インターフェイス

汎用オブジェクトファクトリ(ObjectFactory)は、汎用のBuilderインターフェイスを利用して、あらゆる種類のオブジェクトを作成できます。 key値に基づいてBuilderを登録するメソッドと、keyに基づいて具体的なオブジェクトインスタンスを作成するメソッドを提供します。

一般的なObjectFactoryの実装を見てみましょう。

# In object_factory.py

class ObjectFactory:
    def __init__(self):
        self._builders = {}

    def register_builder(self, key, builder):
        self._builders[key] = builder

    def create(self, key, **kwargs):
        builder = self._builders.get(key)
        if not builder:
            raise ValueError(key)
        return builder(**kwargs)

ObjectFactoryの実装構造は、SerializerFactoryで見たものと同じです。

違いは、あらゆるタイプのオブジェクトの作成をサポートするために公開するインターフェースにあります。 ビルダーパラメーターは、callableインターフェイスを実装する任意のオブジェクトにすることができます。 つまり、Builderは、.__call__()を実装する関数、クラス、またはオブジェクトにすることができます。

.create()メソッドでは、追加の引数をキーワード引数として指定する必要があります。 これにより、Builderオブジェクトは必要なパラメーターを指定し、残りを特定の順序で無視することができます。 たとえば、create_local_music_service()local_music_locationパラメータを指定し、残りを無視していることがわかります。

ファクトリインスタンスを作成し、サポートするサービスのビルダーを登録しましょう。

# In music.py
import object_factory

# Omitting other implementation classes shown above

factory = object_factory.ObjectFactory()
factory.register_builder('SPOTIFY', SpotifyServiceBuilder())
factory.register_builder('PANDORA', PandoraServiceBuilder())
factory.register_builder('LOCAL', create_local_music_service)

musicモジュールは、factory属性を介してObjectFactoryインスタンスを公開します。 次に、ビルダーがインスタンスに登録されます。 SpotifyとPandoraの場合、対応するビルダーのインスタンスを登録しますが、ローカルサービスの場合は、関数を渡すだけです。

機能を示す小さなプログラムを作成しましょう。

# In program.py
import music

config = {
    'spotify_client_key': 'THE_SPOTIFY_CLIENT_KEY',
    'spotify_client_secret': 'THE_SPOTIFY_CLIENT_SECRET',
    'pandora_client_key': 'THE_PANDORA_CLIENT_KEY',
    'pandora_client_secret': 'THE_PANDORA_CLIENT_SECRET',
    'local_music_location': '/usr/data/music'
}

pandora = music.factory.create('PANDORA', **config)
pandora.test_connection()

spotify = music.factory.create('SPOTIFY', **config)
spotify.test_connection()

local = music.factory.create('LOCAL', **config)
local.test_connection()

pandora2 = music.services.get('PANDORA', **config)
print(f'id(pandora) == id(pandora2): {id(pandora) == id(pandora2)}')

spotify2 = music.services.get('SPOTIFY', **config)
print(f'id(spotify) == id(spotify2): {id(spotify) == id(spotify2)}')

アプリケーションは、アプリケーション構成を表すconfigディクショナリを定義します。 設定は、アクセスするサービスに関係なく、ファクトリのキーワード引数として使用されます。 ファクトリは、指定されたkeyパラメータに基づいて音楽サービスの具体的な実装を作成します。

プログラムを実行して、その動作を確認できます。

$ python program.py
Accessing Pandora with PANDORA_CONSUMER_KEY and PANDORA_CONSUMER_SECRET
Accessing Spotify with SPOTIFY_ACCESS_CODE
Accessing Local music at /usr/data/music
id(pandora) == id(pandora2): True
id(spotify) == id(spotify2): True

指定したサービスタイプに応じて、正しいインスタンスが作成されていることがわかります。 また、PandoraまたはSpotifyサービスをリクエストすると、常に同じインスタンスが返されることがわかります。

コードの読みやすさを改善するためのオブジェクトファクトリの専門化

一般的なソリューションは再利用可能で、コードの重複を防ぎます。 残念ながら、彼らはまた、コードを覆い隠し、読みにくくします。

上記の例は、音楽サービスにアクセスするために、music.factory.create()が呼び出されることを示しています。 これは混乱を招く可能性があります。 他の開発者は、毎回新しいインスタンスが作成されると信じ、初期化プロセスの遅延を避けるためにサービスインスタンスを保持することを決定する場合があります。

Builderクラスは初期化されたインスタンスを保持し、後続の呼び出しのためにそれを返すため、これは起こらないことを知っていますが、これはコードを読むだけでは明らかではありません。

良い解決策は、汎用実装を特化して、アプリケーションコンテキストに具体的なインターフェイスを提供することです。 このセクションでは、音楽サービスのコンテキストでObjectFactoryを専門化するため、アプリケーションコードは意図をより適切に伝達し、読みやすくなります。

次の例は、ObjectFactoryを特殊化して、アプリケーションのコンテキストへの明示的なインターフェイスを提供する方法を示しています。

# In music.py

class MusicServiceProvider(object_factory.ObjectFactory):
    def get(self, service_id, **kwargs):
        return self.create(service_id, **kwargs)


services = MusicServiceProvider()
services.register_builder('SPOTIFY', SpotifyServiceBuilder())
services.register_builder('PANDORA', PandoraServiceBuilder())
services.register_builder('LOCAL', create_local_music_service)

ObjectFactoryからMusicServiceProviderを派生させ、新しいメソッド.get(service_id, **kwargs)を公開します。

このメソッドは汎用の.create(key, **kwargs)を呼び出すため、動作は同じままですが、アプリケーションのコンテキストではコードの読み取りが向上します。 また、以前のfactory変数の名前をservicesに変更し、MusicServiceProviderとして初期化しました。

ご覧のとおり、更新されたアプリケーションコードの読み取りが改善されました。

import music

config = {
    'spotify_client_key': 'THE_SPOTIFY_CLIENT_KEY',
    'spotify_client_secret': 'THE_SPOTIFY_CLIENT_SECRET',
    'pandora_client_key': 'THE_PANDORA_CLIENT_KEY',
    'pandora_client_secret': 'THE_PANDORA_CLIENT_SECRET',
    'local_music_location': '/usr/data/music'
}

pandora = music.services.get('PANDORA', **config)
pandora.test_connection()
spotify = music.services.get('SPOTIFY', **config)
spotify.test_connection()
local = music.services.get('LOCAL', **config)
local.test_connection()

pandora2 = music.services.get('PANDORA', **config)
print(f'id(pandora) == id(pandora2): {id(pandora) == id(pandora2)}')

spotify2 = music.services.get('SPOTIFY', **config)
print(f'id(spotify) == id(spotify2): {id(spotify) == id(spotify2)}')

プログラムを実行すると、動作が変更されていないことが示されます。

$ python program.py
Accessing Pandora with PANDORA_CONSUMER_KEY and PANDORA_CONSUMER_SECRET
Accessing Spotify with SPOTIFY_ACCESS_CODE
Accessing Local music at /usr/data/music
id(pandora) == id(pandora2): True
id(spotify) == id(spotify2): True

結論

ファクトリメソッドは、インターフェイスの具体的な実装が複数存在する多くの状況で使用できる、広く使用されている創造的なデザインパターンです。

このパターンは、保守が難しい複雑な論理コードを削除し、再利用可能で拡張可能な設計に置き換えます。 このパターンは、新しい要件をサポートするために既存のコードを変更することを避けます。

これは重要です。既存のコードを変更すると、動作の変更や微妙なバグが発生する可能性があるためです。

この記事では、次のことを学びました。

  • Factory Methodのデザインパターンとそのコンポーネント

  • Factory Methodを活用するために既存のコードをリファクタリングする方法

  • Factory Methodを使用する必要がある状況

  • オブジェクトファクトリがファクトリメソッドを実装するための柔軟性を高める方法

  • 汎用オブジェクトファクトリの実装方法とその課題

  • より良いコンテキストを提供するために一般的なソリューションを特化する方法

参考文献

ファクトリメソッドやその他のデザインパターンについて詳しく知りたい場合は、GoFによるDesign Patterns: Elements of Reusable Object-Oriented Softwareをお勧めします。これは、広く採用されているデザインパターンの優れたリファレンスです。

また、EricFreemanとElisabethRobsonによるHeads First Design Patterns: A Brain-Friendly Guideは、デザインパターンの楽しくて読みやすい説明を提供します。

ウィキペディアには、最も一般的で有用なパターンのページへのリンクを含むdesign patternsの優れたカタログがあります。