ファサード/Swift デザインパターン学習[Facade]

GoFのデザインパターンのFacadeをSwiftで記述した記事です。
デザインパターンは、オブジェクト指向言語における設計のベストプラクティス集にあたるもので、
元々、C++やJavaで説明された書籍が有名です。
他の言語でもデザインパターンの考え方を取り入れることで、よりよい設計を行うことができます。

個人的には次の点から、デザインパターンを学習する価値は十分にあると思っています。

  • 特定のプログラミング言語に依存するものではない = 言語に関係なく応用が効く
  • 言語やフレームワークの流行りとは関係ないソフトウェア設計の考え方であること
  • 20年以上前に出された書籍にも関わらず、今も開発現場で活用されており知識が陳腐化していない

Swiftデザインパターン

Facadeパターンの概要

Facadeパターンが有効なのは、アプリケーションが以下の条件にあてはまる場合です。

  • 複数のクラスのAPIを特定の順番で実行するような複雑な手続きがある
  • 上記の複雑な手続きをアプリケーションの複数箇所で呼び出す必要がある

Facadeという馴染みのない言葉が出てくるととまどってしまいますが、普段、プログラミングする中で自然と似たようなことをやっていたりします。

facadeの意味

facadeのイメージ

facadeを英和辞書で調べると「(建物の)正面」、「見かけ、外見」という意味が出てきます。
よくわかりませんね。
Oxfordの英英辞典で調べてみました。

The principal front of a building, that faces on to a street or open space

"通りやオープンスペースに面している建物の面"のことらしいです。
ソフトウェアデザインの文脈で意訳すると、外部に公開されている面、窓口といったところでしょう。

Facadeパターンの主なメリット

  • クライアントは本来複数の相手とやりとりする必要があるが、Facadeを通すことでやりとりする相手はFacadeだけでよくなる
  • Facadeが窓口になることで、裏側の詳細な処理や順番をクライアントは知らなくてもよくなる
  • 一連の処理をFacadeに押し込めることができるためメンテナンスしやすくなる

Facadeパターンのサンプル

現実の世界でも、Facadeパターンと似たようなことはあります。(というよりは、現実世界の考えをソフトウェアの世界に応用しているのですが。)

例えば、旅行に出かけるときに飛行機や宿泊先を自分で予約してもいいですが、旅行代理店を通して予約をすることができます。

お客は旅行代理店に人数と日程、予算を伝えると、旅行代理店が飛行機のスケジュール、ホテルの空き状況などを確認してプランを提示してくれます。
お客は航空会社、ホテルなどの連絡先や手続き方法を知らなくても、予約をとることができます。
このように代理店を通すことで、お客は窓口を一本化することができるというメリットがあります。

Facadeパターンを実装してみる

上記の旅行の予約手続きをSwiftのコードで表現してみます。
まず、代理店(Facade)を使わずに、お客(クライアント)が自分で旅行プランを作る場合のコードです。

お客(クライアント)が直接、航空会社、ホテルの空き状況を調べてプランを作る

①フライトスケジュールの検索を行うクラス

// フライトスケジュールの構造体
struct FlightSchedule {
    var start: NSDate
    var end: NSDate
    var price: Double
}

// フライトスケジュール検索のプロトコル
protocol AirlineBooking {
    func searchFlight(start: NSDate, end: NSDate, passengers: Int) -> FlightSchedule?
}

// 航空会社A社のクラス
class AirlineA: AirlineBooking {
    func searchFlight(start: NSDate, end: NSDate, passengers: Int) -> FlightSchedule? {
      // do something
    }
}

// 航空会社B社のクラス
class AirlineB: AirlineBooking {
    func searchFlight(start: NSDate, end: NSDate, passengers: Int) -> FlightSchedule? {
      // do something  
    }
}

// 航空会社C社のクラス
class AirlineC: AirlineBooking {
    func searchFlight(start: NSDate, end: NSDate, passengers: Int) -> FlightSchedule? {
      // do something
    }
}

②ホテルの検索を行うクラス

// ホテルの空き部屋を表す構造体
struct AvailableRoom {
    var hotel: String
    var price: Double
}

// ホテルの空き部屋を検索するサービス
class HotelBookingService {
    class func searchAvailableRooms(start: NSDate, end: NSDate) -> [AvailableRoom] {
      // do something
    }
}

③クライアントコード(旅行プランを作成する)
こちらがメインロジックになります。

// お客(クライアント)のコード
let start = NSDate(timeIntervalSinceNow: 24 * 60 * 60)
let end = NSDate(timeIntervalSinceNow: 24 * 60 * 60 * 5)
let budget: Double = 40000
let passengerNumber = 2

let airlines: [AirlineBooking] = [AirlineA(), AirlineB(), AirlineC()]
let fSchedules = airlines.map {
   $0.searchFlight(start, end: end, passengers: passengerNumber)
  }.filter { $0 != nil }

if fSchedules.count > 0 {
    let rooms = HotelBookingService.searchAvailableRooms(start, end: end, capacity: passengers)

    let plans = fSchedules.flatMap { f in rooms.map { r in TravelPlan(f, r) } }
    let orderedPlans = plans.filter { $0.totalPrice < budget }.sort { $0.totalPrice < $1.totalPrice }
}
  • 最後のクライアントコードを見ると、以下のような問題点があります。
    • フライトに空きが1つもなかった場合、ホテルの検索を行わないことをクライアントが知っている必要がある。
    • TravelPlanオブジェクトの作り方/使い方をクライアントが知っている必要がある
    • 航空会社(Airline)が増えたときにクライアントコードを修正する必要がある
    • クライアントコードが、ホテル検索クラス、フライトスケジュール検索クラス、旅行プランクラスに依存する
    • アプリケーションの他の箇所でも旅行プランを作る必要がある場合に再利用できない

お客(クライアント)が代理店(Facade)に旅行プランの作成を依頼する

③'-1 Facadeクラス(旅行プランを作成する)

// Facadeクラス
class TravelPlanFacade {
  private let airlines: [AirlineBooking] = [AirlineA(), AirlineB(), AirlineC()]

  func createPlans(start: NSDate, end: NSDate, passengers: Int, budget: Double) -> [TravelPlan]? {
    let fSchedules = airlines.map {
       $0.searchFlight(start, end: end, passengers: passengers)
      }.filter { $0 != nil }

    guard fSchedules.count > 0 else { return nil }
    let rooms = HotelBookingService.searchAvailableRooms(start, end: end, capacity: passengers)

    let plans = fSchedules.flatMap { f in rooms.map { r in TravelPlan(f, r) } }
    let orderedPlans = plans.filter { $0.totalPrice < budget }.sort { $0.totalPrice < $1.totalPrice }
    return orderedPlans
  }
}

③'-2 クライアントコード

// Facadeクラス
let start = NSDate(timeIntervalSinceNow: 24 * 60 * 60)
let end = NSDate(timeIntervalSinceNow: 24 * 60 * 60 * 5)
let budget: Double = 40000
let passengerNumber = 2

let facade = TravelPlanFacade()
let travelPlans = facade.createPlans(start, end: end, passengers: passengerNumber, budget: budget)

Facadeパターンを利用したクライアントコードを見ると、Facadeクラスに予約作成を依頼しているだけで、予約処理の詳細には何も関与しなくなったのがわかります。これにより、フライト検索、ホテル検索、旅行プランクラスのAPIに変更があった場合でも、TravelPlanFacadeで変更を吸収することでクライアントコードへの影響を最小限にすることができます。

まとめ

Facadeパターンの特長をまとめます。

  • クライアントから見ると、Facadeが提供するAPIに一本化することができる(複雑な処理をFacadeで隠蔽)
  • Facadeに複数のクラスとのやりとり、複雑なロジックを押し込めることができるので、APIの変更などをFacadeで吸収することができる
  • クライアントはFacade以外のクラスとの依存関係を減らすことができる