Help us understand the problem. What is going on with this article?

Swiftを使用してデザインパターンまとめ 1.Iteratorパターン

More than 1 year has passed since last update.

(・ω・`)初投稿ですのでお手柔らかにお願いします(´・ω・)
間違っているところや補足事項があったらぜひ教えてください!

はじめに

この投稿は 増補改訂版 Java言語で学ぶデザインパターン入門 に沿って学んだことを章ごとにまとめるものです.
今回は第1章 Iterator についてまとめます.

Iterator とは

配列などのデータ構造のそれぞれの要素に順番にアクセスするために使用されます.
例えば,下のよく見かけるようなfor文で使用されている変数iが Iterator の役割をしています.

for(i=0;i<str.count;i++) {
    printf(str[i]);
}

ここでは,Iterator の役割を抽象化・一般化したものを Iteratorパターンと呼びます.

Iteratorパターンの概要

Iteratorパターンのクラス図は下のように表せます.

iterator.png

細かいプロパティやメソッドは文献によって異なりますが,

  • 抽象クラスの Aggregate が Iterator 役を作成できる
  • ConcreteAggregate で Aggregate が指定したインタフェースを実装する
  • 抽象クラスの Iterator は,次の要素が存在するかどうか・存在するのであれば次の要素は何かを調べる術がある
  • ConcreteIterator で Iterator が指定したインタフェースを実装する

という点は共通しています.

実装

クラス図に沿って抽象クラスを作成して実装

実際にクラス図に沿って実装していきます.

まずは抽象クラスの実装です.

Aggregateクラス

class Aggregate {
    /// イテレータを作成します
    public func iterator() -> Iterator {
        fatalError("サブクラスで実装してください")
    }
}

Iteratorクラス

class Iterator {
    /// 次の要素が存在するかどうかを判定します
    public var hasNext: Bool {
        fatalError("サブクラスで実装してください")
    }
    /// 次の要素を返します
    public func next() -> AnyObject {
        fatalError("サブクラスで実装してください")
    }
}

Aggregate クラスと Iterator クラスでは具体的な実装は全てサブクラス側で行うため,メソッドやプロパティの枠組みだけを用意しています.
また,クラス図では Iterator クラスの hasNext はメソッドになっていますが,今回は計算型のプロパティを使用しました.

次に,具体的なクラスの実装を説明します.
今回はクローゼットに服をしまう動作を例に実装しました.

まずは集合の要素として使用するクラスを用意します.

Clothesクラス

/// 服クラス
final class Clothes: NSObject {
    /// 服の種類
    let type: String
    /// 色
    let color: String
    /// 模様
    let pattern: String

    required init(type: String, color: String, pattern: String) {
        self.type = type
        self.color = color
        self.pattern = pattern
    }

    var detail: String {
        return "\(color) \(pattern) \(type)"
    }
}

服の種類・色・模様のプロパティを持つクラスです.
きちんと実装するのであれば種類・色・模様の型は列挙型で定義すると良いと思うのですが,今回は特に重要な箇所ではないので String としました.

次に, Aggregate クラスと Iterator クラスの具象クラスの実装をしていきます.

ConcreteAggregate クラス

/// ConcreteAggregate にあたるクラス
final class Closet: Aggregate {
    /// 服クラスの配列
    private var clothes: [Clothes] = []
    /// 服の数
    public var count: Int {
        return clothes.count
    }
    /// index に対応する服を返します
    public func getClothes(at index: Int) -> Clothes {
        return clothes[index]
    }
    /// 服を追加します
    public func append(_ clothes: [Clothes]) {
        self.clothes.append(contentsOf: clothes)
    }
    /// イテレータを作成します
    override public func iterator() -> Iterator {
        return ClosetIterator(self)
    }
}

iterator() の具体的な実装をしています.
また,具体的なデータ構造を外から隠すために,配列 clothes をプライベートプロパティにしています.
そのため,外から服の情報を得たり,服を追加したりするために必要なメソッドを追加しました.

getClothes(at:) は配列 clothes の範囲外の index を引数に与えてしまうとクラッシュの原因になるので,本来は

    /// index に対応する服を返します
    public func getClothes(at index: Int) -> Clothes? {
        guard index < count else { return nil }
        return clothes[index]
    }

とした方が良いと思いますが,今回は簡単のため範囲外の値を引数に渡すことはない,という想定で行きます.

ConcreteIterator クラス

/// ConcreteIterator にあたるクラス
final class ClosetIterator: Iterator {
    private let closet: Closet
    private var index = 0

    required init(_ closet: Closet) {
        self.closet = closet
    }
    /// 次の要素が存在するかどうかを判定します
    override public var hasNext: Bool {
        return index < closet.count
    }
    /// 次の要素を返します
    override public func next() -> AnyObject {
        let nextItem = closet.getClothes(at: index)
        // index を次の要素に合わせます
        index += 1
        return nextItem
    }
}

Iterator クラスの具体的な実装をしました.

これでクローゼットの準備が終わりました.
早速このクローゼットと Iterator を使用してみようと思います.

// クローゼットの作成
let myCloset = Closet()
// 服を追加
myCloset.append([Clothes(type: "blouse", color: "white", pattern: "plane"),
                 Clothes(type: "skirt", color: "red", pattern: "check"),
                 Clothes(type: "coat", color: "green", pattern: "plane"),
                 Clothes(type: "onePieceDress", color: "violet", pattern: "stripes")])
// イテレータを作成
let iterator = myCloset.iterator()

while iterator.hasNext {
    print((iterator.next() as! Clothes).detail)
}

ここでの出力は,

white plane blouse
red check skirt
green plane coat
violet stripes onePieceDress

となりました.

Iteratorパターンのメリット

ConcreteAggregateクラスの実装方法が変更されてもConcreteAggregateクラスの利用者には関係ない

ClosetクラスにClothes要素を追加するために append(_:) を使用したり,Iteratorが次の服を取り出すために getClothes(at:) を使用したりしていましたが,ClosetにClothesを追加する利用者も,Iteratorも,Closetクラスがどのようなデータ構造にClothesを収納しているのか知りません.

Closetクラスが,Clothesを追加したり,取り出したりするために必要なメソッドだけを公開しているからです.

これによって,Closetクラスが配列ではなく,別のデータ構造を使用してClothesを管理しようと思った時に,Closetクラスを利用しているコードの修正をすることなくデータ構造を取り替えることができるようになります.

最初の例のように,for文を使用して変数iで配列内の要素にアクセスしていた場合,データ構造が変更されるとfor文を記述していた箇所に修正を加える必要があります.

しかし,Iteratorパターンを使用していた場合は提供していたメソッドやプロパティの名前を変更したり削除したりせずに正しい値を返している限り,Closetクラスの利用者側には一切影響しません.

ライブラリ等で,アップデートによって内部の実装方法が変わってもメソッド名やプロパティ名,提供する機能が変わっていなければライブラリの利用者はコードを直すことなく使用できるイメージです.

イテレートできないデータ構造にイテレート機能を追加できる

Iteratorパターンを適用することで,列挙型の要素に順番にアクセスすることができるようになります.

思ったこと

今回Iteratorパターンについてまとめるにあたって本を何度も読み返したり,実際に自分で実装してみたりしました.
先週の金曜日に社内の読書会で本を読んだだけでは,いまいち恩恵がわからずfor文で回しまくればよいのでは...?と思っていたのですが,実際に手を動かしてみて,なんとなくデザインパターンの考え方がわかったような気がします.

特になんども変更される可能性が高い箇所を,クラスの利用者側から隠してしまえば,変更が入った時に利用側のクラスの実装を一つ一つ直す必要がなくなり,修正忘れによってエラーが発生したり,気づかないうちに想定外の動作をしていたりすることを防ぐことができてとても良いなぁと思いました.

とはいえ,何でもかんでもIteratorパターンを適用する必要もないと思うので,複雑な実装や変更が発生しやすそうな場所など,上手に見極めて適用できると良いなぁと思いました.

まとめ

  • Iteratorパターンは Aggregate クラスでデータ構造とイテレータを保持し, Iterator クラスで次の要素を提示する
  • ConcreteAggretageクラス側を変更してもConcreteAggretageクラスの利用側の実装を修正する必要がなくなる

初投稿なのでめちゃくちゃ頑張って書いたら他のことを何もやることなく日曜日が終わってしまったので,来週からはもう少しゆるっと気軽に書いていこうと思います(´・ω・`)

付録: せっかくなので別の実装方法を考えてみました

上ではクラス図に合わせて抽象クラスを作成して実装しましたが,別の方法も試してみました.

Generics を使用した抽象クラスを作成して実装

先ほどの実装では,Iterator の next() を使用して得られる要素の型をうまく指定出来ず, Clothes をクラスにし, next() で得られる要素を AnyObject にしていました.
そのため,実行側で以下のように強制キャストする必要がありました.

while iterator.hasNext {
    print((iterator.next() as! Clothes).detail)
}

Clothes を構造体にしたい,あまり強制キャストは使いたくない,ということで Generics を使用して実装してみました.

Aggregateクラス

class Aggregate<Elem> {
    /// イテレータを作成します
    public func iterator() -> Iterator<Elem> {
        fatalError("サブクラスで実装してください")
    }
}

ここでの Elem は,集合体の要素の型を表します.
今回のクローゼット実装では Clothes にあたります.

Iteratorクラス

class Iterator<Elem> {
    /// 次の要素が存在するかどうかを判定します
    public var hasNext: Bool {
        fatalError("サブクラスで実装してください")
    }
    /// 次の要素を返します
    public func next() -> Elem {
        fatalError("サブクラスで実装してください")
    }
}

ConcreteAggregateクラス

/// ConcreteAggregate にあたるクラス
final class Closet: Aggregate<Clothes> {
    /// 服クラスの配列
    private var clothes: [Clothes] = []
    /// 服の数
    public var count: Int {
        return clothes.count
    }
    /// index に対応する服を返します
    public func getClothes(at index: Int) -> Clothes {
        return clothes[index]
    }
    /// 服を追加します
    public func append(_ clothes: [Clothes]) {
        self.clothes.append(contentsOf: clothes)
    }
    /// イテレータを作成します
    override public func iterator() -> ClosetIterator {
        return ClosetIterator(self)
    }
}

具体的な実装を行うクラスで Elem の指定をしました.

ConcreteIteratorクラス

/// ConcreteIterator にあたるクラス
final class ClosetIterator: Iterator<Clothes> {
    private let closet: Closet
    private var index = 0

    required init(_ closet: Closet) {
        self.closet = closet
    }
    /// 次の要素が存在するかどうかを判定します
    override public var hasNext: Bool {
        return index < closet.count
    }
    /// 次の要素を返します
    override public func next() -> Clothes {
        let nextItem = closet.getClothes(at: index)
        // index を次の要素に合わせます
        index += 1
        return nextItem
    }
}

これによって Clothes を構造体にすることができるようになりました.

Clothes

/// 構造体バージョン
struct Clothes {
    /// 服の種類
    let type: String
    /// 色
    let color: String
    /// 模様
    let pattern: String

    var detail: String {
        return "\(color) \(pattern) \(type)"
    }
}

使用例

実装の仕方は微妙に変わりましたが, Aggregate クラス, Iterator クラスが持っているメソッドは変わっていないため,強制キャストする必要がなくなった以外に使用方法に違いはありません.

// クローゼットの作成
let myCloset = Closet()
// 服を追加
myCloset.append([Clothes(type: "blouse", color: "white", pattern: "plane"),
                 Clothes(type: "skirt", color: "red", pattern: "check"),
                 Clothes(type: "coat", color: "green", pattern: "plane"),
                 Clothes(type: "onePieceDress", color: "violet", pattern: "stripes")])
// イテレータを作成
let iterator = myCloset.iterator()

while iterator.hasNext {
    print(iterator.next().detail)
}

プロトコルを使用して実装

抽象クラスではなく,プロトコルとして AggregateIterator を実装しました.
Clothesは構造体のものを使用しています.

Aggregate

protocol Aggregate {
    // イテレータの型
    associatedtype Iterator: IteratorType
    /// イテレータを作成します
    func iterator() -> Iterator
}

Iterator

protocol IteratorType {
    // 要素の型
    associatedtype Elem
    /// 次の要素が存在するかどうかを判定します
    var hasNext: Bool { get }
    /// 次の要素を返します
    func next() -> Elem
}

ConcreteAggregate

/// ConcreteAggregate にあたるクラス
final class Closet: Aggregate {
    typealias Iterator = ClosetIterator
    /// 服クラスの配列
    private var clothes: [Clothes] = []
    /// 服の数
    public var count: Int {
        return clothes.count
    }
    /// index に対応する服を返します
    public func getClothes(at index: Int) -> Clothes {
        return clothes[index]
    }
    /// 服を追加します
    public func append(_ clothes: [Clothes]) {
        self.clothes.append(contentsOf: clothes)
    }
    /// イテレータを作成します
    public func iterator() -> ClosetIterator {
        return ClosetIterator(closet: self)
    }
}

ConcreteIterator

/// ConcreteIterator にあたるクラス
final class ClosetIterator: IteratorType {
    typealias Elem = Clothes

    private let closet: Closet
    private var index: Int = 0

    required init(closet: Closet) {
        self.closet = closet
    }
    /// 次の要素が存在するかどうかを判定します
    public var hasNext: Bool {
        return index < closet.count
    }
    /// 次の要素を返します
    public func next() -> Clothes {
        let clothes = closet.getClothes(at: index)
        index += 1
        return clothes
    }
}

抽象クラスでの実装よりも型指定が増えました.
利用方法は変わらないので割愛します.

IteratorProtocol を使用して実装

Swiftには,Aggregateに近いものとして,Sequenceプロトコルがあります.
Sequenceに準拠すると便利なメソッドが使用できるようになります.
特に, for-in を使用してイテレートできるようになります.

Sequenceプロトコルに準拠するためには, makeIterator() -> Self.Iterator メソッドを実装する必要があります.
ここで返す Iterator は,IteratorProtocol に準拠する必要があります.

IteratorProtocol は Iteratorパターンにおける Iterator の役割を持ちます.
IteratorProtocol に準拠するためには, next() -> Self.Element? メソッドを実装する必要があります.

Closetクラス

final class Closet: Sequence {
    /// 服クラスの配列
    private var clothes: [Clothes] = []
    /// 服の数
    public var count: Int {
        return clothes.count
    }
    /// index に対応する服を返します
    public func getClothes(at index: Int) -> Clothes? {
        guard index < clothes.count else { return nil }
        return clothes[index]
    }
    /// 服を追加します
    public func append(_ clothes: [Clothes]) {
        self.clothes.append(contentsOf: clothes)
    }
    /// イテレータを作成します
    public func makeIterator() -> ClosetIterator {
        return ClosetIterator(closet: self)
    }
}

ClosetIteratorクラス

final class ClosetIterator: IteratorProtocol {
    typealias Element = Clothes
    private let closet: Closet
    private var index: Int = 0

    required init(closet: Closet) {
        self.closet = closet
    }
    /// 次の要素を返します
    public func next() -> Clothes? {
        if let next = closet.getClothes(at: index) {
            index += 1
            return next
        } else {
            return nil
        }
    }
}

次の要素がない場合に next() メソッドが nil を返すため, getClothes(at:) メソッドと next() メソッドの実装を若干変更しましたが,Iteratorパターンのクラス図と似たように実装することができました.

利用方法

let myCloset = Closet()
myCloset.append([Clothes(type: "blouse", color: "white", pattern: "plane"),
                 Clothes(type: "skirt", color: "red", pattern: "check"),
                 Clothes(type: "coat", color: "green", pattern: "plane"),
                 Clothes(type: "onePieceDress", color: "violet", pattern: "stripes")])
for clothes in myCloset {
    print(clothes.detail)
}

出力は上の例と同じなので割愛します.

このように,SwiftにはIteratorパターンに相当するプロトコルが用意されているので,わざわざ抽象クラスやプロトコルを用意して実装することなくIteratorパターンを活用できます.

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away