BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ ニュース Swiftにおけるプロトコル指向プログラミング

Swiftにおけるプロトコル指向プログラミング

原文(投稿日:2015/06/18)へのリンク

WWDCにて、C++/Boostで知られ、現在はAppleでSwift Standard Libraryグループのリーダを務めるDave Abrahams氏が、Swiftをプロトコル指向言語として紹介し、プロトコルがコード改善にどう使えるのか説明した。

プロトコル指向プログラミングというのは、OOP(オブジェクト指向プログラミング)のパラダイムの一つで(注:Abrahams氏はそうは言っていないとのこと)、クラスよりもプロトコル(インターフェイスに相当)と構造体の利用を好んでいる。

クラスは素晴らしい?

OOPで知られているように、クラスは以下を提供するのに使われる。

  • カプセル化
  • アクセス制御
  • 抽象化
  • 名前空間
  • 表現力
  • 拡張性

実のところ、これらはすべて型の特性であり、クラスは型を実装する一つの方法にすぎないとAbrahams氏は言う。だが、クラスはプログラマに多大な犠牲を強い、次のようなことが起こるおそれがある。

  • 暗黙の共有。たとえば、2つのオブジェクトが第3のオブジェクトを参照している場合、2つのオブジェクトは互いにそのことを知らずに、第3のオブジェクトを変更できてしまう。共有を回避するため、参照するオブジェクトを複製すると、今度は効率がわるくなる。ロックを使って競合を避けるという方法もある。だが、これはさらに効率をわるくし、デッドロックにつながるおそれもある。コードはさらに複雑になる。これはバグがさらに増えることを意味する。
  • 継承問題。多くのオブジェクト指向言語では、スーパークラスを一つしか持つことができず、初期の段階で選ばなくてはならない。スーパークラスをあとから変更するのは、極めて困難だ。またスーパークラスは派生クラスにストアドプロパティを強制する。これは初期化の処理と、スーパークラスが求める普遍性を壊さないようにするのを複雑にする。さらに通常は、オーバーライドできるもの、そのやり方、いつすべきでないかに制約がある。こうした制約は通常、ドキュメントに残される。
  • 型関係の喪失。これはインターフェイスと実装の合体によるものだ。通常、実装できないベースクラスのメソッドに現れ、派生クラスのメソッド実装において、派生クラスにダウンキャストする必要がある。これは次のコードを使って説明された。
class Ordered {
  func precedes(other: Ordered) -> Bool { fatalError("implement me!") }
}

class Label : Ordered { var text: String = "" ... }

class Number : Ordered {
  var value: Double = 0
  override func precedes(other: Ordered) -> Bool {
    return value < (other as! Number).value
    }
}

Abrahams氏によると、プロトコル指向プログラミングは、以下の点で、より優れた抽象化の仕組みだという。

  • バリュー型(クラスを除く)
  • 静的な型関係(動的ディスパッチを除く)
  • レトロプロアクティブなモデリング
  • モデルにデータを強制しない
  • 初期化の負担がない
  • 何が実装されなくてはならないかが明確

プロトコル指向プログラミング

Swiftで新たに抽象化を考えるとき、最初のステップは常にプロトコルであるべきだとAbrahams氏は言う。そして、Orderedクラスの例をプロトコルと構造体を使って書き直すことで、実装がいかにクリーンになるか説明した。

protocol Ordered {
  func precedes(other: Self) -> Bool
}
struct Number : Ordered {
  var value: Double = 0
  func precedes(other: Number) -> Bool {
    return self.value < other.value
  }
}

precedesのプロトコル要件にSelfを使うことで、Numberクラスのprecedesメソッド実装は、適切なパラメータを正しく取得することができ、キャストが不要になる。

Self要件は、それを含むプロトコルの使用に重要な意味を持っている。Orderedのインスタンスの配列を引数にとるbinarySearchメソッドを定義すると、次のようなコードになるだろう。

class Ordered { ... }

func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int {
  var lo = 0
  var hi = sortedKeys.count
  while hi > lo {
    let mid = lo + (hi - lo) / 2
    if sortedKeys[mid].precedes(k) { lo = mid + 1 }
    else { hi = mid }
  }
  return lo
}

これに対し、Self要件を含むプロトコルを使うと、ジェネリックなメソッドを定義する必要がある。

protocol Ordered { ... }

func binarySearch(sortedKeys: [T], forKey k: T) -> Int {
  ...
}

Self要件を使うのと使わないのとの違いは、広範囲に及ぶ。とりわけ、Self要件は私たちを静的ディスパッチの場に置き、ジェネリックスとホモジニアスなコレクションを要求する。違いは以下の図にまとめられている。

 

Retroactive modeling

Abrahams氏はプロトコルと構造体がクラス階層を置き換えるのにどう使えるか説明するために、幾何図形をレンダリングすることを目的としたRenderer playgroundを紹介した。これはプロトコルと構造体が提供するretroactive modelingの可能性を示すものだ。この例では、Retroactive modelingを適用して、Rendererプロトコル要件を実装したCGContextの拡張を作っている。

protocol Renderer {
  func moveTo(p: CGPoint)
  func lineTo(p: CGPoint)
  func arcAt(center: CGPoint, radius: CGFloat,
  startAngle: CGFloat, endAngle: CGFloat)
}

extension CGContext : Renderer {
   ...
}

こうすることで、CGContext型はRenderer以前に定義されていたにもかかわらず、Renderer型が使えるところで使えるようになる。

その一方で、幾何図形のテキスト表現を出力するTestRendererクラスにを通して、プロトコルの匿名実装を提供することができる。

struct TestRenderer : Renderer {
  func moveTo(p: CGPoint) { print("moveTo(\(p.x), \(p.y))") }
  func lineTo(p: CGPoint) { print("lineTo(\(p.x), \(p.y))") }
  ...
}

これら2つのRenderer実装は、入れ替えて使うことができる。

プロトコル拡張

プロトコルがもっと便利に使えるよう、Swift 2.0にはプロトコル拡張という新機能が導入された。この機能を使うことで、プロトコル要件のデフォルト実装を提供することが可能になる。これは以下のコードを使って説明された。

protocol Renderer {
  func moveTo(p: CGPoint)
  func lineTo(p: CGPoint)
  func circleAt(center: CGPoint, radius: CGFloat)
  func arcAt(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat)
}

extension Renderer {
  func circleAt(center: CGPoint, radius: CGFloat) {
    arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
  }
}

Rendererのプロトコル拡張にcircleAtを定義すると、その実装はCGContextTestRendererで共有される。

制約付き拡張

プロトコル拡張を使うことで、使われる型の制約を明示することができる。以下の例では、コレクションの要素が要件を満たすときだけ、CollectionTypeのプロトコル拡張が定義される。

extension CollectionType where Generator.Element : Equatable {
  public func indexOf(element: Generator.Element) -> Index? {
    for i in self.indices {
      if self[i] == element {
        return i
      }
    }
    return nil
  }
}

Generator.ElementEquatableと宣言することで、indexOf内で==オペレータが利用できるようになる。

最後に、ジェネリック関数定義をきれいにするなど、プロトコル拡張と制約で実現できるテクニックがいくつか紹介された。たとえば以下は、

func binarySearch<
 C : CollectionType where C.Index == RandomAccessIndexType, C.Generator.Element : Ordered
>(sortedKeys: C, forKey k: C.Generator.Element) -> Int { ... }

次のように書くことができる。

extension CollectionType where Index == RandomAccessIndexType, 2 Generator.Element : Ordered {
  ...
}

いつクラスを使うべきか?

Abrahams氏はプレゼンテーションの最後で、もしあなたが暗黙の共有を望むなら、クラスにもまだ居場所があると述べた。具体的には、以下のような場合だ。

  • インスタンスのコピーや比較が意味をなさない場合
  • インスタンスのライフタイムが外的影響に結びついている場合。例:TemporaryFile
  • インスタンスが「シンク」であり、CGContextのように外部状態を変更するだけの場合

さらにAbrahams氏はこう語る。Cocoaのような、オブジェクトとサブクラスの考え方で構築されたフレームワークを使う場合、システムに対して戦いを挑んでも仕方がない。だが、大きなクラスをリファクタリングするときには、プロトコルと構造体を使って取り除くと、もっと改善できるだろう。

この記事に星をつける

おすすめ度
スタイル

BT