「後から機能を足したい。でも継承でクラスが増えすぎて管理が大変…。」そんな悩みはありませんか?
デコレータパターンは、まさにその問題を解決するための構造的デザインパターンです。元のオブジェクトを“包む(ラップする)”オブジェクトを用意し、実行時に新しい振る舞いを重ねていけるのが最大の特徴です。コーヒーにミルクやシロップを後からトッピングしていくイメージを思い浮かべると、理解しやすいでしょう。
ここでは、Python初心者の方でも迷わず読めるように、実用的なサンプルとあわせて丁寧に解説します。「@」を使うPythonの関数デコレータと混同しやすいポイントについても触れます。
まず前提として、デコレータパターンは継承を使わずに機能を拡張するための設計手法です。拡張したい対象(コンポーネント)と同じインターフェース(同じメソッド名)を持つ“包み役(デコレータ)”を作り、必要な分だけ重ねて使います。すると、元のクラスを変更せずに、しかも実行時に柔軟に振る舞いを追加できます。
ここでは、よくある場面をイメージしやすいように、利用シーンを簡単に整理します。
少し紛らわしいのですが、Pythonには関数やメソッドに使う「@decorator」という文法があります。今回学ぶデコレータパターンは、オブジェクトを包む“設計パターン”で、クラス同士の組み合わせで表現します。名前は似ていますが、用途と実装の視点が少し違います。とはいえ本質は同じで、元のものに手を入れず、外側から振る舞いを追加するという考え方は共通です。
まずは、とてもシンプルな「テキストを返すクラス」から始めます。その上に、機能を足すデコレータを重ねていき、最終的に見た目が変わる様子を確認しましょう。
class Text:
"""素のテキストを保持するコンポーネント"""
def __init__(self, content: str):
self.content = content
def get_content(self) -> str:
return self.content
このTextクラスは、文字列を受け取って、そのまま返すだけのシンプルな役者です。デコレータパターンでは「共通のメソッド名」を揃えることが大切なので、ここではget_contentという名前に統一します。以降に登場するデコレータも、必ずget_contentを実装し、同じ振る舞いで呼び出せるようにします。
続いて、テキストを大文字にするデコレータを作ります。
#上記コードの続き
class UppercaseDecorator:
"""渡されたコンポーネントのテキストを大文字にするデコレータ"""
def __init__(self, text):
self.text = text # Text でも、別のデコレータでもOK
def get_content(self) -> str:
original = self.text.get_content()
return original.upper()
このUppercaseDecoratorは、Textと同じget_contentメソッドを持ちます。中では、包んでいるオブジェクトのget_contentを呼んで元のテキストを取り出し、その結果だけを大文字に変換して返しています。元のTextクラスを一切変更していないのに、外側から新しい振る舞い(大文字化)を追加できている点がポイントです。
次に、テキストの先頭にプレフィックス(固定の文字列)を付けるデコレータを用意します。
#上記コードの続き
class PrefixDecorator:
"""テキストの先頭にプレフィックスを付けるデコレータ"""
def __init__(self, text, prefix: str):
self.text = text
self.prefix = prefix
def get_content(self) -> str:
return self.prefix + self.text.get_content()
PrefixDecoratorもやはり同じ形式で、元のテキストを取得してから、先頭にprefixを足して返しています。これらのデコレータは互いに独立しており、必要なときに好きな順番で重ねられるのが利点です。
ここまで用意できたら、いよいよ“重ねがけ”してみましょう。順番を変えると結果も変わるので、そこにも注目してください。
#上記コードの続き
# 基本のテキスト
simple_text = Text("hello world")
# 大文字化してから、プレフィックスを付ける
uppercase_text = UppercaseDecorator(simple_text)
decorated_text = PrefixDecorator(uppercase_text, "Greeting: ")
print(decorated_text.get_content())
# => Greeting: HELLO WORLD
このコードでは、まず元のテキストを大文字にし、その結果に対して「Greeting: 」を前置しています。デコレータは“同じインターフェース(get_content)”を持つため、入れ子にしても書き方が変わらず読みやすいのが嬉しいところです。
試しに順番を入れ替えると、少し印象が変わります。
#上記コードの続き
# 先にプレフィックスを付け、その後で大文字化する
prefixed_first = PrefixDecorator(Text("hello world"), "Greeting: ")
decorated_text_2 = UppercaseDecorator(prefixed_first)
print(decorated_text_2.get_content())
# => GREETING: HELLO WORLD
今度はプレフィックスも含めて大文字化されました。順番を自由に変えられるのは強力ですが、意図した結果になるよう順序を設計することが大切です。「なぜ違う結果になったんだろう?」と立ち止まって考えることで、デコレータの動作がよく理解できます。
ここまでの流れを踏まえ、メリットをあらためて確認しておきます。読み進めやすいよう、前提を押さえてから箇条書きにします。
デコレータパターンは「元のクラスはそのまま」「必要な機能は外から足す」という、現場でとても役立つ設計手法です。
Pythonでは、同じメソッド名(ここではget_content)を共有する小さなデコレータを積み重ねることで、実行時に柔軟に振る舞いを合成できます。継承の数を増やさず、単一責任を保ちつつ、テストもしやすい。そんな“扱いやすい拡張”を、ぜひ小さな場面から試してみてください。
次のステップとして、ログ付与やキャッシュといった「横断的な関心事」をデコレータで表現してみるのもおすすめです。「この機能、後から足せたら便利だな?」と思ったとき、デコレータパターンが活躍する合図です。