デザインパターン-Proxy

PythonにおけるProxyの実装パターンまとめ

Proxy - 定義

A class that functions as an interface to a particular resource. That resource may be remote, expensive to construct, or may require logging or some other added functionality.

  • A proxy has the same interface as the underlying object
  • To create a proxy, simply replicate the existing interface of an object
  • Add relevant functionality to the redefined member functions
  • Different proxies (communication, logging, caching, etc.) have completely different behaviors

前言

ソフトウェア設計において、あるオブジェクトに「直接アクセスさせたくない」「まだアクセスしたくない」という場面は意外と多くあります。

  • セキュリティ:特定の権限がないと実行させたくない。
  • パフォーマンス:初期化に時間がかかるので、本当に必要になるまで作りたくない。
  • ロギング:実行の前後にログを残したい。

これらを解決するのがProxyパターンです。 Proxyは、対象となるオブジェクトと同じインターフェースを持ち、クライアントと対象オブジェクトの間に入って制御を行います。

Pythonによる具体的なコード例を使って、代表的な2つのProxyパターンの実装をまとめます。

実装方法

Protection Proxy - アクセス制御

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class SensitiveDocument:
    def __init__(self, content):
        self.content = content

    def read(self):
        print(f"Reading content: {self.content}")

class DocumentProxy:
    def __init__(self, user, document):
        self.user = user
        self.document = document

    def read(self):
        # アクセス制御ロジック: 役職がManagerの場合のみ許可
        if self.user.role == 'Manager':
            self.document.read()
        else:
            print(f"Access Denied: User '{self.user.name}' does not have permission.")

class User:
    def __init__(self, name, role):
        self.name = name
        self.role = role

        
>>> employee = User('Alice', 'Staff')
>>> secret_doc = SensitiveDocument('Company Secrets...')
>>> proxy = DocumentProxy(employee, secret_doc)
>>> proxy.read()
Access Denied: User 'Alice' does not have permission.

>>> manager = User('Bob', 'Manager')
>>> proxy_for_manager = DocumentProxy(manager, secret_doc)
>>> proxy_for_manager.read()
Reading content: Company Top Secrets...

ここでのDocumentProxyは、本物のSensitiveDocumentと同じread()メソッドを持っています。

クライアントから呼び出された際に、まずユーザーの役職をチェックします。そして、「Managerである」という条件を満たしたときだけ、 内部で保持している本物のSensitiveDocumentに処理を委譲しています。

これにより、「SensitiveDocument自体に、権限確認のロジックを書かなくて済む」 という大きなメリットが生まれます。

SensitiveDocumentクラスは純粋に「中身を表示する」ことだけに集中し、 面倒な権限管理はすべてProxyが担当する。 これによって、コードの役割分担がきれいに実現できます。

Virtual Proxy - 遅延初期化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import time

class RealDatabase:
    def __init__(self):
        print("Connecting to the database... ")
        # 接続に時間がかかることをシミュレート
        time.sleep(3)
        print("Connected to Database!")

    def query(self, sql):
        print(f"Executing SQL: {sql}")

class LazyDatabaseProxy:
    def __init__(self):
        self._real_db = None

    def query(self, sql):
        # 実際にクエリを投げるときまで、接続(インスタンス化)を遅らせる
        if self._real_db is None:
            self._real_db = RealDatabase()
        self._real_db.query(sql)

# クライアントコード
def run_analytics(db):
    print("Preparing analytics report...")
    # ここで初めてデータベース接続が発生する
    db.query("SELECT * FROM sales")
    print("Report finished.")


# この時点ではまだ接続しない(3秒待たされない)
>>> db_proxy = LazyDatabaseProxy()

# ユーザーの操作などで、必要になったタイミングで処理を開始
>>> run_analytics(db_proxy)
Preparing analytics report...
Connecting to the database... 
Connected to Database!
Executing SQL: SELECT * FROM sales
Report finished.

RealDatabaseクラスは初期化の時点でデータベースへの接続処理を行ってしまうため、インスタンスを作っただけで待ち時間が発生します。 仮にインスタンスを作った後で、一度もクエリを実行しなかった場合、その接続コストは完全に無駄になります。

LazyDatabaseProxyは、実際のquery()メソッドが呼ばれるその瞬間まで、本物のRealDatabaseに接続しません。

インターフェースが同じであるため、既存のビジネスロジックを一切変更することなく、 「必要なときだけ接続する」というパフォーマンス改善を導入することができます。

Built with Hugo
Theme Stack designed by Jimmy