2015年3月24日火曜日

エラーメッセージ出力機能のライブラリ化

当ブログでは、いろいろな機能のライブラリ化を紹介しています。もちろん、ソースコードも公開無しながら。それらのコードの中では、エラー検出機能も含まれていて、検出時にはエラーメッセージを出力しています。通常は、println文を使って。

それでも構わないのですが、println文で出力したメッセージは、コンソールのログを見なければ確認できません。とくに、iPadの実機上で実行中のアプリなら、簡単には見れないのが欠点です。

そこで、エラーメッセージの出力方法を変更しようと考えました。コンソールにも一応出力しますが、その他として、Documentsフォルダ内のファイルにも書き出します。こうすると、以前に「ファイル閲覧機能のライブラリ化」で作った、Documentsフォルダ内のファイル閲覧機能で見ることが可能となります。閲覧機能を新たに作らなくても、エラーメッセージを見れるというわけです。

また、エラーメッセージの出力先を1つにまとめることで、間接参照を実現したことにもなります。println文以外の出力先を追加したり、別な出力先に変更したりが、1カ所の変更だけで可能です。また、エラーメッセージの内容に応じて、出力先を切り替える機能も、一カ所の変更で実現できます。つまり、今回の機能のライブラリ化は、エラーメッセージの出力方法を、間接参照で作ろうという意味でもあります。

 

いつものように最初は、作るべき機能の仕様を検討しましょう。実現方法としては、アプリ内のグローバル関数とします。いろいろなところから呼び出すので、簡単に呼び出せたほうが使いやすいでしょうから。この関数を入れるファイルは、UI部品を生成するグローバル関数が入っている「BP_Base.swift」としました。

大まかな機能は、次のようにします。呼び出し側からメッセージを受け取り、メッセージの先頭に現在の日付と時刻を追加して、コンソールと指定ファイルに出力します。既にファイルがあるか調べて、あればファイルの最後に追加し、なければファイルを新規で作ります。

時刻の文字列生成や、ファイルの読み書きは、すでに独自ライブラリとして作ったものがあります。でも、今回はそれを使いません。エラーメッセージの生成は、アプリの基本となる機能で、他のライブラリに依存するのは良くありません。依存すると、ライブラリの読み込み順序などにも影響され、余計な考慮が必要となります。まったく依存しないようにと、普通にコーディングしました。

 

将来の拡張も、最初から考慮すべき点です。今回の関数内でのエラーメッセージの扱い方が、1種類だけで済むとは限りません。将来的に、ファイルに出力しないメッセージとか、メールで送信するメッセージとか、新たな扱い方のメッセージが追加となる可能性もあります。どう処理するのか、呼び出し側で指定できるような機能の追加も必要になるでしょう。その場合、それまで作った呼び出し側のコードを変更しないように作っておくのがベストです。

最初に考えたのは、エラーメッセージの扱い方を示す番号を、Int型の引数として加える方法でした。最初はゼロを指定しておき、別な扱い方のメッセージが出てきたら、ゼロ以外の番号を指定するわけです。最初に作った呼び出し側は、全部がゼロを指定していますから、後から変更する必要はありません。変更が予想される機能の実現では、よく使われる方法でしょう。

でも、まったく変更しないときは、余計な引数が付いたままとなり、美しくありません。今回は、エラーメッセージが引数なので、文字列のデータ型です。ということは、いろいろな情報が含められるということです。もし新しい扱い方のメッセージを追加するときには、エラーメッセージの先頭に特殊な文字を入れることで対応可能です。たとえば、メールで送るエラーメッセージなら、メッセージの先頭を「@」にするとか。つまり、先頭の1文字か2文字を、コマンドとして利用するわけです。文字列型ならではの拡張方法です。

文字列の先頭にコマンドを入れる方法は、いろいろな応用が可能です。たとえば、先頭が「@」のとき、メールアドレスを続けるような仕様にして、メールアドレスの終了特殊文字も規定します。先頭「@」の次から、その終了特殊文字の前までが、メールアドレスの文字列として解釈され、終了特殊文字の後ろがエラーメッセ時として扱われるという仕様にも作れます。このように、引数付きのコマンドも実現可能なのです。まあ、実際に作るかは別な話ですけど。ともかく、何とかなる余地が残っていることが大切です。

 

いろいろと書きましたが、機能としては単純です。短時間で作れました。具体的には、次のようなSwiftコードになっています。

// エラーメッセージを出力するためのグローバル関数

let STR_FNAME_ERR = "AppErrMsg.txt"    // エラーメッセージのファイル名

func zSendErrMsgF(rMsg:String) {
    let iErrMsg: String = getStrDateNowF() + " " + rMsg
    println(iErrMsg)
    // エラーメッセージファイルへ出力(存在してなければ新規作成)
    var iStrWrite: String = ""    // 書き出す文字列
    let iPathDocs = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
    let iPathFile: String = iPathDocs.stringByAppendingPathComponent(STR_FNAME_ERR)
    let iFileManager: NSFileManager = NSFileManager.defaultManager()
    if iFileManager.fileExistsAtPath(iPathFile) {    // ファイルが存在したら、その内容を読み込む
        let iStrOld = NSString(contentsOfFile:iPathFile, encoding:NSUTF8StringEncoding, error:nil) as String
        iStrWrite = iStrOld
    }
    iStrWrite += iErrMsg + "\n"
    iStrWrite.writeToFile(iPathFile, atomically:true, encoding:NSUTF8StringEncoding, error:nil)
}
// 現在の日付時刻の文字列を返す
private func getStrDateNowF() -> String {
    let iDateFmtr = NSDateFormatter()
    iDateFmtr.locale = NSLocale(localeIdentifier:"ja_JP")
    iDateFmtr.dateFormat = "yyyy/MM/dd HH:mm:ss"
    return iDateFmtr.stringFromDate(NSDate())
}

見てすぐに分かるコードですが、簡単に説明します。

ファイルが存在するかは、きちんと調べています。読み込みが失敗したら存在しないという判断する方法は使っていません。存在したら読み込んで、新しいメッセージを追加します。メッセージごとに改行したいので、最後に改行文字を追加しています。

ファイルの読み書きでは、エラーチェックをしていません。エラーを発見したとしても、おそらく何もできないからです。エラーメッセージが消えるだけですし、普通の状態ではエラーが発生しないでしょうから、何もしなくて構わないと判断しました。

日付時刻の文字列を生成するgetStrDateNowF関数では、日付と時刻の桁が縦に揃うように、前ゼロ付きの書式を選びました。また、時刻の後ろに空白文字を入れるのですが、この関数内では入れずに、日付時刻を追加する行でわざわざ入れています。これには理由があります。エラーメッセージ出力の関数が拡張されるとき、日付時刻の文字列がどのように使われるのか不明です。文字列の最後に余計な空白付きで返ってくるのは、その空白が不要になったときに、この関数の変更が生じるでしょう。最後に空白を入れるのは、この関数の戻り値を使う側の都合であるため、使う側で付けるのが当然の作り方です。とても小さなことですが、このように考えて作ると、将来の余計な手間を増やさずに済みます。

 

関数名に関しても、少しだけ解説します。私の場合は、UI部品の生成などをグローバル関数として用意しています。ただし今までは、関数名を普通に付けていました。でも、グローバル関数と分かるような関数名を付けたほうが、後でメンテナンスするときに良いと思うようになりました。

そこで今回から、グローバル関数の名前は、先頭に「z」を付けることにしました。なぜ「g」ではなく「z」かという理由ですが、まず「g」はメソッド名の先頭文字に多く使われますから避けたいと考えました。逆に「z」は使われません。また、日付やファイル処理の機能をクラスとして用意し、そのグローバル変数の名前として、先頭に「z」を付けています。それと共通なら分かりやすいと考え、先頭に「z」を付けることとしました。この辺の考え方は、個人差が大きいと思います。

既存のグローバル関数は、今回のルールに従っていません。何かの機会に、同じルールで変更する予定です。

あと、動詞を何にするかで迷いました。とりあえず「Send」にしたのは、メッセージを送るという感じが、自分なりには動きに一番近いと思えたからです。これがベストという感じはしてませんが、とりあえず何か付けないといけないので。

 

使い方は、非常に簡単です。今までprintln文で書いていたものを、zSendErrMsgF関数を呼び出す形に変更するだけで済みます。言ってみれば、「println」の文字列を「zSendErrMsgF」の文字列で置き換えるだけです。次の例のように。

// エラーメッセージ出力の使用例
zSendErrMsgF("ERROR:GD_PT:商品テーブルの読み込みに失敗しました。")

極端な話、ソースコードの文字列検索&置き換え機能を使い、すべての「println」文字列を「zSendErrMsgF」で一括置き換えしても構わないぐらいです。まあ、実際には行ないませんけど。もし置き換え機能を使うとしたら、1つ1つ確認しながらですね。

 

今回の機能を作ったことで、今後のエラーメッセージ処理は、これを常に使うことになりそうです。次回以降の新作アプリとアプリ更新では、すべてのエラーメッセージをファイルへも出力するでしょう。

Documentsフォルダ内のファイルを削除しない限り、エラーメッセージは消えずに追加され続けますから、時間が経過した過去のエラーも後から見れます。これが一番大事な点で、エラーを見逃さない仕組みが用意できたとも言えるでしょう。これが、作った一番の理由かもしれません。

また何か思い付いたら、自分の独自ライブラリに機能を追加したいと思います。

 

(使用開発ツール:Xcode 6.2, SDK iOS 8.1)

0 件のコメント:

コメントを投稿