nikkie-ftnextの日記

イベントレポートや読書メモを発信

読書ログ | #ちょうぜつ本 第8章 Proxyパターン 〜Python文法のデコレータは1つの例〜

はじめに

変更しやすいコードが書けないのにソフトウェア開発とか舐めているのですか

天使様1ごめんなさい〜、nikkieです。
2024年の干支は辰だと思っていましたが、天使様!という可能性に心が踊っています2

「かわいい」と技術書が夢の合体を果たした、ちょうぜつ本(『ちょうぜつソフトウェア設計入門』)!🤗
技術書界のきららという評もあります(アニメ企画来てくれ〜!)

昨年から読書会を共同主催しており、現在は第8章「デザインパターン」を読み進めています。
次回の範囲の予習から、Proxyを取り上げます。

目次

前回のちょうぜつ本!

前から順に読み進めて第8章に突入し、読書会では範囲を細かくしてじっくりと読み進めています。

Mediatorはむずかしかったですが、完全に理解しました。

Proxyパターン

呼び出し元と呼び出し先のオブジェクトの間に割って入ります。
呼び出し先オブジェクトをラップする(包む)わけです

ラッパーのパターンにはProxyだけではなく、他にDecoratorAdapterもあります。
ちょうぜつ本ではこれら3つを目的で区別して解説していました。
比較したことで分かりやすいと思ったのが以下です。

元のコードを変更せずに、同じ機能呼び出しの振る舞いを拡張するためのパターン (Kindle版 p.376)

利用者から見たメソッドの使い方を変えずに、特定の振る舞いを拡張するラッパー (Kindle版 p.380)

Proxyが同じ型に見せかけた別の振る舞いを生み出す (Kindle版 p.384)

引用2つ目と3つ目は例と合わせて納得しました。

Proxyの例

メールを送信するジョブの例です。

class MailerInterface(Protocol):
    def send(self, mail: Mail) -> None:
        ...


class JobWorker:
    def __init__(self, mailer: MailerInterface) -> None:
        self.mailer = mailer

    def process(self) -> None:
        report_mail = Mail()
        self.mailer.send(report_mail)


class RealMailer:
    def send(self, mail: Mail) -> None:
        print("メール送信")


worker = JobWorker(RealMailer())
worker.process()

ここでメール送信処理のログ出力を考えます。
Proxyパターンを使って、MailerInterfaceを実装し(=sendメソッドを持ち)、かつ、ログ出力3もするクラスを実現できます。

class LoggingInterface(Protocol):
    def info(self, message: str) -> None:
        ...


class LoggingMailerProxy:
    def __init__(
        self, target: MailerInterface, logger: LoggingInterface
    ) -> None:
        self.target = target
        self.logger = logger

    def send(self, mail: Mail) -> None:
        self.logger.info(f"Before send {mail.address}")
        self.target.send(mail)
        self.logger.info(f"After send {mail.address}")


class StdlibLogger:
    ...  # この記事では省略(GitHub側をどうぞ)


worker = JobWorker(LoggingMailerProxy(RealMailer(), StdlibLogger()))
worker.process()
  • LoggingMailerProxyMailerInterfaceを実装している
  • LoggingMailerProxysendメソッドはMailerInterfacesendメソッドの前後にログ出力処理を挟む
    • このMailerInterfaceRealMailerを想定
    • つまり、RealMailersendメソッドに(RealMailer自体は変えずに)ログ出力が追加された

ソースコード全体はこちらからどうぞ

Pythonはデコレータという文法でProxyパターンができる

できることを示すために雑に書いた実装です。
(本番コードに組み込むなら、loggingモジュールを使ったloggerの初期化を1回限りにするなど工夫したいです)

def enable_mail_logging(func):
    # logger初期化処理(省略)

    def wrapper(mailer, mail) -> None:
        logger.info(f"Before send {mail.address}")
        func(mailer, mail)
        logger.info(f"After send {mail.address}")

    return wrapper


class RealMailer:
    @enable_mail_logging  # これだけでログ出力が追加できた!🙌
    def send(self, mail: Mail) -> None:
        print("メール送信")

デコレータは関数を返す関数です4
func関数を受け取り、func関数の前後に処理を追加したwrapper関数を返します。

@の記法により、デコレータが返す関数(wrapper)はデコレータに渡された関数の名前(send)に再代入されます。

Proxyパターンの目的、RealMailerに手を入れずにログ出力を追加するというのは、Python文法のデコレータでも実現できました!
ちょうぜつ本のコードの方が小さいものが寄せ集まっている綺麗さを感じるので、ここで示したデコレータ実装は伸びしろがあります(この場合は、デコレータとして振る舞うクラスにしたい気持ち)

コードの全体はこちらのスクリプトをどうぞ

「マンガでわかる Proxy」より

ちょうぜつ本の元になったアドベントカレンダー

Proxy は Adapter や Decorator とは目的が異なり、かならずしもラップ対象に委譲するわけではない、というのがポイントです。

Proxyの利用シーンとして、負荷対策遅延評価が挙がっています。

Proxy の目的は「対外的な結果を変えずに、いかに本来の機能をサボるか」

ちょうぜつ本で完全に理解した!となりましたが、アドベントカレンダーを覗いたところ、負荷対策や遅延評価のようなサボる実装は独力ではまだできそうにないなと鼻柱を折られました。
実装例を探してみよう5

終わりに

ちょうぜつ本 8章よりProxyパターンでした。
Pythonの文法にあるデコレータを実装した経験から、私にはとっつきやすかったです。

おまけ:GPT-4に頼んで「まんがタイムきらら」風に表現したProxyデザインパターンのイラスト

P.S. 1/5(金) 第8章「デザインパターン」(8-6)のちょうぜつ本_読書py!

次回ちょうぜつ本_読書py(Python使い視点でちょうぜつ本を読む、みんなのアウトプット中心の読書会)は1/5(金)です!

常連さんも、お久しぶりの方も、はじめましての方もデザインパターンに興味ある方、大歓迎です!
ぜひぜひお気軽にお越しください〜


  1. ちょうぜつ本読書ログシリーズではおなじみのこちらの書き出し。元は『お隣の天使様にいつの間にか駄目人間にされていた件』の「家事ができないのに一人暮らしとか舐めているのですか」です
  2. 真昼だらけの一年」🤩
  3. easyな方法のルートロガーは採用せずに、子のロガーを用意してそれに追加したハンドラがどちらもinfo以上をロギングするように実装しています
  4. 用語集 https://docs.python.org/ja/3/glossary.html#term-decorator
  5. 単純にメモ化してキャッシュ」はPython文法のデコレータでやってみたことあるかも