Swiftで遊ぼう! on Hatena

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

Swiftで遊ぼう! - 429 - Swiftでデザインパターン - Singleton(シングルトン)

2016年12月1日:Swift 3向けに改訂*1

yataiblue.hatenablog.com

Introducing iOS Design Patterns in Swift – Part 1/2チュートリアルにトライしています。次のパートで「MVC」の説明が入っています。The King of Design Patterns(デザインパターンの王様)という説明のようにiOSプログラミングに必要不可欠で知らないと話にならない考え方です。さすがに理解力の乏しい初心者親父でも随分前からこの言葉を知っていて何度も復習をしています*2

今日の本題はシングルトン(Singleton)デザインパターンです。

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

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

シングルトン・パターン

まずシングルトンの実際のコードを見ていく前に概念を理解しようと思ってネットで調べてみました...

調べるとますます何がなんだか訳がわからなくなります(^_^;)

まずシングルトンの意味は「クラスのインスタンスが1つだけしか作られないことが保証されていて、そのデータには自由にアクセスできる」ということですね。

どういう時に必要になるのでしょう?

クラスのインスタンス化(初期化?)にはかなりパワーを必要とするようです(メモリーやCPU)。特に大きなデータを何度も扱うような場合です。チュートリアルではログフィルを指しています。たぶんAppleのチュートリアルで扱ったNSCodingを組み込んだMealクラスもシングルトン・モデルのような気がします...

シングルトンをコードする前にありがちなクラス図を眺めて考えます。

f:id:yataiblue:20150901152426j:plain

このクラス図をみると、SingletonクラスはstaticなSingletonクラスのインスタンを1つ持っていて、外からアクセスするためにgetInstance()メソッドを使うという感じです。インスタンスが1つだけを保証するためにタイプ・プロパティ(クラス・プロパティ)*3が使われています。

このクラス図は、Swift風に書かれていないので、Swiftを使ったシングルトンの説明を見ていきます。

Swift 1の頃は、言語仕様が貧弱だったため知恵を絞ってシングルトンを実装する方法が提案されていました。Qittaで神様のようなsusieyyさんの記事を勉強しました。

qiita.com

初心者親父には難しかったです... 特に開発環境が「Xcode6 Beta3」だったんで古すぎました(T_T) 

古い情報で混乱するところがネットを使って勉強をしている時の弊害です。

susieyyさんのSingletonのコードを見てみます。

class Singleton {
    class var sharedInstance : Singleton {
        struct Static {
            static let instance : Singleton = Singleton()
        }
        return Static.instance
    }
}

このコードは「class内でstaticな定数は利用できない」ことを前提に作られています。Singletonクラス内にクラス・プロパティとしてSingletonクラスのインスタンスsharedInstanceを計算型プロパティとして持ちます。これは計算型プロパティなので、stract型のstaticなSingletonクラスのinstance定数インスタンスを返す、という頭が混乱しそうなコーディングでした(T_T)

Swift 1.2以降(Swift 3)になって、class内でstaticな定数を取れるようになったためこのコーディングが超簡単になりました!

class Singleton {
    static let sharedInstance: Singleton = Singleton()
    private init() {}
}

こういう説明で「なるほど!」ポンと手を叩く人は当然のように上級者プログラマーでしょう。

私のような初心者が読んでいるのであれば、この説明じゃ何が言いたいのか訳がわからないです。私もそうでした(^_^;)

Swiftを使ったデザインパターンの説明が身近にあったのですが、ここも私には難しかったです(T_T)

llcc.hatenablog.com

このブログで紹介されているオブジェクト指向における再利用のためのデザインパターンも購入してみました。しかし、ちんぷんかんぷんなんで、また後で再挑戦してみます。

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

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

ここまでで説明しているシングルトン・モデルのコーディングは単純化されすぎているので実際の使用法のイメージが掴めないんです。

では抽象的概念を理解のために少し具体的なコードで考えてみます。

class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

このクラスは一般的なクラスで人のデータ、といっても名前と年齢だけ持つクラスです。これを管理するクラスをシングルトン・パターンを使って考えてみます。SingletonPersonManagerクラスとしましょう。

class SingletonPersonManager {
    
    static let sharedInstance = SingletonPersonManager()

    var persons: [Person]
    // 何をどのように管理するのかシングルトンで管理するプロパティを加える。
    
    private init() {
        persons = []
    }
    // 管理するプロパティをここでイニシャライズする。
}

// 管理するプロパティをここでイニシャライズする。
}

これでタイプ・プロパティはグローバル定数になるので、このクラスで唯一無二なプロパティを保持できるということです。使い方にもルールがあります。必ずタイプ・プロパティで定数を宣言しなければなりません。playgroundを使って確認します。

let a = SingletonPersonManager.sharedInstance
// SingletonPersonManager
// playgorundで表示されています。
// グローバル定数としてSingletonPersonMangerクラスのインスタンス化
// しているということですね。

a.persons.append(Person(name: "山田", age: 8))
// [{name "山田", age 8}]
// 「a」が保持しているのは[Person]のデータなので
// appendメソッドを使って値を追加できます。

let b = SingletonPersonManager.sharedInstance
// もう一つ定数を作ります。
// しかしこれはグローバル定数をそのまま持つことになります。
// どちらかと言えばリンクのようなものです。

b.persons[0].name
// "山田"
// 既に値を持っています。

a.persons[0].name = "滝口"
// aを使ってname値を変更

b.persons[0].name
// "滝口"
// bでも変更されています。

実際の利用は、このSingletonPersonManagerクラスに外からアクセスできるメソッドを色々用意してグローバル・プロパティの値を制御します。

このSingletonクラスのイニシャライザに「pirvate」の枕詞が付いています。「private」の意味はクラス外からアクセスできないという事です*4。ということは「クラス内でしかイニシャライザは使えない」ということになります。「static let sharedInstance = SingletonPersonManager()」だけで利用可能で、まさしく一枚岩の構造を実現しています。

let c = SingletonPersonManager() // エラーになります。

2016年1月5日追記:クラスコード内にプライベートなイニシャライザを使っていますが、iOSプログラミングでは、できるだけイニシャライザを使わない方法が好まれます。シングルトン・パターンでも同様です。クロージャーを使って初期セットアップをする方法を紹介します。

class Singleton {
    static let sharedInstance: Singleton = {
        let instance = Singleton()
        // ここで初期値セットアップのコードを書きます。
        return instance
    }()
}

たぶん、この書き方がベストのような気がします!

シングルトン・パターンの実装

スターター・プロジェクトのナビゲーターエリアにはAPIグループのフォルダがあります。

f:id:yataiblue:20151203232918j:plain

シングルトンクラスはAPIとして使うので、ナビゲーターエリアで「BlueLibrary」が選ばれた状態で、メニューの「File -> New -> Group」を選んでフォルダーを作ります。ダブルクリックして「API」と名前を変えます。

HTTPClientクラス

スターター・プロジェクトのAPIフォルダーの中に「HTTPClient.swift」ファイルがあるので。白紙のクラスファイルを用意してコードを加えていきます。APIフォルダが選択された状態でメニューから「File -> New -> File...」を選んで、「iOS -> Source -> Swift File」を選択して、「HTTPClient」と同じ名前にして空クラスを作ります。デフォルトのすべてのコードを消して、次のコードに変更します*5

/*
 * Copyright (c) 2014 Razeware LLC
 *
 * Permission is hereby granted, free of charge,
 * to any person obtaining a copy
 * of this software and associated documentation
 * files (the "Software"), to deal
 * in the Software without restriction,
 * including without limitation the rights
 * to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell
 * copies of the Software, and to permit
 * persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this
 * permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS",
 * WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO
 * THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 * FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

import UIKit

class HTTPClient {
    
    func getRequest(url: String) -> (AnyObject) {
        return Data() as (AnyObject)
    }
    
    func postRequest(url: String, body: String) -> (AnyObject) {
        return Data() as (AnyObject)
    }
    
    func downloadImage(url: String) -> (UIImage?) {
        let aUrl = URL(string: url)
        do {
            let data = try Data(contentsOf: aUrl!)
            let image = UIImage(data: data)
            return image!
        } catch {
            print("error to get data")
            return nil
        }
    }
}

今のところこのファイルの内容に関して説明はしません*6

LibraryAPIクラス

同じようにAPIフォルダが選択された状態で、メニューから「File -> New -> File...」を選び、「iOS -> Source -> Cocoa Touch Class」を選択して、「LibrarlyAPI」と名前を付けて、subclassは「NSObject」を選択します。「Next」、「Create」を押していくと新しいクラス・ファイルができます。

次のシングルトン・コードを加えます。

import UIKit

class LibraryAPI: NSObject {
    
    static let sharedInstance = LibraryAPI()
    
    private override init(){}
    
}

NSObjet継承クラスなのでイニシャライザには「override」の枕詞が付きます。これが分からなければイニシャライザの勉強をもう少しする必要があります -> Swiftで遊ぼう! - 407 - Initializer イニシャライザ - Swiftで遊ぼう! on Hatena

たったこれだけですが、コンパイル(Cmd + B)できることを確かめます。

PersistencyLibraryクラス

次はAlbumクラスのデータを保持するクラスを用意します。

同じ「API」フォルダが選ばれている状態で、メニューから「File -> New -> File...」を選び、「iOS -> Source -> Cocoa Touch Class」を選択して、「PersistencyLibrary」と名前を付けて、subclassは「NSObject」を選択します。「Next」、「Create」を押していくと新しいクラス・ファイルができます。

このクラスでAlbumデータを管理するため1つだけ次のプロパティを持たせます。

private var albums = [Album]()

プライベートにしてクラス外からアクセスできないようにします。保守性を保つためです。
そしてイニシャライザでデモを読み込ませます。

override init() {
//Dummy list of albums
 let album1 = Album(title: "Let's Dance",
     artist: "David Bowie",
     genre: "Pop",
     coverUrl: "https://s3.amazonaws.com/
                 CoverProject/album/
                   album_david_bowie_lets_dance.png",
     year: "1980")
        
 let album2 = Album(title: "These Days",
     artist: "Bon Jovi",
     genre: "Pop",
     coverUrl: "https://s3.amazonaws.com/
                 CoverProject/album/
                   album_bon_jovi_these_days.png",
     year: "1994")
        
 let album3 = Album(title: "The Dream of the Blue Turtles",
     artist: "Sting",
     genre: "Pop",
     coverUrl: "https://s3.amazonaws.com/
                 CoverProject/album/
                   album_sting_dream_of_the_blue_turtles.png",
     year: "1985")
        
 let album4 = Album(title: "Rattle and Hum",
     artist: "U2",
     genre: "Pop",
     coverUrl: "https://s3.amazonaws.com/
                 CoverProject/
                   album/album_u2_rattle_and_hum.png",
     year: "1990")
        
 let album5 = Album(title: "Something to Remember",
     artist: "Madonna",
     genre: "Pop",
     coverUrl: "https://s3.amazonaws.com/
                 CoverProject/album/
                   album_madonna_something_to_remember.png",
     year: "2002")
        
 albums = [album1, album2, album3, album4, album5]
}

先ほど説明をしましたがデータの保守性を上げるためにalbumsにはアクセスできないのでアクセスができるようにメソッドを用意します。アルバムデータを「得る」「加える」「消す」を次のメソッドとして加えます。

    func getAlbums() -> [Album] {
        return albums
    }
    
    func add(album: Album, index: Int) {
        if (albums.count >= index) {
            albums.insert(album, at: index)
        } else {
            albums.append(album)
        }
    }
    
    
    func deleteAlbumAt(index: Int) {
        albums.remove(at: index)
    }

これで実装が終わりました。コンパイル(Cmd + B)ができることを確かめます。

シングルトン?

あれ? シングルトンは? 実はまだシングルトンの利用はできません(T_T)

シングルトン利用の前に、もう1つデザインパターンの理解が必要です。

Facadeデザインパターンです。私は日本語で読めなかったです(T_T)

Facadeって、ファカーデと思ったら「ファサード」でした。

まだシングルトンの実装が終わっていませんが、ファサードの解説に進みます。

GoFのデザインパターーんは23個あります。まだまだ道のりは長いですよ(^_^;)

次の記事です。
yataiblue.hatenablog.com

*1:2016年1月5日追記:シングルトン・パターン宣言法を加える、2015年12月4日改訂:デザインパターンの復習

*2:かなり昔に知り -> Swiftで遊ぼう! - 46 アプリを作ってみよう2 MVCモデル:Life-LOG OtherSide、かなり理解が進みました -> Swiftで遊ぼう! - 259 - HappinessViewControllerでコントール MVC - Swiftで遊ぼう! on Hatena

*3:ここの説明にあるstaticですね。Swiftで言えば「class var」ってとこですね。インスタンスを1つにするために「let」を使うというのもポイントです。

*4:アクセス・コントロールを理解するのも重要です -> Swiftで遊ぼう! - 460 - アクセス・コントロール(Access Control) - Swiftで遊ぼう! on Hatena

*5:Swift 3風に書き換えています

*6:ここで説明が入ります→Swiftで遊ぼう! - 443 - Swiftでデザインパターン 21 Observer(オブザーバー) - Swiftで遊ぼう! on Hatena