2015年3月31日火曜日

エラーメッセージ補助機能のライブラリ化

エラーメッセージ出力機能を独自ライブラリに加えたので、エラーメッセージの内容を改めて考えてみました。今まで作ったときに考えていたのは、エラーを知らせることだけです。今後は、時間が経過してから見た場合も考慮して、もっと良い内容でメッセージが出せないのか、考え直してみたというわけです。

考えた結果、エラーメッセージの補助機能を独自ライブラリに加えることにしました。エラーメッセージを後から見るときだけでなく、開発時のデバッグ時にも使える補助機能が作れました。単純な機能ですが、ここで紹介します。

 

先日作成した、エラーメッセージ出力機能のライブラリ化では、メッセージの頭に日付と時刻を加えます。これで、発生した時期が特定できるようになりました。

また、どのアプリが出しているのかの情報は、必要ありません。そのアプリのDocumentsフォルダへ保存するのですから、どのアプリかは明らかです。

 

エラーメッセージに、上記以外の情報は何か必要でしょうか。

そう考えて思い浮かんだのが、過去に作ったオンラインシステムでのトラブル対処です。大量のデータを扱うシステムだったので、エラーが発生したとき、どのデータで発生しているのか、なかなか特定できませんでした。特定するために、エラーメッセージを改良して、どのデータで発生したのか出力するようにしました。その結果、エラーの発生したデータが特定でき、システムの改良が容易になりました。

時間が経過してから見る、iPadアプリのエラーメッセージにも、同じような要望があると思います。どのような状況で発生したのか、どのデータで発生したのか、エラーの種類によっては詳しく知りたいはずです。その要望を満たすような機能があれば、非常に有益だと言えるでしょう。

 

そこで、状況やデータを示せるようなエラーメッセージ補助機能を、独自ライブラリに追加することにしました。状況やデータの値に関係なく、メッセージへ情報を簡単に加えられる機能です。どの情報を加えるかは、その補助機能を使う側が決めます。

基本として、メッセージに追加したい情報を設定する機能と、設定した情報をエラーメッセージに加える機能が必要です。これも、エラーメッセージ出力機能と同じように、アプリのどこからでも呼び出しやすいことが大事です。そのため、グローバル関数として作ることにしました。作る場所は、前回のエラーメッセージ出力機能と同じ「BP_Base.swift」です。

メッセージへ入れる情報に合わせて、データ構造を考える必要があります。今回は、何層かの階層として扱えたほうが良いと考えました。たとえば、1番目の階層には、画面(View)の名前を入れます。次の階層には、画面に含まれる処理名を入れます。さらに次の階層には、キーとなるデータの値を入れます。同様に、データの値を4階層ほど用意します。

4階層も付けたのは、4つの階層が必要になるという意味よりも、同じ階層で2つ以上の値を使うかもしれないと考えたからです。階層関係があるので使い方は制限されますが、階層の余裕を別な形で使えるようにとの配慮です。

階層で大事なのは、自動クリアー機能です。もし画面が変わったら、それ以下の階層に設定された値は意味がなくなります。そのため、第1階層の値が変更されたら、第2階層以下の値は自動クリアーします。同様に、第2階層の値が変更されたら、第3階層以下の値を自動クリアーします。このように、全ての階層で、下の階層を自動クリアーする機能を組み込みます。

 

完全な階層構造の値だけだと、より自由な使い方はできません。そこで、階層関係とは別の値を入れる場所も2つ用意しました。

ただし、こちらも自動クリアー機能がないと、使いやすくはなりません。そこで、1つの値は、第1階層の設定でクリアーさせます。もう1つの値は、第1階層および第2階層の設定でクリアーさせます。第2階層だけのクリアーでない理由は、すべての設定をクリアーする機能が必要で、それが第1階層の役目だと考えたからです。

 

こうして作ったのが、以下のグローバル関数です。実際のSwiftコードは次のとおりです。

// エラーメッセージの補助機能:画面やデータを特定するために、処理中の画面やデータを記録に残す
private var procKeyL1: String = ""  // 通常はView名を設定(V=XXX)
private var procKeyL2: String = ""  // 通常は処理の種類(P=XXX)
private var procKeyL3: String = ""  // 通常は処理データのレベル1
private var procKeyL4: String = ""  // 通常は処理データのレベル2
private var procKeyL5: String = ""  // 通常は処理データのレベル3
private var procKeyL6: String = ""  // 通常は処理データのレベル4
private var procKeyA1: String = ""  // 通常は処理データの属性(L1設定でクリアー)
private var procKeyA2: String = ""  // 通常は処理データの属性(L1設定とL2設定でクリアー)

// 処理中のキーを設定する
func zSetProcKeyL1F(rStr:String) {
    procKeyL1 = rStr
    zSetProcKeyL2F("")
    zSetProcKeyA1F("")
    zSetProcKeyA2F("")
}
func zSetProcKeyL2F(rStr:String) {
    procKeyL2 = rStr
    zSetProcKeyL3F("")
    zSetProcKeyA2F("")
}
func zSetProcKeyL3F(rStr:String) {
    procKeyL3 = rStr
    zSetProcKeyL4F("")
}
func zSetProcKeyL4F(rStr:String) {
    procKeyL4 = rStr
    zSetProcKeyL5F("")
}
func zSetProcKeyL5F(rStr:String) {
    procKeyL5 = rStr
    zSetProcKeyL6F("")
}
func zSetProcKeyL6F(rStr:String) {
    procKeyL6 = rStr
}
func zSetProcKeyA1F(rStr:String) {
    procKeyA1 = rStr
}
func zSetProcKeyA2F(rStr:String) {
    procKeyA2 = rStr
}

// 処理中のキーを得る
func zGetProcKeyF() -> String {
    let STR_DELIMIT: String = ","
    var iStr: String = procKeyL1
    if procKeyL2 != "" { iStr += STR_DELIMIT + procKeyL2 }
    if procKeyL3 != "" { iStr += STR_DELIMIT + procKeyL3 }
    if procKeyL4 != "" { iStr += STR_DELIMIT + procKeyL4 }
    if procKeyL5 != "" { iStr += STR_DELIMIT + procKeyL5 }
    if procKeyL6 != "" { iStr += STR_DELIMIT + procKeyL6 }
    if procKeyA1 != "" { iStr += STR_DELIMIT + procKeyA1 }
    if procKeyA2 != "" { iStr += STR_DELIMIT + procKeyA2 }
    if iStr == "" {
        return ""
    } else {
        return "(" + iStr + ")"
    }
}

難しい処理はしてませんので、見てのとおりの機能です。いつのもように要点だけ解説します。

エラーメッセージに表示する値(キー)は、8つの変数に入れます。6階層に作られたL1〜L6と、階層ではないA1とA2の8変数です。それぞれに設定関数が用意され、値を設定すると同時に、関連する自動クリアー機能が含まれています。値のクリアー機能は用意せず、空の文字列を入れることでクリアーと同じの役目をします。

階層構造の値の設定では、下の階層をクリアー(空文字の設定)を順番に呼び出す形で作っています。このように作っておくと、階層を増やすときの変更箇所が少なくて済みます。

階層とは関係ない2つの値(A1とA2)の設定では、クリアーする処理が含まれません。下の階層を持ってないからです。ただし、これらの値を自動クリアーする必要があるため、第1と第2の階層の設定処理で、それぞれの階層に応じたクリアー処理を含めています。

最後は、これらキーの値を文字列として取り出す処理(zGetProcKeyF関数)です。順番に追加するだけですが、追加する値が空でないときだけ、区切り文字を間に入れて追加しています。処理を良く見ると分かるのですが、第1階層が空で、第2階層以下のどれかが空でない場合は、最初に区切り文字が付いてしまいます。これに対処するとソースが汚くなるのと、第1階層を使わない状況は想定してないので、この形にしました。

この補助機能を使わない場合は、8つの変数が全て空です。その場合に、中身の入ってない括弧が表示されると醜いので、最後の処理では、中身が入っているときだけ括弧で囲んでいます。

 

これでエラーメッセージの補助機能は作り終わりました。ただし、このままだと、エラーメッセージに毎回追加する処理が必要です。そこで、前回紹介したエラーメッセージの処理を少し変更して、キーの値が自動的に追加するようにしました。以下のように、たった1行だけの変更です。

// 変更前
let iErrMsg: String = getStrDateNowF() + " " + rMsg
// 変更後
let iErrMsg: String = getStrDateNowF() + " " + rMsg + zGetProcKeyF()

前述のように、補助機能を使ってないときは空の文字列となるので、キー値の追加されない(つまり以前と同じ)エラーメッセージが出力されます。

 

すべて作り終わったので、使い方を簡単に紹介しましょう。

まずは、基本的なルールから。複数の値を一緒に表示するため、それぞれが何の値を表示しているのか理解しやすいように、形式を決めます。画面であればViewを意味するVをキー名として、「V=Menu」という形式で値を入れます。このように、キー名とキー値をペアにして等号でつなげた形を、私の使用ルールとしました。

第1階層のキーには、どの画面を動かしているかの情報を入れます。編集画面の場合は、次のような使い方になります。

// 編集画面に入った直後に
zSetProcKeyL1F("V=Edit")  // キーの設定
// この後に画面生成処理を続ける
...

// 画面を消す処理の最後にも
...
zSetProcKeyL1F("")  // キーのクリアー

この例のように、画面を意味するキー設定だけでなく、キー設定をクリアーする処理も最後に加えています。この形にする理由が2つあります。1つは、画面に関わる処理の範囲を明確に示し、画面外の処理で間違った表示をさせないためです。もう1つは、キー設定を全部の画面に付けるとは限らないので、クリアー処理を付けないと、別な画面で間違ったキーが設定されたままになってしまいます。全ての画面に付ければ、クリアー処理がなくても構わないのですが、他に依存するような使い方は好ましいとは言えません。トラブルの元になります。そのため、設定とクリアーの両方を付けるがベストな使い方と言えます。

このようなキー設定により、エラーメッセージが出されたとき、どの画面で動いているのか明らかになります。複数の画面で使われているような処理では、どの画面で使われたときに発生したエラーなのか分かると、原因の究明に大いに役立つでしょう。

 

続いて、第2階層の処理です。ボタンをタップしたとき呼ばれる関数の例を挙げてみました。

// 追加ボタンをタップしたときの処理
func AddDataF(rSender:UIButton) {
zSetProcKeyL2F("P=AddBtn")  // キーの設定
...
... // ここにタップ時の処理を入れる
...
zSetProcKeyL2F("")  // キーのクリアー
}

こちらでも画面の場合と同じように、キーのクリアーを入れています。また、キーの形式もルールに則り、「P=AddBtn」としています。

処理の途中でエラーメッセージが出されると、画面のキーとともに、処理の種類のキーも一緒に表示されます。どの画面の、どの処理でエラーが発生したのか判明すれば、原因の絞り込みがさらに容易になるでしょう。

 

これ以下の階層でも、基本的な使い方は同じです。処理する値ごとにキーを設定すれば、どの値の処理中にエラーが発生したのか、明らかにできます。たとえば、商品idごとにキーを設定するなら、値の形式を「ProdID=XXXXX」として、「XXXXX」の部分に商品idの値を入れることになります。キーの名前を極端に短くしないことが、後から見て理解しやすいコツでしょう。

原因究明に役立つのは、データのキーとなる値だけではありません。何件目のデータを処理しているのか、全部で何件のデータを処理しているのかといった、処理件数の関わる情報も役立つことがあります。その場合は「cnt=XXXXX」と入れることになります。過去のデバッグ経験と照らし合わせて、ヒントになりそうな情報を入れるのも上手な使い方と言えそうです。

階層とは関係のないA1とA2は、少し違う使い方が可能です。入力画面であれば、入力モードを記録するとか、入力エラーの有無を記録するとか、階層に関係のない独立した値なら何にでも使えます。エラー発生に関係しそうな情報を入れるのがコツでしょう。

 

以上のように使った場合、エラーメッセージを出すグローバル関数zSendErrMsgFの使い方は以前と同じですが、エラーメッセージの最後に、補助機能で設定した情報が括弧付きで加わります。具体的には、次のようになります。

// エラーメッセージを出力するソースコードは、以前と同じまま
zSendErrMsgF("ERROR:ZF_WF:ファイル書き出しが失敗しました。")
// エラーメッセージの出力例
2015/03/31 15:23:47 ERROR:ZF_WF:ファイル書き出しが失敗しました。(V=Edit,P=AddBtn,ProdID=S015037)

zSendErrMsgF関数により、前側に日付と時刻が、後ろ側に補助機能による付加情報が加わってメッセージが出力されます。もし補助機能を使っていなければ、後ろ側の括弧の部分が、まったく表示されません。もし一部の画面だけで使っている場合は、使ってない画面(補助機能が設定されてない画面)でも、後ろ側の括弧の部分が表示されません。あくまで、補助機能が設定されているときだけ、設定された情報を追加します。

 

当然のことですが、エラーメッセージの出る確率は、アプリの信頼性が高いほど少なくなります。作り手が優秀な場合、エラーメッセージはほとんど出ないはずです。

というわけなので、キーを細かく設定しても、無駄な処理を動かしているだけになります。実際には、多量のデータを扱う箇所に入れない限り、処理自体は軽いので、入れておいてもバッテリー消耗に影響がないでしょう。保険のために入れておくという感じでしょうか。

加えて、多少ですが、入れる手間も増えます。現実的には、全部の画面に入れるのではなく、大事な画面を選び、その中の重要な処理にだけ入れるというのが現実的だと思います。その場合、画面名と処理名は必須として、データのキーとなる値を入れるかどうかは悩みます。こちらも、難しい処理をしているときにだけ、念のために入れておくというのが現実的でしょうか。

最初入れてなくて、エラーが発見されたときに、入れたバージョンのアプリと入れ替えて様子を見るという使い方も可能です。臨機応変に活用するのも現実的な使い方でしょう。

 

ここまで、エラーメッセージの補助機能として紹介してきました。しかし、それ以外にも有効な使い方があります。開発中のデバッグです。

まず、この補助機能を、開発当初から入れておきます。主な画面と主な処理で。加えて、集中的にデバッグしたい処理では、データのキーとなる値を設定する処理も追加します。

面白いのは、ここからです。エラーメッセージが出るのを待つのではなく、条件文付きのprintln文を使って、zGetProcKeyF関数を呼び出すのです。すると、条件を満たしたときにメッセージが出て、実行中のキーの値が表示できます。println文を入れた箇所からは参照が困難な値も、この補助機能を使えば表示できるというわけです。

 

今回は、より良いエラーメッセージを考えている中で、思い付いた機能を作ってみました。この補助機能を使えば、発生状況が特定しづらいエラーの状況判明に、かなり役立つと思います。次に作るアプリや、既存アプリの改良時に、組み込んでみようと考えています。どのレベルまで入れるのか悩みながら、上手な使い方を模索してみます。

 

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

0 件のコメント:

コメントを投稿