Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 432 - Swiftでデザインパターン 10 Decorator(デコレータ)

2016年12月2日:Swift 3に完全対応*1

最初のページに戻る -> Swiftで遊ぼう! - 423 - Swiftでデザインパターン まえがき - Swiftで遊ぼう! on Hatena

yataiblue.hatenablog.com

Introducing iOS Design Patterns in Swift – Part 1/2チュートリアルデザインパターンの勉強は続いています。シングルトン・デザインのLibrayAPIクラスをファサード・パターンとして使用できるようになりました。TableViewにデータを表示させるために次はデコレータ・パターンを使います。

オブジェクト指向における再利用のためのデザインパターン

オブジェクト指向における再利用のためのデザインパターン

デコレータ・パターン

デコレータ、訳すと「装飾」です。ちょっと調べるとクラス継承をすることなくメソッドを拡張させる方法のようです。

しかし、実際の実装法をネットで調べると、数々のバリエーションがあり、プログラミング言語の言語仕様に依存しているようです。

まず抽象的なイメージをクラス図で眺めます。

f:id:yataiblue:20150905170622j:plain

デコレータを使用したいシチュエーションは、メソッドを動的に変更させたい場合です。アイスクリームやカレーのトッピングの選択によって値段が変わる例がよく挙げられています。

class MenuComponent {
    func getPrice() -> Int {
        fatalError("must be override")
    }
}
// Componentクラスです。具体的な実装はされていません。

class CurryMenu: MenuComponent {
    override func getPrice() -> Int {
        return 490
    }
}
// ConcreteComponentクラスで具体的な実装をします。

class NoodleMenu: MenuComponent {
    override func getPrice() -> Int {
        return 290
    }
}
// もう一つのConcreteComponentクラス

class ToppingDecorator: MenuComponent {
    let menu: MenuComponent
    
    init(_ menu: MenuComponent) {
        self.menu = menu
    }
}
// Decoratorクラスは、Componentクラスで初期化する。

class CornTopping: ToppingDecorator {
    override func getPrice() -> Int {
        return menu.getPrice() + 100
    }
}
// ConcreteDecoratorクラスで具体的な実装

class SausageTopping: ToppingDecorator {
    override func getPrice() -> Int {
        return menu.getPrice() + 200
    }
}
// 別のConcreteDecoratorクラス

let cornCurry = CornTopping(CurryMenu())
cornCurry.getPrice()
// カレーの値段が490円で、
// コーンのトッピングが100円なので
// 合計590円が得られます。

let sausageNoodle = SausageTopping(NoodleMenu())
sausageNoodle.getPrice()
// ヌードルの値段が290円で
// ソーセージのトッピングが200円なので
//合計490円が得られます。

しかし、このコーディングでは、クラス継承があり、「クラス継承を使わないで(overrideしないで)、既存クラスに変更を加えること無く機能を装飾する方法」という説明に反します。

以下のコーディングだDecorator(デコレータ)パターンになります。

class Untouchable {
    func test() -> String {
        return "This sentence is untocuable, "
    }
}
// ベースになる変更を加えないからComponentクラス?

class DecoratorOption {
    let untouchable = Untouchable()
    
    func assTest() -> String {
        return untouchable.test() + "and I can add this."
    }
}

let test1 = Untouchable()
let test2 = DecoratorOption()

test1.test()
// "This sentence is untocuable, "
test2.addTest()
// "This sentence is untocuable, and I can add this."

必ずコンポーネントをクラスをインスタンスとして持つことでメソッドの拡張ができます。

Extnsion(エクステンション)

デコレータ・パターンの実装ですが、Swift言語には「Extension」が用意されています。

デコレータと異なる点は、拡張させたい(Component)クラスのインスタンスをプロパティとして持つ必要がないところで、簡単に拡張させられます。しかし、元あるメソッドをオーバーライドすることはできないので、独自の名前を付けて個別化させないと混乱します。

デザイン・パターンのチュートリアルに戻って「デコレート」として「Extension」を作ります。

まずテーブルビューで表示するデータは、Albumクラスですが、表示に最適化したデータ構造を持っていません。Albumクラスは、あくまでもMVCのモデルなので、表示に関して知らぬ存ぜぬという立場なので、UITableViewで表示しやすいデータ形式にするために、Albumクラスを拡張(Extension)させます。

Xcodeのメニューから「File -> New -> File...」を選んで「iOS -> Source -> Swift File」を選択して、「AlbumExtensions」と名前をつけて作成します。

import Foundation

extension Album {
    
    func ae_tableRepresentation() -> 
        (titles: [String], values: [String]) {
        
        return (["Artist", "Album", "Genre","Year"], 
                [artist, title, genre, year])
        
    }
    
}

コードはメソッド1つだけです。ネーミングも工夫しています。Extensionで追加されるメソッドはオリジナルクラスのメソッドをオーバライドすることができないため、オリジナルメソッドと区別していないと混乱してしまう場合があります。混乱を避けるためにメソッド名の頭に「ae_」を付けてalbum extensionで追加されたメソッドを明確化させています。

この関数は、アレー型のデータを返すメソッドです。

f:id:yataiblue:20150909152419j:plain

たったこれだけでAlbumクラスにメソッドを追加することができます。当然クラスの継承を使って実装することもできますが、アプリケーションの設計思想に左右されるところです。抽象的なデータだけを扱う「Album」クラスに表示のためのメソッドを継承して加えるのはソフトウエア開発設計からみれば美しくありません。こういう点でExtensionを使用するというのは理にかなっていることになります。

Delegation(デリゲーション)

そしてもう一つの重要なデコレータ・パターンが「デリゲーション」です。ジェネリック(一般的)なオブジェクトが具体的な働きを他のオブジェクトにデリゲーション(委譲)することで、元になるジェネリックなオブジェクトに変更は加えません。変更を加えないで具体的な機能を追加するというところは、まさに「デコレータ・パターン」ということです。

UITableViewクラスはジェネリック(一般的)なので、何をいくつ表示するといった機能はありません。UITableViewクラスを使用するために必ず実装しなければならないデリゲーションメソッドが2つあります

// 必須メソッド1
func tableView(tableView: UITableView,
  numberOfRowsInSection section: Int) -> Int {
    // code
}

// 必須メソッド2
func tableView(tableView: UITableView,
  cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    // code
}

Cellの個数を返すデリゲーションメソッドです。ここでUITableViewとデリゲートとして働くUIViewControllerの関係を示すと次のような関係になります。

f:id:yataiblue:20150911110849j:plain

このデリゲーションの実装をしていこうと思います。

デリゲーションを実際iOSフレームワークで実装していく場合、例えば「UIViewとUIViewController」や「UITableViewとUIViewController」の関係をコードする場合、プロトコールと組み合わせるのが一般的です

しかし、このチュートリアルでは、まだプロトコールの説明が済んでいいないので、Extensionを使って、デリゲーションメソッドを組み込んでいます。

あっ!!でした(^_^;)

私には新鮮な驚きでした。制限はあるけど、別にこういうやり方でメソッドを実装しても間違いじゃないんです。これがプログラミングの多様性です。

では、デリゲーション(委譲される)を受ける「デリゲート」クラスをコードしていきます。ViewController.swiftを開いて次のプロパティを実装させます。

fileprivate var allAlbums = [Album]()
fileprivate var currentAlbumData: (titles:[String], values:[String])?
fileprivate var currentAlbumIndex = 0

アルバムデータをすべて保持するアレー型の変数allAlbums、cell表示用のデータを保持するタプル変数currentAlbumDataとcell行を意味するcurrentAlbumIndexです。

次はviewDidLoad()に実装をしていきます。

override func viewDidLoad() {
 super.viewDidLoad()
// ファサードでくるまったシングルトンクラスインスタンスのメソッド
allAlbums = LibraryAPI.sharedInstance.getAlbums()

Albumクラスのデータを保持しているのはPersistancyManagerですが、LibraryAPIが間接的にアクセスして操作することで覆い隠しています。デベロッパーはLibraryAPIの利用だけを考えたらいいようになっています。

dataTable.delegate = self
dataTable.dataSource = self
dataTable.backgroundView = nil

これらがデリゲーション・プロパティで、viewController自身を持たせます。

もう少し詳しく説明すると、UITableViewクラスは、具体的な処理をさせるためのプロパティを持っています。そのプロパティがそれぞれ、「delegate」と「dataSource」です。これらプロパティは、TableViewの状態を変化させるためのメソッドを持っています。これらプロパティをViewController型(タイプ)にすることで、ViewControllerの具体的なコード実装でTableViewを変化させられます... 初心者には解りづらい説明ですねm(_ _)m この概念は非常に重要なので、私の過去の記事を読んで下さい

そして次のコードを入れます。

dataTable.backgroundView = nil
view.addSubview(dataTable!)

TableViewのバックグラウンドを透明にしてデフォルトのviewに表示させるコードです。

ここまでのコード入力を終えたところで、Xcode上のdataTable.delegateとdataTable.dataSourceの前に赤丸エラー表示がされているはずです。

f:id:yataiblue:20161202073626j:plain

dataTable型は@IBOutletのプロパティとして存在しますが、dataTable.delegateは、UITableViewDelegate型(タイプ)になり、この「型」はUIViewControllerは持っていません。この型を組み込むためにUIViewControllerクラスに「拡張(Extension)」する必要があります。何度も言いますが一般的にプロトコールに準拠するのが一般的です。しかし、このチュートリアルプロトコールの概念はまだ説明されていないので、他の方法としてExtensionを利用しています。UIViewControllerクラス宣言最後の「}」の外側に次のコードを加えます。

extension ViewController: UITableViewDataSource {
    
}

extension ViewController: UITableViewDelegate {
    
}

すると、dataTable.delegateとdataTable.dataSourceの前についていた赤い丸が消えます! 当然です。UIViewControllerは拡張されて、UITableViewDelegateプロトコール(型)とUITableViewDataSourceプロトコール(型)をプロパティとして持つことができるようになったからです。

しかし、今度はextension ViewController: UITableViewDataSourceの前に赤丸が出現します。

f:id:yataiblue:20161202074442j:plain

もう皆さん解りますね。DataSourceプロトコールは実装しなければならなに必須メソッドが2つあるからです。UITableViewDelegateに赤丸が付かないのは、メソッドがすべてオプション扱いだからです。

最後にデリゲーションメソッドを実装させます。

extension ViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, 
             numberOfRowsInSection section: Int) -> Int {
        if let albumData = currentAlbumData {
            return albumData.titles.count
        } else {
            // デバック用にコンソールに表示
            print("データがありません")
            return 0
        }
    }
    
    func tableView(_ tableView: UITableView, 
             cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell:UITableViewCell = 
            tableView.dequeueReusableCell(withIdentifier: "Cell", 
                for: indexPath)
        if let albumData = currentAlbumData {
            cell.textLabel!.text = albumData.titles[indexPath.row]
            cell.detailTextLabel!.text = 
                albumData.values[indexPath.row]
        }
        return cell
    }
    
}

これで完成!

ランすると...

f:id:yataiblue:20161202080205j:plain

あれ? テーブルが表示されていません!

入力するコードが多くなると何が抜けているのか分からなくなることがあります。よく見るとcurrentAlbumDataに値が取りこまれていません。

もうワンステップ用意する必要があります。

// テーブルに表示用にデータを整えるメソッドです。
    func showDataForAlbum(atIndex: Int) {
        // クラッシュをさけるために保護コード
        if (atIndex < allAlbums.count && atIndex > -1) {
            //アルバムデータを習得します
            let album = allAlbums[atIndex]
            // Extensionで拡張したメソッドを使って
            // 表示用のタップルデータを習得します。
            currentAlbumData = album.ae_tableRepresentation()
        } else {
            // デバック用にコンソールに表示
            print("メソッドでアルバムデータが習得できません")
            currentAlbumData = nil
        }
        // データが変化するとテーブル表示をリロード
        dataTable!.reloadData()
    }

これでテーブル表示用のデータは用意できたので何処で呼びますか? そうです! viewDidLoad()メソッド内です。

self.showDataForAlbum(atIndex: currentAlbumIndex)

もう一度ランさせます。

f:id:yataiblue:20161202171130j:plain

じゃじゃーん! ちゃんと表示されました!

yataiblue.hatenablog.com

*1:2016年12月1日:Swift 3に向けて改訂中、2015年12月5日改訂:デザインパターンの復習をしています。