読者です 読者をやめる 読者になる 読者になる

Swiftでデザインパターン(同時上映:クロージャもつかうよ!)

こんにちは。長期出張でへとへとになってしまったたーせるです。おひさしぶりーふ


6月3日の未明、Appleが「Swift」という新しいプログラミング言語を発表しました。

スローガンはObjective-C without C。ぼくはわくわくが止まりません。

さっそく Xcode 6 beta をダウンロードして、新生言語 Swift をいじってみました。

人生初 Swift です。

Hello World

まずはお決まりの儀式*1から。

main.swift

import Foundation

println("Hello, World!")

main()関数もセミコロンもいらないあたりがチャームポイントですな。


勢い優先で Xcode 6 をインストールしたものの、取り立てて作りたいものを思いつかなかったので(おい)、ちょいと言語に慣れるためにいくつかデザインパターンを実装してみました。

参考にした本は「Rubyによるデザインパターン」です。Twitter のタイムラインで「 Swift は Ruby に似ている」ってつぶやいている人もいましたし。

TemplateMethodパターン

おなじみ、TemplateMethodパターンです。

TemplateMethodパターンの特徴は、手続きの順序だけを基底クラスで規定して(←シャレ)実際のアルゴリズムは派生クラスに記述することです*2

このような設計を行う理由は、「不変なもの」と「可変なもの」をプログラム上で分離するためです。極端な話、TemplateMethodは、手続きの順序だけは何があっても変わらない場合に有効です。

お題

ある月次報告を、HTML と プレーンテキスト形式で出力する「レポートジェネレータ」を作ることを想定してみましょう。まぁこの題材は「Rubyによるデザインパターン」の パクr 参考なのですが。

HTMLバージョン
<html>
  <head>
  <title>月次報告</title>
  </head>
  <body>
  <p>順調</p>
  <p>最高の調子</p>
  </body>
</html>
プレーンテキストバージョン
**** 月次報告 ****
順調
最高の調子

どちらも、タイトルを出力して、本文を出力して……といった具合に、手続きが固定化されています。これは(ちょっと強引だが) TemplateMethod の予感!!

まずは、固定化された手続きを基底クラス「Report」に記述します。

Reportクラス

class Report {
    var title : String
    var text : String[]
    
    // コンストラクタみたいなやつです
    init() {
        title = "月次報告"
        text = ["順調", "最高の調子"]
    }
    
    func outputReport() {
        outputStart()
        outputHead()
        outputBodyStart()
        outputBody()
        outputBodyEnd()
        outputEnd()
    }
    
    func outputBody() {
        for line in text {
            outputLine(line)
        }
    }
    
    func outputStart() { }
    func outputHead() { }
    func outputBodyStart() { }
    func outputLine(line: String) { }
    func outputBodyEnd() { }    
    func outputEnd() { }
}

outputReport()の中に、たくさんのメソッド呼び出しが順序立てて書かれています。それぞれのメソッドはだいたいからっぽです。

まるで処理の順序をテンプレ化しているように見えます。これがTemplateMethodと呼ばれるゆえん。

次に、Reportクラスを継承したHTMLReportクラスを書きましょう。派生クラスでは、規定クラスに書いた各種手続きメソッドをオーバーライドして、具体的な処理を実装していきます。

HTMLReportクラス

class HTMLReport : Report {
    override func outputStart() {
        println("<html>")
    }
    
    override func outputHead() {
        println("  <head>")
        println("  <title>\(title)</title>")
        println("  </head>")
    }
    
    override func outputBodyStart() {
        println("  <body>")
    }
    
    override func outputLine(line : String) {
        println("  <p>\(line)</p>")
    }
    
    override func outputBodyEnd() {
        println("  </body>")
    }
    
    override func outputEnd() {
        println("</html>")
    }
}

そして、プレーンテキストバージョンも同様に基底クラスをオーバーライドします。

PlainTextReportクラス

class PlainTextReport : Report {
    override func outputHead() {
        println("**** \(title) ****")
    }
    
    override func outputLine(line : String) {
        println(line)
    }
}

これでよし。

あとは、実際にReportクラスを使う処理を書きます。

var report : Report

report = HTMLReport()
report.outputReport()

report = PlainTextReport()
report.outputReport()

実行結果

<html>
  <head>
  <title>月次報告</title>
  </head>
  <body>
  <p>順調</p>
  <p>最高の調子</p>
  </body>
</html>
**** 月次報告 ****
順調
最高の調子
Program ended with exit code: 0

これ、いろんな利点がありますが、だいたい以下2点は多くの方に同意していただけるのではないでしょうか。

  • 月次報告を新たに別のフォーマットで出力する必要が生じたときに、対応するのがラク(Reportを継承して、必要な処理を埋めるだけだから)
  • レポートクラスを使う側もラク(フォーマットクラスによらず、使い方が一貫しているから)

一方で、TemplateMethodパターンには欠点もあります。

継承ベースの設計では、派生クラスが基底クラスに依存するため、たとえば現行のテンプレートに必ずしも基づかないフォーマットで月次報告を出力したくなったときに、かなりの修正が必要になるかも知れません。

次は、この問題点を解決するために別の切り口から考えてみます。

Strategyパターン

Strategyパターンは、継承ではなく委譲ベースでアルゴリズムをスイッチするのが特徴です。

class Report {
    var title: String
    var text: String[]
    
    var formatter: Formatter
    
    init(formatter : Formatter) {
        title = "月次報告"
        text = ["順調", "最高の調子"]
        self.formatter = formatter
    }
    
    func outputReport() {
        formatter.outputReport(self)
    }
}

Formatterプロトコル

protocol Formatter {
    func outputReport(context: Report)
}

HTMLFormatterクラス

class HTMLFormatter : Formatter {
    func outputReport(context: Report) {
        println("<html>")
        println("  <head>")
        println("    <title>\(context.title)</title>")
        println("  </head>")
        println("  <body>")
        for line in context.text {
            println("    <p>\(line)</p>")
        }
        println("  </body>")
        println("</html>")
    }
}

PlainTextFormatterクラス

class PlainTextFormatter : Formatter {
    func outputReport(context: Report) {
        println("**** \(context.title) ****")
        for line in context.text {
            println("\(line)")
        }
    }
}

使い方はさっきと少し違います。

var report : Report

report = Report(formatter: HTMLFormatter())
report.outputReport()

report = Report(formatter: PlainTextFormatter())
report.outputReport()

Reportクラスに、Formatterを採用したオブジェクトを渡してやります。Strategyでは、委譲メインの設計になったこともあり、継承の静的な依存関係から解放されました。

余談ですが、僕がデザインパターンを学んだときに初めて目にしたのが、このStrategyパターンでした。

とはいえ、TemplateMethodと較べてStrategyが絶対的に優れているわけではなく、あくまで切り口が違うだけだと思っています。お仕事でプログラムを書いていると、むしろTemplateMethodを適用したソースをよく目にしますし。

実行結果は先程と同じなので割愛します。

おまけ:クロージャでお手軽Strategy

ちなみに、Swiftではクロージャが使えるようになりました。

邪道かも知れませんが、その場限りのコンテキストならクラスである必要はなく、クロージャで記述することも可能です。

ちょっと丁寧に書くとこんな感じでしょうか。

main.swift

// Reportクラス
class Report {
    var title: String
    var text: String[]
    
    var formatter: (context: Report) -> ()
    
    init(formatter: (context: Report) -> ()) {
        title = "月次報告"
        text = ["順調", "最高の調子"]

        self.formatter = formatter
    }
    
    func outputReport() {
        formatter(context: self)
    }
}

// クロージャで作るストラテジ
var formatter = { (context: Report) -> () in
    println("<html>")
    println("  <head>")
    println("    <title>\(context.title)</title>")
    println("  </head>")
    println("  <body>")
    for line in context.text {
        println("    <p>\(line)</p>")
    }
    println("  </body>")
    println("</html>")
}

// ためすよ
var report : Report

report = Report(formatter)
report.outputReport()

ちなみに、report のイニシャライザに直接クロージャをぶち込むこともできます。なんかちょっと変態的になりました。

var report = Report({ (context: Report) -> () in
    println("<html>")
    println("  <head>")
    println("    <title>\(context.title)</title>")
    println("  </head>")
    println("  <body>")
    for line in context.text {
        println("    <p>\(line)</p>")
    }
    println("  </body>")
    println("</html>")
    })

report.outputReport()

なんか当初の目的からだいぶ逸れた気もします。ごめんなさい。

あと、Swiftの日本語ドキュメントが早く出たらいいなぁと思いました。まる。

*1:新しいプログラム言語を学ぶときのはじめの一歩は、画面に「Hello World」と表示する簡単なプログラムを書くことです。

*2:このタイミングでSwiftに手を出す開発者にとっては釈迦に説法かも知れませんが。

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.