第8章: Decorator パターン¶
はじめに¶
Decorator パターンは、既存のオブジェクトに新しい機能を動的に追加するパターンです。このパターンを使用すると、サブクラス化による継承よりも柔軟な方法で機能を拡張できます。
関数型プログラミングでは、高階関数を使って関数をラップし、横断的関心事(ログ、キャッシュ、認証など)を追加します。本章では、形状のジャーナリングと関数デコレータについて学びます。
1. パターンの構造¶
2. JournaledShape - 形状デコレータ¶
基本形状の定義¶
/// ジャーナルエントリ(操作の記録)
type JournalEntry =
| Translate of dx: float * dy: float
| Scale of factor: float
| Rotate of angle: float
/// 基本形状(デコレート対象)
[<RequireQualifiedAccess>]
type Shape =
| Circle of centerX: float * centerY: float * radius: float
| Square of topLeftX: float * topLeftY: float * side: float
| Rectangle of topLeftX: float * topLeftY: float * width: float * height: float
module Shape =
let translate dx dy shape =
match shape with
| Shape.Circle(cx, cy, r) -> Shape.Circle(cx + dx, cy + dy, r)
| Shape.Square(x, y, s) -> Shape.Square(x + dx, y + dy, s)
| Shape.Rectangle(x, y, w, h) -> Shape.Rectangle(x + dx, y + dy, w, h)
let scale factor shape =
match shape with
| Shape.Circle(cx, cy, r) -> Shape.Circle(cx, cy, r * factor)
| Shape.Square(x, y, s) -> Shape.Square(x, y, s * factor)
| Shape.Rectangle(x, y, w, h) -> Shape.Rectangle(x, y, w * factor, h * factor)
ジャーナル付き形状(デコレータ)¶
/// ジャーナル付き形状(デコレータ)
type JournaledShape =
{ Shape: Shape
Journal: JournalEntry list }
module JournaledShape =
let create shape =
{ Shape = shape; Journal = [] }
let translate dx dy js =
{ Shape = Shape.translate dx dy js.Shape
Journal = js.Journal @ [ Translate(dx, dy) ] }
let scale factor js =
{ Shape = Shape.scale factor js.Shape
Journal = js.Journal @ [ Scale factor ] }
let clearJournal js = { js with Journal = [] }
/// ジャーナルエントリを再生
let replay entries js =
entries
|> List.fold
(fun acc entry ->
match entry with
| Translate(dx, dy) -> translate dx dy acc
| Scale factor -> scale factor acc
| Rotate _ -> acc)
js
使用例¶
// 形状をデコレート
let circle = Shape.Circle(0.0, 0.0, 5.0)
let journaled =
JournaledShape.create circle
|> JournaledShape.translate 2.0 3.0
|> JournaledShape.scale 5.0
// ジャーナルを確認
JournaledShape.getJournal journaled
// => [Translate(2.0, 3.0); Scale(5.0)]
// 実際の形状を確認
JournaledShape.getShape journaled
// => Circle(2.0, 3.0, 25.0)
3. 関数デコレータ¶
関数型プログラミングでは、高階関数を使って関数をデコレートします。
ログ付きデコレータ¶
/// ログコレクター(副作用を追跡)
type LogCollector() =
let mutable logs: string list = []
member _.Add(msg: string) = logs <- logs @ [ msg ]
member _.GetLogs() = logs
/// ログ付きデコレータ
let withLogging (name: string) (collector: LogCollector) (f: 'a -> 'b) : 'a -> 'b =
fun a ->
collector.Add(sprintf "[%s] called with: %A" name a)
let result = f a
collector.Add(sprintf "[%s] returned: %A" name result)
result
// 使用例
let collector = LogCollector()
let add10 = fun x -> x + 10
let logged = withLogging "add10" collector add10
logged 5 // 15 (ログも記録される)
リトライデコレータ¶
/// リトライデコレータ
let withRetry (maxRetries: int) (f: 'a -> 'b) : 'a -> 'b =
fun a ->
let rec attempt remaining =
try
f a
with ex ->
if remaining > 0 then
attempt (remaining - 1)
else
raise ex
attempt maxRetries
// 使用例
let unreliableFn = withRetry 3 callExternalService
キャッシュデコレータ¶
/// キャッシュデコレータ
let withCache () : ('a -> 'b) -> ('a -> 'b) =
fun f ->
let cache = System.Collections.Generic.Dictionary<'a, 'b>()
fun a ->
if cache.ContainsKey(a) then
cache.[a]
else
let result = f a
cache.[a] <- result
result
// 使用例
let expensiveFn = withCache () (fun x -> x * x)
バリデーションデコレータ¶
/// バリデーションデコレータ
let withValidation (validator: 'a -> bool) (errorMsg: string) (f: 'a -> 'b) : 'a -> 'b =
fun a ->
if validator a then
f a
else
invalidArg "input" (sprintf "%s: %A" errorMsg a)
// 使用例
let positiveOnly = withValidation (fun x -> x > 0) "Must be positive" (fun x -> x * 2)
positiveOnly 5 // 10
positiveOnly -5 // Exception!
エラーハンドリングデコレータ¶
/// Option結果デコレータ(例外をOptionに変換)
let withOptionResult (f: 'a -> 'b) : 'a -> 'b option =
fun a ->
try
Some(f a)
with _ ->
None
/// Either結果デコレータ(例外をResultに変換)
let withEitherResult (f: 'a -> 'b) : 'a -> Result<'b, exn> =
fun a ->
try
Ok(f a)
with ex ->
Error ex
/// デフォルト値デコレータ
let withDefault (defaultValue: 'b) (f: 'a -> 'b) : 'a -> 'b =
fun a ->
try
f a
with _ ->
defaultValue
4. デコレータの合成¶
/// 複数のデコレータを合成
let composeDecorators (decorators: (('a -> 'b) -> ('a -> 'b)) list) (f: 'a -> 'b) : 'a -> 'b =
decorators |> List.fold (fun decorated decorator -> decorator decorated) f
// 使用例
let collector = LogCollector()
let double' = fun x -> x * 2
let decorators = [
withLogging "double" collector
withValidation (fun x -> x > 0) "Must be positive"
]
let composed = composeDecorators decorators double'
composed 5 // 10 (ログも記録される)
5. AuditedList - コレクションデコレータ¶
/// 操作履歴付きリスト
type AuditedList<'T> =
{ Items: 'T list
Operations: string list }
module AuditedList =
let empty<'T> : AuditedList<'T> = { Items = []; Operations = [] }
let add (item: 'T) (list: AuditedList<'T>) =
{ Items = list.Items @ [ item ]
Operations = list.Operations @ [ sprintf "add(%A)" item ] }
let remove (item: 'T) (list: AuditedList<'T>) =
{ Items = list.Items |> List.filter ((<>) item)
Operations = list.Operations @ [ sprintf "remove(%A)" item ] }
let map (f: 'T -> 'U) (list: AuditedList<'T>) : AuditedList<'U> =
{ Items = List.map f list.Items
Operations = list.Operations @ [ "map" ] }
let filter (predicate: 'T -> bool) (list: AuditedList<'T>) =
{ Items = List.filter predicate list.Items
Operations = list.Operations @ [ "filter" ] }
// 使用例
let list =
AuditedList.empty
|> AuditedList.add 1
|> AuditedList.add 2
|> AuditedList.add 3
|> AuditedList.filter (fun x -> x > 1)
list.Operations // ["add(1)"; "add(2)"; "add(3)"; "filter"]
6. HTTPクライアントデコレータ¶
/// HTTPレスポンス
type HttpResponse = { StatusCode: int; Body: string }
/// HTTPクライアントインターフェース
type IHttpClient =
abstract member Get: string -> HttpResponse
/// シンプルなHTTPクライアント
type SimpleHttpClient() =
interface IHttpClient with
member _.Get(url: string) =
{ StatusCode = 200
Body = sprintf "Response from %s" url }
/// ログ付きHTTPクライアント(デコレータ)
type LoggingHttpClient(client: IHttpClient, collector: LogCollector) =
interface IHttpClient with
member _.Get(url: string) =
collector.Add(sprintf "[HTTP] GET %s" url)
let response = client.Get(url)
collector.Add(sprintf "[HTTP] Response: %d" response.StatusCode)
response
/// キャッシュ付きHTTPクライアント(デコレータ)
type CachingHttpClient(client: IHttpClient) =
let cache = System.Collections.Generic.Dictionary<string, HttpResponse>()
interface IHttpClient with
member _.Get(url: string) =
if cache.ContainsKey(url) then
cache.[url]
else
let response = client.Get(url)
cache.[url] <- response
response
// デコレータの組み合わせ
let collector = LogCollector()
let client =
SimpleHttpClient() :> IHttpClient
|> fun c -> CachingHttpClient(c) :> IHttpClient
|> fun c -> LoggingHttpClient(c, collector) :> IHttpClient
7. FunctionBuilder - ビルダースタイル¶
/// 関数ビルダー
type FunctionBuilder<'a, 'b>(f: 'a -> 'b, collector: LogCollector) =
let mutable current = f
member _.WithLogging(name: string) =
current <- withLogging name collector current
FunctionBuilder(current, collector)
member _.WithValidation(validator: 'a -> bool, errorMsg: string) =
current <- withValidation validator errorMsg current
FunctionBuilder(current, collector)
member _.WithRetry(maxRetries: int) =
current <- withRetry maxRetries current
FunctionBuilder(current, collector)
member _.Build() = current
// 使用例
let collector = LogCollector()
let fn =
FunctionBuilder.create collector (fun x -> x * 2)
|> fun b -> b.WithLogging("double")
|> fun b -> b.WithValidation((fun x -> x > 0), "Must be positive")
|> fun b -> b.Build()
8. TransactionalOperation - トランザクションデコレータ¶
/// トランザクション状態
type TransactionState =
| NotStarted
| InProgress
| Committed
| RolledBack
/// トランザクション付き操作
type TransactionalOperation<'T> =
{ Operations: ('T -> 'T) list
State: TransactionState
OriginalValue: 'T option
CurrentValue: 'T option }
module TransactionalOperation =
let create () =
{ Operations = []; State = NotStarted; OriginalValue = None; CurrentValue = None }
let begin' (value: 'T) (tx: TransactionalOperation<'T>) =
{ tx with State = InProgress; OriginalValue = Some value; CurrentValue = Some value }
let addOperation (op: 'T -> 'T) (tx: TransactionalOperation<'T>) =
match tx.State with
| InProgress ->
let newValue = tx.CurrentValue |> Option.map op
{ tx with Operations = tx.Operations @ [ op ]; CurrentValue = newValue }
| _ -> tx
let commit (tx: TransactionalOperation<'T>) =
match tx.State with
| InProgress -> { tx with State = Committed }
| _ -> tx
let rollback (tx: TransactionalOperation<'T>) =
match tx.State with
| InProgress ->
{ tx with State = RolledBack; CurrentValue = tx.OriginalValue; Operations = [] }
| _ -> tx
パターンの利点¶
- 単一責任の原則: 各デコレータは一つの機能のみを追加
- 開放/閉鎖の原則: 既存コードを変更せずに機能を追加
- 柔軟な組み合わせ: 必要な機能だけを選択して組み合わせ可能
- 実行時の決定: どのデコレータを適用するか実行時に決定可能
F# での特徴¶
高階関数によるデコレータ¶
// 関数を受け取り、関数を返す
let withLogging name collector f =
fun a ->
collector.Add(sprintf "called with: %A" a)
f a
パイプラインでデコレータを適用¶
let decorated =
originalFn
|> withCache ()
|> withLogging "fn" collector
|> withRetry 3
不変性を維持したデコレータ¶
// 元のデータは変更されない
let original = JournaledShape.create circle
let decorated = original |> JournaledShape.translate 2.0 3.0
// original は変更されていない
Clojure/Scala との比較¶
| 概念 | Clojure | Scala | F# |
|---|---|---|---|
| 高階関数 | (defn with-logging [f] ...) |
def withLogging[A,B](f: A => B) |
let withLogging f = fun a -> ... |
| クロージャ | (let [cache (atom {})] ...) |
val cache = mutable.Map.empty |
let cache = Dictionary() |
| 合成 | (comp decorator1 decorator2) |
compose(f, decorators) |
composeDecorators decorators f |
| 状態 | atom |
var or mutable |
mutable (explicit) |
| ビルダー | なし(関数合成) | class with fluent API | F# class with methods |
まとめ¶
本章では、Decorator パターンについて学びました:
- JournaledShape: 形状操作の履歴を記録するデコレータ
- 関数デコレータ: ログ、リトライ、キャッシュなどの横断的関心事
- AuditedList: 操作履歴付きコレクション
- HTTPクライアント: インターフェースベースのデコレータ
- FunctionBuilder: ビルダースタイルのデコレータ適用
- TransactionalOperation: トランザクション機能
Decorator パターンは、既存のコードを変更せずに機能を拡張する強力なパターンです。F# では高階関数を使って関数レベルでデコレータを実装でき、型安全で柔軟な設計が可能です。
参考コード¶
本章のコード例は以下のファイルで確認できます:
- ソースコード:
apps/fsharp/part3/src/Library.fs - テストコード:
apps/fsharp/part3/tests/Tests.fs
次章予告¶
次章では、Adapter パターンについて学びます。異なるインターフェース間の変換とレガシーシステムとの統合を探ります。