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

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

この記事では、Factory Methodの設計パターンとPythonでの実装について説明します。 デザインパターンは、いわゆるギャングオブフォー(GoF:Gamma、Helm、Johson、Vlissides)がhttps://realpython.com/asins/0201633612[Design Patterns:Elements of Reusable]を出版した後、90年代後半に人気のトピックになりました。オブジェクト指向ソフトウェア]。

この本は、ソフトウェアで繰り返し発生する問題に対するコア設計ソリューションとしての設計パターンについて説明し、各設計パターンを問題の性質に応じてhttps://en.wikipedia.org/wiki/Software_design_pattern#Classification_and_list[categories]に分類します。 各パターンには、名前、問題の説明、設計ソリューション、およびそれを使用した結果の説明が与えられます。

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

これは、ファクトリメソッドを最も広く使用されているデザインパターンの1つにする*再発する問題であり、それを理解し、それを適用する方法を知ることが非常に重要です。

この記事の終わりまでに、次のことを行います

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

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

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

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

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

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

無料ボーナス: link:[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 +`クラスと、 `+ format +の値に従って + song + オブジェクトを + string + 表現に変換できる + SongSerializer + `クラスがあります。 `パラメータ。

`+ .serialize()`メソッドは、https://json.org/[JSON]とhttps://www.xml.com/axml/axml.html[XML]の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')
'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'

>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  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 +`条件構造を使用すると、コードが読みにくくなり、理解しにくくなり、維持しにくくなります。

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

それでも、上記のコードはあまりにも多くの作業を行っているため、維持するのが困難です。 https://en.wikipedia.org/wiki/Single_responsibility_principle [単一責任原則]は、モジュール、クラス、またはメソッドでさえ、明確に定義された単一の責任を持つべきであると述べています。 それはただ一つのことをし、変更する理由を一つだけ持つべきです。

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

  • *新しい形式が導入された場合:*メソッドは、その形式へのシリアル化を実装するために変更する必要があります。

  • * `+ Song `オブジェクトが変更されるとき:* ` Song +`クラスにプロパティを追加または削除するには、新しい構造に対応するために実装を変更する必要があります。

  • フォーマットの文字列表現が変更されたとき(プレーンhttps://json.org/[JSON] vs JSON API): `+ .serialize()`メソッドは表現が ` .serialize()+`メソッドの実装でハードコード化されているため、形式の目的の文字列表現が変更された場合に変更します。

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

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

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

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

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

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

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

次のセクションでは、動作を変更せずに既存のコードを変更する方法を学習します。 これは、コードのhttps://en.wikipedia.org/wiki/Code_refactoring[refactoring]と呼ばれます。

Martin Fowlerは著書https://realpython.com/asins/0134757599[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 +」です。

Factory Methodの実装を完了するには、目的の `+ 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)

注意: `+ ._ get_serializer()+`メソッドは具体的な実装を呼び出さず、関数オブジェクト自体を返します。

これで、 `+ ._ get_serializer()`を使用してFactoryメソッドの実装を完了するために、 ` SongSerializer `の ` .serialize()+`メソッドを変更できます。 次の例は、完全なコードを示しています。

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 コンポーネントです。 作成者は、使用する具体的な実装を決定します。

既存のコードから始めたため、Factory Methodのすべてのコンポーネントは同じクラス `+ 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')

注意: `+ 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')
'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'

>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  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

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

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

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

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

*複雑な論理コードの置き換え:*要件の変更に応じて新しい論理パスが必要になるため、 `+ if/elif/else +`形式の複雑な論理構造は維持が困難です。

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

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

*外部データからの関連オブジェクトの構築:*データベースまたは他の外部ソースから従業員情報を取得する必要があるアプリケーションを想像してください。

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

*同じ機能の複数の実装のサポート:*画像処理アプリケーションは、衛星画像をある座標系から別の座標系に変換する必要がありますが、変換を実行するには精度の異なる複数のアルゴリズムがあります。

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

*共通のインターフェースで同様の機能を組み合わせる:*画像処理の例に従って、アプリケーションは画像にフィルターを適用する必要があります。 使用する特定のフィルターは、いくつかのユーザー入力によって識別でき、Factory Methodは具体的なフィルター実装を提供できます。

*関連する外部サービスの統合:*音楽プレーヤーアプリケーションは、複数の外部サービスと統合し、ユーザーが音楽の発信元を選択できるようにします。 アプリケーションは、音楽サービスの共通インターフェースを定義し、Factory Methodを使用して、ユーザーの好みに基づいて正しい統合を作成できます。

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

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

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

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

理想的には、既存の実装を変更せずに新しいクラスを実装することで、新しいオブジェクトのシリアル化の追加をサポートする設計が必要です。 アプリケーションでは、オブジェクトを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')

*注意:*上記の例は完全な `+ Serializer +`インターフェースを実装していませんが、私たちの目的とFactory Methodを実証するには十分なはずです。

`+ Serializer `インターフェースは、https://www.python.org/[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 `の実装は完全に汎用的であり、パラメーターとして ` serializable `と ` format +`のみに言及しています。

`+ 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')
'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'

>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  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)

*注意:*この例を実装するには、 + pip install PyYAML +`を使用して、https://pypi.org/project/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'))
<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>

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

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

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

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

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

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

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

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

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

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

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

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

*注:*この例で定義する要件は説明のためのものであり、https://www.pandora.com [Pandora]やhttps://wwwなどのサービスと統合するために実装する必要がある実際の要件を反映していません。 .spotify.com [Spotify]。

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

アプリケーションが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 +`で実装されます。

`+ SpotifyService `と ` SpotifyServiceBuilder +`の実装を見てみましょう:

# 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'

*注意:*音楽サービスインターフェースは `+ .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 +`で見たものと同じです。

違いは、あらゆるタイプのオブジェクトの作成をサポートするために公開するインターフェースにあります。 builderパラメーターには、https://docs.python.org/3.7/library/functions.html#callable [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を使用する必要がある状況

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

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

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

参考文献

Factory Methodやその他のデザインパターンの詳細については、GoFのhttps://realpython.com/asins/0201633612 [デザインパターン:再利用可能なオブジェクト指向ソフトウェアの要素]をお勧めします。採用されたデザインパターン。

また、Eric FreemanとElisabeth Robsonによるhttps://realpython.com/asins/0596007124[Heads First Design Patterns:A Brain-Friendly Guide]は、デザインパターンの楽しくて読みやすい説明を提供します。

ウィキペディアには、https://en.wikipedia.org/wiki/Software_design_pattern [デザインパターン]の優れたカタログがあり、最も一般的で有用なパターンのページへのリンクがあります。