2015年6月14日日曜日

ファイルの読み書きも間接参照で(本編+使用例編)

ファイルの読み書きを間接参照で作る話の続きです。前回は作成理由を書いたので、いよいよSwiftコードを紹介します。

 

その前に、ファイルを読み書きするライブラリの話を少しだけ。

今回のリンククラスを作る前に、ファイルの読み書きライブラリを更新しました。今まではクラスとして作っていたのですが、よく考えると、値を保持する必要はありません。ということは、UI部品の生成関数と同じく、グローバル関数で作ったほうが適しています。グローバル関数なら、インスタンス化が不要となり、何の準備もせずに使い始められますから。また、アプリの初期処理で、インスタンスを生成する順番を気にせずに済みます。

あとエラー処理に関する話も少しだけ。ファイルの読み書きでは、NSErrorを指定できるメソッドの場合、必ず指定しています。処理後にNSErrorがnilでなければ、内部的にエラーメッセージを出して、そのまま処理を終わらせます。読んだ結果を返すメソッドでは、nilが返ることになります。

このような形で作ってあるため、この読み書きライブラリを使う側は、基本的に内部向けのエラーメッセージを出しません。ユーザーに何かを伝える必要があるときだけ、画面上にメッセージを出します。

具体的なSwiftコードも1つだけ。アプリ内のリソースから、テキストファイルを読み込む関数です。

// アプリ内リソースから、テキストファイルを読み込む
func zFReadFileTextResF(rFName:String, rFNameSuf:String) -> String? {
    let iPathFile: String? = NSBundle.mainBundle().pathForResource(rFName, ofType:rFNameSuf)
    if iPathFile == nil { zSendErrMsgF("ERROR:BPF_RFR:パスの生成に失敗:ファイル名を確認"); return nil }
    var iErr: NSError? = nil
    var iNSStr: NSString? = NSString(contentsOfFile:iPathFile!, encoding:NSUTF8StringEncoding, error:&iErr)
    if iErr != nil { zSendErrMsgF("ERROR:BPF_RFR:NSError=\(iErr)") }
    return iNSStr as String?
}

見てのとおり、NSErrorに何かが入っていると、内部的なエラーメッセージを出します。NSErrorの内容もテキスト化して出力されるので、エラーの原因究明に役立つはずです。

この関数では、ファイルを含んだパスを生成する箇所でも、エラーチェックを入れています。これは普通のファイルでは不要ですが、アプリ内のリソースから読む場合には必須となります。もし存在しないファイル名を指定すると、パスの値がnilになり、続きの読み込み処理でクラッシュするからです。クラッシュを避け、原因を知らせるために入れています。

他の読み書き関数も、似たような作り方を採用しました。また、ディレクトリとファイル名を指定してパスを生成する処理などは、共通の関数として別に用意し、いくつもの読み書き関数で使っています。

このライブラリの関数を使ったとき、処理が成功したかどうかは、戻り値を見て判定します。読み込み関数では、読み込んだ結果がnilなら処理の失敗です。書き出し関数では、戻り値のBool値がfalseなら処理の失敗です。

 

ようやく本題に入ります。まずは、テキストを扱うリンククラスから。次のようなSwiftコードになりました。

// ============================== 文字列をファイルへ読み書き
class ZSLText2File {
    var cDir: ZFDir = .Docs              // 仮の値
    var cFileName: String = "dummy.txt"
    //初期化
    init(_ rDir:ZFDir, _ rFileName:String) {
        cDir = rDir
        cFileName = rFileName
    }
    // アプリ内のリソースから
    func loadDefaultF() -> String? {
        let iFileNameN: String = cFileName.stringByDeletingPathExtension
        let iFileNameS: String = cFileName.pathExtension
        let iStr: String? = zFReadFileTextResF(iFileNameN, iFileNameS)
        return iStr
    }
    // 指定されたディレクトリから読み込む
    func loadF() -> String? {
        let iStr: String? = zFReadFileTextF(cDir, cFileName)
        return iStr
    }
    // 指定されたディレクトリへ書き出す
    func saveF(rStrData:String) -> Bool {
        let iBool: Bool = zFWriteFileTextF(cDir, cFileName, rStrData)
        return iBool
    }
}

いつものように、要点だけ簡単に説明します。

初期化の処理で、ディレクトリとファイル名を指定します。これで保存先が決まりますから、このインスタンスを使う限り、保存先を意識する必要がなくなります。

メソッドは3つだけです。デフォルト値のファイルは、アプリ内のリソースに入れますから、そこから読むグローバル関数を呼び出します。残りの読み書きでは、初期化で設定したディレクトリとファイル名を指定します。

どのメソッドでも、ファイル読み書き関数の戻り値を、そのまま返しています。そのため、ファイル読み書き関数と同じ方法で、処理の失敗を呼び出し側で判定できます。

ディレクトリの指定は、好きな値を使えるわけではありません。ファイルの読み書きライブラリで用意した、列挙型の値を使っています。「.Docs」はDocumentsフォルダを意味しています。このクラスを初期化する際にも、同じ列挙型の値を使います。

 

続いて、NSDictionaryのデータを扱うリンククラスです。次のようなSwiftコードになりました。

// ============================== NSDictionaryをファイルへ読み書き
class ZSLDic2File {
    var cDir: ZFDir = .Docs                    // 仮の値
    var cFileName: String = "dummy.plist"
    //初期化
    init(_ rDir:ZFDir, _ rFileName:String) {
        cDir = rDir
        cFileName = rFileName
    }
    // アプリ内のリソースから
    func loadDefaultF() -> NSDictionary? {
        let iFileNameN: String = cFileName.stringByDeletingPathExtension
        let iFileNameS: String = cFileName.pathExtension
        let iNSDic: NSDictionary? = zFReadFileDicResF(iFileNameN, iFileNameS)
        return iNSDic
    }
    // 指定されたディレクトリから読み込む
    func loadF() -> NSDictionary? {
        let iNSDic: NSDictionary? = zFReadFileDicF(cDir, cFileName)
        return iNSDic
    }
    // 指定されたディレクトリへ書き出す
    func saveF(rNSDic:NSDictionary) -> Bool {
        let iBool: Bool = zFWriteFileDicF(cDir, cFileName, rNSDic)
        return iBool
    }
}

見てのとおり、文字列のリンククラスとほぼ同じです。扱っているデータがNSDictionaryに変わっている点と、ファイルを読み書きする関数がNSDictionary用に入れ替わっている点だけが違います。

つまり、もっとも異なるファイルへの読み書き部分が、ファイル関係のライブラリに入っているので、それを呼び出す側は関数名が違うだけになります。結果としてリンククラスは、保存場所やファイル名を保持し、間接参照を担当するだけになっています。まさに目的どおりの機能です。

 

本題とは関係ないですが、このソースコードを見て、return文の書き方に無駄が多いと感じた人もいるのではないかと思います。iNSDicやiBoolという途中の変数を使わず、return文に関数を直接書いたほうが、ソースコードが短くなると。

この途中の変数は、あえて入れています。関数の戻り値のデータ型を、ソースコード上で明示的に記述したいからです。データ型の種類だけでなく、Optional型の有無まで含めて、何のデータ型で返されるのかを示しています。そのため、関数から返されるデータ型で書くのが決まりです。

背景となる考え方としては、データ型を強く意識したプログラミングをしたいのと、忘れたころに見ても思い出しやすく作りたいからです。関数の戻り値のデータ型が何かは、使う前に調べてますから、書いている段階では覚えています。でも少しでも時間が経つと、簡単に忘れてしまいます。この例のように、関数を使う際に一時的な変数を用意して、関数の戻り値と同じデータ型にすると、戻り値のデータ型を明示する効果があります。必ずletの変数にして、戻り値と同じデータ型にするのが必須条件です。

戻り値のデータ型が明確に分かると、データ型を強く意識したプログラミングが容易になります。Optrional型かどうか、キャスト可能かどうか、自動変換してくれるかどうかなど、きちんと作るための基礎情報が揃うからです。

自分自身が数年後にソースコードを見たときも、データ型が分かることで、処理の細かな点まで理解しやすくなります。当然、私以外の人がメンテするときも、同様に理解しやすいでしょう。

この辺のことは、あくまで私の考え方ですから、他人に勧める気はありません。

 

話を本題に戻しましょう。残りのリンククラスは、NSArrayとバイナリー(NSData)です。これらも、紹介した2つのリンククラスと非常に似ていますから、掲載は省略します。

どのリンククラスも、非常に単純なクラスに仕上がっています。もともと間接参照を実現するためのクラスですから、当然ですね。

 

では続いて、使用例を紹介しましょう。とても簡単です。iOS実験専用アプリでテストしたコードから一部を抜き出し、組み合わせて貼り付けます。

// アプリの初期処理内でインスタンス生成
let zData2Storage = ZSLText2File(.Docs, "test_data.txt")
let zPref2Storage = ZSLDic2File(.Docs, "test_pref.plist")

// アプリの中心処理内で使う(読み込み)
var iStrData: String = ...
if let iStr: String = zData2Storage.loadF() {
    // 正常に読み込めたときの処理
    iStrData = iStr
} else {
    // 読み込みが失敗したときの処理
    setMsgF("エラー:保存データの読み込みに失敗しました。")
    ...
}

// アプリの中心処理内で使う(書き出し)
let iResult: Bool = zPref2Storage.saveF(iNSDic)
if iResult {
    // 正常に書き出せたときの処理
    ...
} else {
    // 書き出しが失敗したときの処理
    setMsgF("エラー:データの保存に失敗しました。")
    ...
}

まず最初に、アプリの初期処理内でリンククラスのインスタンスを生成します。このとき、保存場所のディレクトリとファイル名を設定します。あとは必要に応じて、これらのインスタンスにロードまたはセーブのメソッドを実行します。

ロードの際には、正常に読み込めたかを確認します。nilが返ると失敗ですから、if let文を使えば切り分けが簡単です。

セーブの際にも、戻り値で成功と失敗を判定します。falseが返ると失敗なので、ユーザーにメッセージを出してから、失敗時の処理を続けます。

 

以上の使用例は、単純に使う場合です。もっと凝った作り方をする際の、拡張方法も簡単にですが取り上げてみます。

まずは、別な保存先へ切り替える場合です。今回のリンククラスと同じメソッドを持ったクラスを用意して、リンククラスを新しいクラスに置き換えるだけです。変数名が同じままなら、変数のデータ型を新しいリンククラス名に置き換えて、インスタンス生成の箇所を直すだけです。

このような変更を事前に予想し、リンククラスの変数名は、ファイルに関係ないものにしたいものです。「...2File」よりも「...2Storage」のほうが適しています。

次に、複数の保存先へ保存する場合も考えてみましょう。単純に切り替えるなら、同じメソッドの別なリンククラスを用意するでしょう。同じように、複数へ保存するリンククラスを作るのですが、すでにファイルへ保存するリンククラスがあります。同じコードを重複して書くのは好ましくないので、ファイルへのリンククラスのサブクラスを作って、2番目の保存先へ保存する処理だけ加えます。そのサブクラスへ切り替えるというわけです。

2つの保存先といっても、保存する先が2箇所だけで、ロードするときは片方だけでしょう。だとすると、上書きするのはセーブのメソッドだけになります。もしロードも2つから選ぶ場合は、既存のロードはメインの保存先としてそのまま残し、サブとなる保存先からロードするメッソドを追加する形が良いと思います。

 

一応の使い方を紹介しましたが、実際のアプリで使う際には、別な考慮点があります。それは、ファイルへ保存する情報を扱っているクラスとの関係です。

一般的な作り方として、情報をファイルへ保存するのであれば、その情報を扱っているクラスが存在します。MVCの考え方で言うと、Mのモデルに相当するクラスのはずです。保存するファイルごとに、関係するモデルのクラスがあるので、それぞれのファイルはどこかのモデルクラスと関連付けられます。

今回のリンククラスでは、インスタンスを生成する部分のコードに、ディレクトリとファイル名が含まれています。それをモデルクラスに入れてしまうと、モデルクラスは保存先情報を持ってしまいます。別な保存先に切り替えるとき、モデルクラス内のインスタンス生成コード部分で、ディレクトリとファイル名に相当する情報を書き換える必要が生じます。良い使い方とは言えません。

モデルクラスの役割は、ファイルへ保存するデータを生成する部分です。どこへ保存するかは含まず、保存する処理へ保存データを渡すだけなのが基本でしょう。

だとしたら、保存データを生成して渡すgetメソッドを作る方法が、真っ先に考えられます。アプリ全体をコントロールしている処理(中央処理と呼びましょうかね)が、モデルクラスから保存データを受け取り、リンククラスへ渡して保存します。リンククラスのインスタンス生成も、当然ながら、中央処理が担当します。

ロードも同様で、中央処理がリンククラス経由でロードし、モデルクラスに渡します。つまり、モデルクラス側では、ロードした出たを受け取るsetメソッドも必要になります。

モデルクラスが扱っている情報が複雑な場合は、保存ファイルが複数になることもあるでしょう。しかも、保存ファイルのデータ形式が同じとは限りません。今回用意した4種類の中で、複数のデータ形式を使うとかです。そうなると、モデルクラスで用意する保存データ用メソッドは、データ形式の種類分だけ増えることになります。美しくない方向へ、どんどん向かっているような気がします。

 

いろいろと悩みましたが、私が考えたベストは、次のような方法です。

まず、リンククラスのインスタンス生成は、中央処理が担当します。これによりモデルクラスは、保存先の情報を持たなくて済みます。

続いて大事な点ですが、モデルクラスと中央処理は、保存データの受け渡しをしません。保存する処理を実行するのは、モデルクラスです。中央処理は、モデルクラスが用意した保存メソッドを呼び出すだけで、呼び出されたモデルクラスが、リンククラスを呼び出して保存します。

では、モデルクラスとリンククラスの関係はどうなるのでしょうか。モデルクラスでは、中央処理が生成したリンククラスのインスタンスを、受け取るためのメソッドを用意します。つまり、setメソッドです。モデルクラスは、受け取ったリンククラスのインスタンスに対して、生成した保存データを渡して保存させたり、ロードしたデータを受け取って、自分自身の内部データを一括更新したりします。

このような構造だと、モデルクラスはリンククラスに依存します。もしリンククラスの仕様が変更になれば、モデルクラスも変更しなければなりません。ただし、リンククラスは非常に単純なので、仕様変更が発生する可能性はほぼゼロでしょう。つまり、実用上は、依存している点が問題にならないということです。

この方法には、別な良い点もあります。モデルクラスは、保存先のリンククラスを設定するメソッドを持っています。そのおかげで、保存先となるリンククラスを簡単に切り替える機能を持つことになるのです。それによって、複数の保存先へ保存する処理も作りやすくなります。

たとえば、次のような方法で。中央処理が最初に、保存先となるリンククラスのインスタンスを複数用意します。複数へ保存したいときは、最初のリンククラスをモデルに設定してから、保存メソッドを呼び出します。続いて、2番目のリンククラスを、同じモデルクラスに設定し、その後に保存メソッドを再び呼び出します。このように実行すれば、現状のリンククラスのままで(リンククラスを複数保存へ対応させずに)、複数の保存先へ保存する処理が簡単に作れます。

1つのモデルクラスで、複数のファイルを保存する場合も、少し考えてみましょう。複数のファイルですから、リンククラスのインスタンスも複数生成し、それぞれをモデルクラスに設定します。ただし、保存やロードはまとめて行って構わないケースがほとんどでしょうから、保存やロードのメソッドは1つだけにまとめることになります。この形だと、メソッド数が増えすぎて困ることもありません。

 

以上のような形で作るとすると、すぐに思い付くのは、リンククラスを扱う機能だけプロトコルにできないかということでしょう。当然、考えました。でも、すぐに諦めました。

今回のリンククラスは、とりあえず作っただけでも4種類のデータ形式があります。それに加えて、1つのモデルクラスが複数のデータを保存することもあるでしょう。それらが同じデータ形式とは限りません。つまり、何個を保存するのか、それぞれどんな形式なのか、先に問い合わせて、その回答内容に合った保存命令を出す必要があります。

プロトコル作り以上に、そのプロトコルで保存命令を出す側の処理が、かなり複雑になりそうです。やっぱり、それぞれのモデルクラスごとに適したメソッドを用意して、それに合わせて呼び出すのが現実的と言えそうです。もちろん、メソッドの形式は統一しますが。

 

もう1点、リンククラスのインスタンスの管理も、アプリ側での作り方で考慮すべき点です。

リンククラスのインスタンスは、どれかのモデルクラスへ関連付けられます。モデルクラスもインスタンスを生成しますから、それと同じ場所で生成する方法もあるでしょう。

しかし私は、リンククラスのインスタンスだけ、まとめて生成する方法が好きです。アプリの初期化処理の中で、すべての保存ファイル用のインスタンスを生成し、そのコードを一箇所にまとめて書きます。まとめて書くことで、どんなファイルを使っているのか、一目瞭然になるからです。

また一箇所にまとめると、一括バックアップなどの処理が作りやすくなります。リンククラスのインスタンスを並べ、それぞれのロードメソッドで保存データを受け取り、別な場所へ一括保存するといったバックアップ処理です。ファイルだけをまとめて管理しやすいので、こうした方法が好きです。

 

以上で説明は終わりです。後半のほうは、説明だけになってしまいましたので、要点だけでも読み取ってください。

ファイルの読み書きを間接参照で作るという狙いで、今回のようなリンククラスを作りました。間接参照の実現方法は、人によってつくり方が違うと思います。もし今回の考え方に興味を持った場合は、自分なりの間接参照を設計してみたらいかがでしょう。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)

0 件のコメント:

コメントを投稿