web-dev-qa-db-ja.com

キャッシングファクトリーデザイン

_class XFactory_のオブジェクトを作成するファクトリ_class X_があります。 Xのインスタンスは非常に大きいため、ファクトリの主な目的は、クライアントコードに対してできるだけ透過的にインスタンスをキャッシュすることです。 _class X_のオブジェクトは不変であるため、次のコードは妥当なようです。

_# module xfactory.py
import x
class XFactory:
  _registry = {}

  def get_x(self, arg1, arg2, use_cache = True):
    if use_cache:
      hash_id = hash((arg1, arg2))
      if hash_id in _registry:
        return _registry[hash_id]
    obj = x.X(arg1, arg2)
    _registry[hash_id] = obj
    return obj

# module x.py
class X:
  # ...
_

それは良いパターンですか? (実際のファクトリーパターンではないことはわかっています。)変更すべき点はありますか?

現在、Xオブジェクトをディスクにキャッシュしたい場合があります。そのためにpickleを使用し、オブジェクトへの参照の代わりに、ピクル化したオブジェクトのファイル名を__registry_に値として保存します。もちろん、__registry_自体は永続的に保存する必要があります(おそらく、独自のピクルファイル、テキストファイル、データベース、または単純にピクルファイルに_hash_id_を含むファイル名を与えることによって)。 。

現在は、キャッシュされたオブジェクトの有効性は、get_x()に渡されるパラメーターだけでなく、これらのオブジェクトを作成したコードのバージョンにも依存しています。

厳密に言えば、誰かが_x.py_またはその依存関係を変更し、プログラムの実行中に再ロードすると、メモリキャッシュオブジェクトでも無効になる可能性があります。これまでのところ、この危険性は無視しました。自分のアプリケーションにとってはありそうにないことです。ただし、オブジェクトが永続ストレージにキャッシュされている場合は、無視できません。

私に何ができる?引数_hash_id_と_arg1_、および_arg2_のファイル名と最終更新日を含むタプルのハッシュを計算することにより、_x.py_をより堅牢にすることができると思います(再帰的に)依存するすべてのモジュールとデータファイル二度と役に立たなくなるキャッシュファイルを削除するために、各レコードの変更された日付のハッシュされていない表現を__registry_に追加します。

しかし、理論的には誰かが動的にモジュールをロードする可能性があるため、このソリューションでさえ100%安全ではありません。ソースコードを静的に分析することからは、私はそれについて知りません。私がすべて出て、プロジェクト内のすべてのファイルが依存関係であると想定した場合、一部のモジュールが外部のWebサイトなどからデータを取得すると、メカニズムはまだ壊れます。

さらに、_x.py_とその依存関係の変更の頻度が非常に高いため、キャッシュの無効化が頻繁に発生します。

したがって、ある程度の安全性を放棄し、明らかな不一致がある場合にのみキャッシュを無効にすることも考えました。つまり、_class X_には、クラスレベルのキャッシュ検証識別子があり、開発者がキャッシュを無効にする変更が発生したと信じている場合は常に変更する必要があります。 (複数の開発者がいる場合、それぞれに個別の無効化識別子が必要です。)この識別子は_arg1_および_arg2_とともにハッシュされ、__registry_に格納されているハッシュキーの一部になります。

開発者は検証識別子を更新するのを忘れるか、既存のキャッシュが無効化されていることに気付かない可能性があるため、別の検証メカニズムを追加するほうがよいように思われます。_class X_は、X。たとえば、Xがテーブルの場合、すべての列の名前を追加します。ハッシュ計算には、特性も含まれます。

このコードを書くことはできますが、重要な何かが欠けていると思います。また、このすべてをすでに実行できるフレームワークまたはパッケージがあるかどうかも疑問に思っています。インメモリキャッシングとディスクベースキャッシングを組み合わせるのが理想的です。

編集:

私のニーズはプールパターンによって十分に満たすことができるように思えるかもしれません。しかし、さらに調査すると、そうではありません。私は違いを挙げようと思いました:

  1. オブジェクトは複数のクライアントで使用できますか?

    • プール:いいえ、各オブジェクトをチェックアウトして、不要になったときにチェックインする必要があります。正確なメカニズムは複雑かもしれません。
    • XFactory:はい。オブジェクトは不変であり、一度に無限に多くのクライアントが使用できます。同じオブジェクトの2番目のコピーを作成する必要はありません。
  2. プールサイズを制御する必要がありますか?

    • プール:多くの場合、はい。もしそうなら、そうするための戦略はかなり複雑かもしれません。
    • XFactory:いいえ。オブジェクトはオンデマンドでクライアントに配信する必要があり、既存のオブジェクトが適切でない場合は、新しいオブジェクトを作成する必要があります。
  3. すべてのオブジェクトは自由に置き換え可能ですか?

    • プール:はい、オブジェクトは通常自由に置き換え可能です(そうでない場合は、クライアントが必要とするオブジェクトを確認するのは簡単です)。
    • XFactory:絶対にそうではありません。特定のオブジェクトが特定のクライアント要求を処理できるかどうかを確認することは非常に困難です。これは、(a)同じ引数と(b)同じバージョンのソースコードで作成された既存のオブジェクトが使用可能かどうかによって異なります。パート(b)はXFactoryで検証できないため、クライアントに支援を求めます。クライアントは2つの方法でこの責任を果たします。最初に、クライアントは、指定された複数の内部バージョンカウンター(開発者ごとに1つ)のいずれかを増分できます。これは実行時に発生することはありません。ソースコードの変更により既存のオブジェクトが使用できなくなると考えた場合にのみ、これらのカウンターを変更できます。次に、クライアントは必要なオブジェクトに関する不変条件を返し、XFactoryはオブジェクトをクライアントに提供する前にこれらの不変条件が違反されていないことを確認します。これらのチェックのいずれかが失敗すると、XFactoryは新しいオブジェクトを作成して配信します。
  4. パフォーマンスへの影響は注意深い分析が必要ですか?

    • プール:はい。場合によっては、オブジェクト管理のオーバーヘッドがオブジェクトの作成/破棄のオーバーヘッドよりも大きいと、プールが実際にパフォーマンスを低下させることがあります。
    • XFactory:いいえ。問題のオブジェクトの計算コストは​​非常に高いことが知られており、それらをメモリまたはディスクからロードすることは、最初から再計算するよりも優れています。
  5. オブジェクトはいつ破壊されますか?

    • プール:プールがシャットダウンされたとき。リソースを(部分的に)解放するように指示された場合、または特定のオブジェクトがしばらく使用されなかった場合、オブジェクトを破壊する可能性もあります。
    • XFactory:不変の違反またはカウンターの不一致のいずれかによって証明されるように、最新ではなくなったソースコードのバージョンでオブジェクトが作成された場合。このようなオブジェクトを適切なタイミングで見つけて破棄するプロセスは非常に複雑です。さらに、すべてのオブジェクトの時間ベースの無効化を実装して、無効なオブジェクトを使用することで蓄積されたリスクを軽減できます。 XFactoryはそれがオブジェクトの唯一の所有者であることを決して確認しないため、このような無効化は、クライアントオブジェクトの追加の「バージョンカウンター」によって最適に達成されます。
  6. マルチスレッド環境にはどのような特別な考慮事項がありますか?

    • プール:オブジェクトのチェックアウト/チェックインでの衝突を回避する必要があります(オブジェクトを2つのクライアントにチェックアウトしたくない)
    • XFactory:オブジェクト作成時の衝突を回避する必要があります(2つの同一のリクエストに基づいて2つのオブジェクトを作成したくない)
  7. クライアントがオブジェクトを解放しない場合、何をする必要がありますか?

    • プール:しばらく待ってから、オブジェクトを他のユーザーが使用できるようにする場合があります。
    • XFactory:適用されません。クライアントは、オブジェクトがいつ処理されたかをXFactoryに通知しません。
  8. オブジェクトを変更する必要がありますか?

    • プール:再利用する前にデフォルトの状態にリセットする必要がある場合があります。
    • XFactory:いいえ、オブジェクトは不変です。
  9. オブジェクトの永続化に関連する特別な考慮事項はありますか?

    • プール:通常はありません。プールはオブジェクト作成のコストを節約するためのものなので、すべてのオブジェクトがメモリに保持されます(ディスクからの読み取りは目的に反します)。
    • XFactory:はい、XFactoryは複雑な計算を実行するコストを節約するためのものです。そのため、事前計算されたオブジェクトをディスクに保存することは理にかなっています。その結果、XFactoryは永続ストレージの一般的な問題に対処する必要があります。例えば初期化時に、永続ストレージに接続し、そこから現在使用可能なオブジェクトに関するメタデータを取得し、要求された場合はそれらをメモリにロードする準備ができている必要があります。オブジェクトは、「存在しない」、「ディスクに存在する」、「メモリに存在する」の3つの状態のいずれかになります。 XFactoryの実行中、状態は一方向(このシーケンスでは右側)にのみ変化します。

要約すると、プールの複雑さはアイテム1、2、4、6、およびおそらく5、7、8にあります。XFactoryの複雑度はアイテム3、6、9にあります。唯一のオーバーラップはアイテム6であり、コアではありませんプールまたはXFactoryのいずれかの機能ですが、マルチスレッド環境で機能する必要があるすべてのパターンに共通する設計上の制約です。

8
max

あなたの懸念は非常に有効であり、元の簡単なキャッシングソリューションが最終的にアーキテクチャの一部になり、自然に新しいレベルの問題を引き起こしていることがわかります。

キャッシングに対する優れたアーキテクチャソリューションは、注釈をIoCと組み合わせて使用​​し、説明したいくつかの問題を解決することです。例えば:

  • キャッシュされたオブジェクトのライフサイクルをより適切に制御できます
  • (実装を変更する代わりに)アノテーションを変更することで、キャッシュ動作を簡単に置き換えることができます。
  • たとえば、メモリ、次にディスクキャッシュに格納できる多層キャッシュを簡単に構成できます
  • アノテーション自体で各メソッドのキー(ハッシュ)を定義できます

私のプロジェクト(JavaまたはC#)では、Springキャッシングアノテーションを使用しています。簡単な説明 ここ を見つけることができます。

IoC は、必要に応じてキャッシュシステムを構成できるため、このソリューションの重要な概念です。

Pythonに同様のソリューションを実装するには、注釈を使用する方法を見つけ、プロキシを構築できるIoCコンテナを検索する必要があります。それが、すべてのメソッドをインターセプトするために注釈が機能する方法です。キャッシングのためのこの特定のソリューションを呼び出して提供します。

4
Alex

キャッシュの見方は良いです-Xはそうではありません。

単一インスタンスのIMHO逆シリアル化は、キャッシュの問題ではありません。それは相応のクラスのための仕事です。ここでの主な問題は、このクラスが頻繁に変更されることです。インスタンスをキャッシュすることの懸念とオブジェクトを逆シリアル化することの懸念を分離することをお勧めします。後者は、Xが古い形式も逆シリアル化できるように改善する必要があります。これは非常にトリッキーで高価な場合があります。それが高すぎる場合、Xが頻繁に変更される限り、古いバージョンをロードする必要があるかどうかを自問する必要がありますか?

ところで、バージョン識別子は必須のようです。 Xの構造についてのさらなる知識がなければ、私は推測しかできませんが、Xの構造は論理的にモジュール化されているようです(たとえば、特性について話しました)。もしそうなら、多分それはこの構造を明示的にすることを助けるでしょう。

1
scarfridge