Python

Python デザインパターンを学ぶ Flyweightパターン

概要

データサイエンスチームに所属するなど、ビッグデータを扱わなければならない場合、このパターンは特に役に立つと思われる。

複数のオブジェクトが共通のメモリ集約状態を持っているような状況では、このFlyweightパターンを実装することで、メモリ集約状態を分離して、共通の共有オブジェクトに格納することができる。つまり、オブジェクト「A」がデータ「x」と「y」をステートとして保持し、オブジェクト「B」がデータ「y」と「z」を保持している場合、「y」を格納するフライウェイトオブジェクト「C」を作成することができる。これで、’A’と’B’は’y’のインスタンスを別々に保持する必要はなく、フライウェイトの状態を共有することができる。

以下の実装では、このパターンを利用して、データベースへの複数のクエリを含む複雑なデータリクエストを処理できるモジュールを構築する。各ComplexRequestは、新しいデータのクエリ(FreshQuery)と過去データのクエリ(HistoricalQuery)の2種類のクエリを受け入れる。さらに、過去データのクエリ用のキャッシュへのポインタを受け取る。過去データは変更されないと仮定して、HistoricalQueryクラスをフライウェイトとして実装し、過去データのCacheはフライウェイト・オブジェクトの共有のステート・キャッシュとして機能する。ユーザーはこれを利用して、新しいデータや以前に取得されていないデータのみを照会する複雑なクエリを作成することができる。

HistoricalDataCacheクラスは、各過去データのクエリに対して、1回のDBのReadが実行される。

Queryクラスを FreshQuery および HistoricalQuery サブクラスに分けることで、キャッシュ可能なクエリを明確にする。self.data属性には、管理しようとしているメモリ集約型の状態が格納される。今回の実装では、初期化時にこの属性を設定することに意味がある。

ComplexRequestクラスは、説明のために実装が簡略化されている。このクラスを利用して、複数のクエリを作成・集約することで、複雑なデータセットを構築することができる。初期化時には、必要なすべてのクエリが、過去のクエリと新しいクエリの2種類に分けられる。過去のクエリの処理に使用されるため、historical_cacheへのポインタも保存される。

get()関数は、実際にする場合は新しいデータと過去のデータの間の関係(複数のデータセットをマージ、結合、ピボット、またはグループ化するなど)を確立する責任がある。

_get_historical_data()関数では、過去に照会した履歴データを再利用するために、履歴データのキャッシュとフライウェイトパターンを利用する。

この例では、database_2テーブルからすべてのデータを取得する履歴クエリを共有する2つの複雑なリクエストを行う。したがって、このクエリは両方のリクエストに追加されているにもかかわらず、DBには一度しか送信されない。

実装例

class HistoricalDataCache:

    # クラスのすべてのインスタンスで共有されるハッシュマップ(dict)として実装される
    _cache = {}

    def get(self, query_string):

        query_hash = self._query_hash(query_string)

        if not (query_hash in self.__class__._cache):
            self.__class__._cache[query_hash] = HistoricalQuery(query_string)

        return self.__class__._cache[query_hash]

    def _query_hash(self, query_string):
        """
        _cache のクエリのユニークキーとして使用する、query_stringのユニークな10桁の整数ハッシュを作成する。
        """

        return abs(hash(query_string)) % (10**10)


class Query:

    def __init__(self, query_string):

        self.query_string = query_string
        self.data = self.get_data()

    def get_data(self):
        # 仮の実装
        return "Data for {}: {}".format(self.__class__.__name__, self.query_string)


class FreshQuery(Query):
    pass


class HistoricalQuery(Query):
    pass


class ComplexRequest:

    def __init__(self, historical_queries, fresh_queries, historical_cache):

        self.historical_queries = historical_queries
        self.fresh_queries = fresh_queries
        self.historical_cache = historical_cache

    def get(self):

        fresh_data = self._get_fresh_data()
        historical_data = self._get_historical_data()
        print("Merging  following data sets:")
        print("\n\t".join(fresh_data))
        print("\n\t".join(historical_data))

    def _get_fresh_data(self):

        fresh_data = []

        for query_string in self.fresh_queries:
            query = FreshQuery(query_string)
            fresh_data.append(query.data)

        return fresh_data

    def _get_historical_data(self):
        historical_data = []

        for query_string in self.historical_queries:
            query = self.historical_cache.get(query_string)
            historical_data.append(query.data)

        return historical_data


if __name__ == '__main__':

    historical_cache = HistoricalDataCache()

    request_1 = ComplexRequest(
        historical_queries=[
            "SELECT * FROM database_1",
            "SELECT * FROM database_2"
        ],
        fresh_queries=[
            "SELECT * FROM table_1",
            "SELECT * FROM table_2"
        ],
        historical_cache=historical_cache
    )
    data_1 = request_1.get()

    request_2 = ComplexRequest(
        historical_queries=[
            "SELECT * FROM database_2",
            "SELECT * FROM database_3"
        ],
        fresh_queries=[
            "SELECT * FROM table_1",
            "SELECT * FROM table_2"
        ],
        historical_cache=historical_cache
    )
    data_2 = request_2.get()