第9章: Adapter パターン¶
はじめに¶
Adapter パターンは、既存のクラスのインターフェースを、クライアントが期待する別のインターフェースに変換するパターンです。このパターンを使用すると、互換性のないインターフェースを持つクラス同士を連携させることができます。
本章では、可変強度ライトのアダプターとデータフォーマット変換について学びます。
1. パターンの構造¶
Adapter パターンは以下の要素で構成されます:
- Target: クライアントが期待するインターフェース
- Adaptee: 既存のクラス(適応される側)
- Adapter: Target インターフェースを実装し、Adaptee を使用
2. VariableLight の例¶
Switchable インターフェース (Target)¶
/// Switchableインターフェース(Target)
type Switchable =
abstract member TurnOn: unit -> unit
abstract member TurnOff: unit -> unit
VariableLight (Adaptee)¶
可変強度を持つライトは、単純なオン/オフではなく、強度を指定する必要があります。
/// 可変強度ライト(Adaptee)
type VariableLight() =
member this.SetIntensity(intensity: int) =
printfn "VariableLight: intensity set to %d" intensity
intensity
VariableLightAdapter (Adapter)¶
/// VariableLightをSwitchableに適応させるアダプター
type VariableLightAdapter(light: VariableLight, minIntensity: int, maxIntensity: int) =
interface Switchable with
member this.TurnOn() =
light.SetIntensity(maxIntensity) |> ignore
member this.TurnOff() =
light.SetIntensity(minIntensity) |> ignore
関数型アダプター¶
F# では、関数を使ったアダプターも自然に実装できます:
module VariableLightAdapter =
/// VariableLight を Switchable 風の関数に適応
let adapt (minIntensity: int) (maxIntensity: int) (light: VariableLight) =
let turnOn () = light.SetIntensity(maxIntensity) |> ignore
let turnOff () = light.SetIntensity(minIntensity) |> ignore
(turnOn, turnOff)
使用例¶
// アダプターを作成(最小0、最大100)
let light = VariableLight()
let adapter = VariableLightAdapter(light, 0, 100) :> Switchable
// Switchableインターフェースで操作
adapter.TurnOn() // VariableLight: intensity set to 100
adapter.TurnOff() // VariableLight: intensity set to 0
// 関数型アダプターの場合
let (turnOn, turnOff) = VariableLightAdapter.adapt 0 100 light
turnOn() // VariableLight: intensity set to 100
turnOff() // VariableLight: intensity set to 0
3. データフォーマットアダプター¶
ユーザーフォーマット変換¶
異なるシステム間でデータフォーマットが異なる場合、アダプターを使って変換します。
/// 旧ユーザーフォーマット
type OldUserFormat =
{ FirstName: string
LastName: string
EmailAddress: string
PhoneNumber: string }
/// 新ユーザーフォーマット
type NewUserFormat =
{ Name: string
Email: string
Phone: string
Metadata: Map<string, obj> }
module UserFormatAdapter =
/// 旧フォーマットを新フォーマットに変換
let oldToNew (oldUser: OldUserFormat) : NewUserFormat =
{ Name = sprintf "%s %s" oldUser.LastName oldUser.FirstName
Email = oldUser.EmailAddress
Phone = oldUser.PhoneNumber
Metadata = Map.ofList [ ("migrated", box true); ("originalFormat", box "old") ] }
/// 新フォーマットを旧フォーマットに変換
let newToOld (newUser: NewUserFormat) : OldUserFormat =
let nameParts = newUser.Name.Split(' ')
let lastName = if nameParts.Length > 0 then nameParts.[0] else ""
let firstName = if nameParts.Length > 1 then nameParts.[1] else ""
{ FirstName = firstName
LastName = lastName
EmailAddress = newUser.Email
PhoneNumber = newUser.Phone }
使用例¶
// 旧フォーマット → 新フォーマット
let oldUser =
{ FirstName = "太郎"
LastName = "山田"
EmailAddress = "taro@example.com"
PhoneNumber = "090-1234-5678" }
let newUser = UserFormatAdapter.oldToNew oldUser
// { Name = "山田 太郎"
// Email = "taro@example.com"
// Phone = "090-1234-5678"
// Metadata = ... }
// 新フォーマット → 旧フォーマット
let converted = UserFormatAdapter.newToOld newUser
// { FirstName = "太郎"
// LastName = "山田"
// EmailAddress = "taro@example.com"
// PhoneNumber = "090-1234-5678" }
4. 温度変換アダプター¶
/// 温度の単位
[<RequireQualifiedAccess>]
type TemperatureUnit =
| Celsius
| Fahrenheit
| Kelvin
type Temperature = { Value: float; Unit: TemperatureUnit }
module TemperatureAdapter =
/// 華氏を摂氏に変換
let fahrenheitToCelsius (temp: Temperature) : Temperature =
match temp.Unit with
| TemperatureUnit.Fahrenheit ->
{ Value = (temp.Value - 32.0) * 5.0 / 9.0
Unit = TemperatureUnit.Celsius }
| _ -> temp
/// 摂氏を華氏に変換
let celsiusToFahrenheit (temp: Temperature) : Temperature =
match temp.Unit with
| TemperatureUnit.Celsius ->
{ Value = temp.Value * 9.0 / 5.0 + 32.0
Unit = TemperatureUnit.Fahrenheit }
| _ -> temp
/// ケルビンを摂氏に変換
let kelvinToCelsius (temp: Temperature) : Temperature =
match temp.Unit with
| TemperatureUnit.Kelvin ->
{ Value = temp.Value - 273.15
Unit = TemperatureUnit.Celsius }
| _ -> temp
5. 日付・時刻変換アダプター¶
module DateTimeAdapter =
/// Unix タイムスタンプを DateTime に変換
let unixToDateTime (timestamp: int64) : System.DateTime =
System.DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime
/// DateTime を Unix タイムスタンプに変換
let dateTimeToUnix (dt: System.DateTime) : int64 =
System.DateTimeOffset(dt).ToUnixTimeSeconds()
/// ISO8601 文字列を DateTime に変換
let iso8601ToDateTime (iso: string) : System.DateTime option =
match System.DateTime.TryParse(iso) with
| true, dt -> Some dt
| false, _ -> None
/// DateTime を ISO8601 文字列に変換
let dateTimeToIso8601 (dt: System.DateTime) : string =
dt.ToString("o")
6. 通貨変換アダプター¶
[<RequireQualifiedAccess>]
type Currency =
| USD of decimal
| JPY of decimal
| EUR of decimal
| GBP of decimal
module CurrencyAdapter =
let private rates =
Map.ofList
[ ("USD_JPY", 150.0m)
("EUR_USD", 1.08m)
("GBP_USD", 1.27m) ]
/// USD を JPY に変換
let usdToJpy (currency: Currency) : Currency =
match currency with
| Currency.USD amount ->
Currency.JPY(amount * rates.["USD_JPY"])
| _ -> currency
/// JPY を USD に変換
let jpyToUsd (currency: Currency) : Currency =
match currency with
| Currency.JPY amount ->
Currency.USD(amount / rates.["USD_JPY"])
| _ -> currency
7. ファイルパス変換アダプター¶
module FilePathAdapter =
/// Windows パスを Unix パスに変換
let windowsToUnix (path: string) : string =
path.Replace("\\", "/")
.Replace("C:", "/c")
.Replace("D:", "/d")
/// Unix パスを Windows パスに変換
let unixToWindows (path: string) : string =
let result = path.Replace("/", "\\")
if result.StartsWith("\\c\\") then
"C:" + result.Substring(2)
elif result.StartsWith("\\d\\") then
"D:" + result.Substring(2)
else
result
8. ログレベル変換アダプター¶
/// アプリケーションのログレベル
[<RequireQualifiedAccess>]
type AppLogLevel =
| Trace
| Debug
| Info
| Warning
| Error
| Fatal
/// 外部ライブラリのログレベル
type ExternalLogLevel =
| Verbose = 0
| Debug = 1
| Information = 2
| Warning = 3
| Error = 4
| Critical = 5
module LogLevelAdapter =
/// アプリのログレベルを外部形式に変換
let appToExternal level =
match level with
| AppLogLevel.Trace -> ExternalLogLevel.Verbose
| AppLogLevel.Debug -> ExternalLogLevel.Debug
| AppLogLevel.Info -> ExternalLogLevel.Information
| AppLogLevel.Warning -> ExternalLogLevel.Warning
| AppLogLevel.Error -> ExternalLogLevel.Error
| AppLogLevel.Fatal -> ExternalLogLevel.Critical
/// 外部形式をアプリのログレベルに変換
let externalToApp level =
match level with
| ExternalLogLevel.Verbose -> AppLogLevel.Trace
| ExternalLogLevel.Debug -> AppLogLevel.Debug
| ExternalLogLevel.Information -> AppLogLevel.Info
| ExternalLogLevel.Warning -> AppLogLevel.Warning
| ExternalLogLevel.Error -> AppLogLevel.Error
| ExternalLogLevel.Critical -> AppLogLevel.Fatal
| _ -> AppLogLevel.Info
9. コレクション変換アダプター¶
module CollectionAdapter =
/// Array を List に変換
let arrayToList (arr: 'T array) = Array.toList arr
/// List を Array に変換
let listToArray (lst: 'T list) = List.toArray lst
/// Seq を List に変換
let seqToList (seq: 'T seq) = Seq.toList seq
/// List を Seq に変換
let listToSeq (lst: 'T list) = List.toSeq lst
/// Dictionary を Map に変換
let dictToMap (dict: System.Collections.Generic.Dictionary<'K, 'V>) =
dict |> Seq.map (fun kvp -> (kvp.Key, kvp.Value)) |> Map.ofSeq
/// Map を Dictionary に変換
let mapToDict (map: Map<'K, 'V>) =
let dict = System.Collections.Generic.Dictionary<'K, 'V>()
map |> Map.iter (fun k v -> dict.[k] <- v)
dict
10. Result 型変換アダプター¶
F# の Result 型と他の形式間の変換:
module ResultAdapter =
/// Option を Result に変換
let optionToResult (error: 'E) (opt: 'T option) : Result<'T, 'E> =
match opt with
| Some v -> Ok v
| None -> Error error
/// Result を Option に変換
let resultToOption (result: Result<'T, 'E>) : 'T option =
match result with
| Ok v -> Some v
| Error _ -> None
/// 例外ベースの処理を Result に変換
let tryToResult (f: unit -> 'T) : Result<'T, exn> =
try
Ok(f ())
with ex ->
Error ex
/// Result を例外ベースに変換(例外をスロー)
let resultToTry (result: Result<'T, exn>) : 'T =
match result with
| Ok v -> v
| Error ex -> raise ex
使用例¶
// Option → Result
let maybeValue: int option = Some 42
let result = ResultAdapter.optionToResult "値が見つかりません" maybeValue
// Ok 42
let nothing: int option = None
let errorResult = ResultAdapter.optionToResult "値が見つかりません" nothing
// Error "値が見つかりません"
// 例外ベース → Result
let safeDiv =
ResultAdapter.tryToResult (fun () -> 10 / 2)
// Ok 5
let failedDiv =
ResultAdapter.tryToResult (fun () -> 10 / 0)
// Error (System.DivideByZeroException)
11. パターンの利点¶
- 既存コードの再利用: 既存のクラスを変更せずに新しいインターフェースで使用
- 単一責任の原則: インターフェース変換のロジックを分離
- 開放/閉鎖の原則: 新しいアダプターを追加しても既存コードは変更不要
- レガシーシステムとの統合: 古いシステムと新しいシステムを連携
12. 関数型プログラミングでの特徴¶
F# での Adapter パターンの実装には以下の特徴があります:
- 関数による変換: 単純なデータ変換は関数で直接実装
- パイプライン演算子: 変換を連鎖させやすい
- イミュータブルなデータ: 変換後も元のデータは変更されない
- 型安全: コンパイル時に型の不整合を検出
- 合成のしやすさ: 複数のアダプターを
>>で合成可能
// アダプターの合成例
let convertAndFormat =
TemperatureAdapter.celsiusToFahrenheit
>> fun temp -> sprintf "%.1f°F" temp.Value
let result = { Value = 100.0; Unit = TemperatureUnit.Celsius } |> convertAndFormat
// "212.0°F"
まとめ¶
本章では、Adapter パターンについて学びました:
- VariableLightAdapter: 可変強度ライトを単純なオン/オフインターフェースに適応
- データフォーマットアダプター: 異なるデータ形式間の変換
- 型変換アダプター: 温度、通貨、日時などの単位変換
- コレクションアダプター: 異なるコレクション型間の変換
- Result アダプター: エラーハンドリング方式間の変換
F# では、関数とパイプライン演算子を活用することで、シンプルで合成しやすいアダプターを実装できます。
参考コード¶
本章のコード例は以下のファイルで確認できます:
- ソースコード:
apps/fsharp/part3/src/Library.fs - テストコード:
apps/fsharp/part3/tests/Tests.fs
次章予告¶
次章では、Strategy パターンについて学びます。アルゴリズムをカプセル化し、実行時に切り替え可能にする方法を探ります。