2015年6月27日土曜日

共通モデルのライブラリ化を試作(本編2)

共通で使えるモデルクラスをライブラリ化する第1段として、記録カードのモデルクラスを作る話の続きです。Swiftコードの紹介を続けます。

レコート関係メソッドが済んだので、項目関係のメソッドを取り上げます。

まずは、項目の値を取得するgetメソッドから。コンパイラのデータ型検査の機能を生かすために、データ型ごとに用意します。今は4つのデータ型を使ってますから、メソッドも4つになります。次のようなSwiftコードになりました。

// ======================================== 値の取得と設定
// 値の取得
func getDataIntF(rRecIdx:Int, _ rFldIdx:Int) -> Int {
    if cAryType[rFldIdx] != .ZmInt { zSendErrMsgF("ERROR:ZMR_GDI:項目定義のデータ型がIntでない"); return -999 }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDI:レコード位置が範囲外"); return -999 }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iInt: Int = cAryDataInt[iDataIdx][rRecIdx]
    return iInt
}
func getDataFloatF(rRecIdx:Int, _ rFldIdx:Int) -> Float {
    if cAryType[rFldIdx] != .ZmFloat { zSendErrMsgF("ERROR:ZMR_GDF:項目定義のデータ型がFloatでない"); return -999.9 }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDF:レコード位置が範囲外"); return -999.9 }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iFloat: Float = cAryDataFloat[iDataIdx][rRecIdx]
    return iFloat
}
func getDataBoolF(rRecIdx:Int, _ rFldIdx:Int) -> Bool {
    if cAryType[rFldIdx] != .ZmBool { zSendErrMsgF("ERROR:ZMR_GDB:項目定義のデータ型がBoolでない"); return false }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDB:レコード位置が範囲外"); return false }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iBool: Bool = cAryDataBool[iDataIdx][rRecIdx]
    return iBool
}
func getDataStrF(rRecIdx:Int, _ rFldIdx:Int) -> String {
    if cAryType[rFldIdx] != .ZmStr { zSendErrMsgF("ERROR:ZMR_GDS:項目定義のデータ型がStringでない"); return "Error!" }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDS:レコード位置が範囲外"); return "Error!" }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iStr: String = cAryDataStr[iDataIdx][rRecIdx]
    return iStr
}

扱うデータ型と、データを取り出す配列が違うだけで、4つとも同じような処理をしています。

最初にチェックするのは、データ型です。指定された項目番号が定義されているデータ型を調べて、メソッドのデータ型と比べています。同じでなければ、エラーメッセージを出して終了です。

続いては、レコード番号の検査です。存在するレコード番号か調べて、範囲内でなければエラーメッセージを出して終了です。

エラーチェックを通ると、FldIdxからDataIdxを求めます。DataIdxは、データが入っている配列のインデックス番号です。それとレコード番号RecIdxにより、目的の項目データを得られます。

鋭い人なら、これら4つのコードを見て、ある疑問が浮かんだと思います。RecIdxは範囲チェックしているのに、FldIdxは範囲チェックしていない、なぜだろうと。実は、FldIdxの範囲チェックを不要にする、使い方の工夫があるのです。使用例編のところで詳しく説明しますので、今は気にしないでください。

 

この4つのメソッドでは、エラーがあったときにnilを返していません。エラーメッセージは出すものの、関係のない値を返しています。このように作った理由は、戻り値をOptional型にしたくなかったからです。

戻り値がOptional型だと、メソッド側でラップし、呼び出し側でアンラップします。両方で無駄な処理が生じます。項目ごとにメソッドを呼び出す方式ですから、もっとも多く使うメソッドになってしまうため、このような無駄を含ませたくないのです。

エラーが出る状況も考えてみました。データ型が間違っているエラーは、メソッドの単なる選択ミスなので、そのコードが最初に動いたときに気付きます。簡単に直して終了です。

もう1つのエラーは、配列の範囲外のインデックス指定です。こちらも、最初に作ったインデックス計算式などが間違っていたときに発生します。その場合は、簡単に直して終了です。でも、隠れているバグにより、インデックス値の計算がミスしてて、間違った値が生じることもあります。この場合は、あきらめるしかないでしょう。ただし、この種のバグが隠れている可能性は極めて低いので、本番時のバグを気にする必要はないと考えます。

このようにエラーが発生する状況を考えた結果、関係のない値を返しても構わないと判断しました。

 

せっかくなので、nilを返した場合の動作も考えてみましょう。戻り値がOptrinal型であっても、正しく動いている前提でプログラミングするため、nilかどうかを検査せず、無条件にアンラップするでしょう。すると、nilが返ったとき、アプリ側でクラッシュします。

配列のインデックス値を検査しないのは、アプリ内で用意した配列を使う場合と同じです。アプリ内の配列でも、インデックス値が範囲外であれば、その場でクラッシュしてしまいます。

つまり、インデックス値が範囲外の場合、アプリ内の配列でも、このモデルクラス内の配列でも、同じようにクラッシュする作り方になるということです。

結果として、Optional型にするかしないかの違いは、インデックス計算のバグが含まれるときに、次のようになります。Optional型でnilを返す場合は、アプリがクラッシュします。Optional型にせず変な値を返す場合は、エラーメッセージが出て、変な値を通常どおり処理し、表示結果などが変な値になるでしょう。また通常時の違いとして、ラップとアンラップの処理がOptional型には含まれます。

インデックス計算のバグは開発時に発見されることがほとんどなので、開発でのバグ発生時にどう動くのが良いのか、ラップとアンラップ処理を気にするのかだけです。最終的な選択は、好みの問題でしょう。私はOptional型にしないほうを選びましたが、それぞれ好きなほうを選べばよいと思います。

 

実行速度が気になる場合は、項目の値の取得や設定で、検査を外すという方法もあります。2つの検査で引っかかるのは、ソースコードを動かし始めたときだけでしょう。アプリ全体が出来上がる直前の段階まで達したら、2つの検査をコメント化することも可能です。次のように。

// 値の取得(2つの検査をコメント化)
func getDataIntF(rRecIdx:Int, _ rFldIdx:Int) -> Int {
    //if cAryType[rFldIdx] != .ZmInt { zSendErrMsgF("ERROR:ZMR_GDI:項目定義のデータ型がIntでない"); return -999 }
    //if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDI:レコード位置が範囲外"); return -999 }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iInt: Int = cAryDataInt[iDataIdx][rRecIdx]
    return iInt
}

こうすると無駄な処理が減って、少しは高速になります。検査を外した状態では、戻り値をOptional型にしなかったのが、まるで正解のようにも感じられます。

これは取得メソッドでのコメント化ですが、後述する設定メソッドでも、同様にコメント化が可能です。コメント化するなら、両方のメソッドで実施したほうが良いでしょう。

本番用のコンパイルで一部の機能を外すときは、昔から#ifdefが広く使われています。Swiftでも、同様の目的で#ifが使えます。この機能を使って、本番用だけは自動的に機能をオフする手もあります。詳しい使い方はネットなどで調べてください。

 

このモデルクラスでは、データ型が異なる4つセットのメソッドが何セットも含まれます。データ型が異なるものの、処理内容が似ているため、共通部分を探して外に出し、それを呼び出す形に作り変えたくなります。

でも今回は、データを入れている配列も別々ですし、途中で使う変数もデータ型が異なります。共通部分を抜き出すのが難しいケースです。しかも、それぞれのメソッドは短めで、共通部分を抜き出すメリットは小さめです。

このような場合は、無理して共通部分を抜き出しても、別なバグを生んだり、処理内容の読みやすさが低下したりと、デメリットのほうが大きくなりがちです。かなり似ている処理ですが、そのまま共通化しないで作るのがベストだと思います。

 

続いて、項目へ値を設定するsetメソッドです。これもデータ型ごとに合計4つあります。

// 値を設定
func setDataIntF(rRecIdx:Int, _ rFldIdx:Int, _ rData:Int) {
    if cAryType[rFldIdx] != .ZmInt { zSendErrMsgF("ERROR:ZMR_SDI:項目定義のデータ型がIntでない"); return }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_SDI:レコード位置が範囲外"); return }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    cAryDataInt[iDataIdx][rRecIdx] = rData
}
func setDataFloatF(rRecIdx:Int, _ rFldIdx:Int, _ rData:Float) {
    if cAryType[rFldIdx] != .ZmFloat { zSendErrMsgF("ERROR:ZMR_SDF:項目定義のデータ型がFloatでない"); return }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_SDF:レコード位置が範囲外"); return }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    cAryDataFloat[iDataIdx][rRecIdx] = rData
}
func setDataBoolF(rRecIdx:Int, _ rFldIdx:Int, _ rData:Bool) {
    if cAryType[rFldIdx] != .ZmBool { zSendErrMsgF("ERROR:ZMR_SDB:項目定義のデータ型がBoolでない"); return }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_SDB:レコード位置が範囲外"); return }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    cAryDataBool[iDataIdx][rRecIdx] = rData
}
func setDataStrF(rRecIdx:Int, _ rFldIdx:Int, _ rData:String) {
    if cAryType[rFldIdx] != .ZmStr { zSendErrMsgF("ERROR:ZMR_SDS:項目定義のデータ型がStringでない"); return }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_SDS:レコード位置が範囲外"); return }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    cAryDataStr[iDataIdx][rRecIdx] = rData
}

検査内容は、getメソッドと同じです。

検査を無事に通過すると、FldIdxからDataIdxを求めます。これもgetメソッドと同じです。あとは、該当するデータ型の配列へ、受け取った値を設定するだけです。

 

次は、苦肉の策として用意したメソッドを取り上げます。

いろいろな使い方への対応も大事です。全部の要望に対応するのは無理なので、アプリ側が何とかできるための手助けを用意するのが、現実的な方法でしょう。

とは言え、どんな手助けが良いのでしょうか。アプリ側の要望を予想して考えてみましょう。たとえばクラス内から、条件に合ったデータを見付けるときです。このクラスで検索機能を用意しても構わないのですが、データ型が4種類あり、それを自由に組み合わせて使えます。検索機能を自由に使える形で実現するのは、難しそうです。

そもそも、検索機能も要望の1つでしかありません。値の傾向を調べたり、中央値を求めたり、要望を考えると次々に出てきます。

そこで思い浮かんだのが、最も簡単に実現でき、かなり幅広く使える機能です。その機能とは、特定の項目の全データを返す機能です。この機能が全部の項目で使えれば、アプリ側で何とでも使いこなせるでしょう。

たとえば、ある項目で上位3つの値を持つレコードだけ探すとしましょう。その項目の全データを得られれば、全データを比較して上位3つを選び出せます。このクラスから受け取ったデータはレコード順に並んでいるため、該当するレコードを特定することは簡単です。あとは、見付かったレコードをメソッドで取得するだけです。レコード単位の取得は用意してないので、項目単位で取得することになりますが。

他の要望も、項目の全レコードを取得することで、何とかなるでしょう。複数の項目で検索する場合も、該当する複数項目の全データがあれば可能です。極端な話、全部の項目で全データを取得すると、クラス内の全データを得ることができます。

以上のように考えて作ったのが、特定項目の全データを返すメソッドです。4つのデータ型ごとに必要なので、4つのメソッドになりました。具体的には、次のようなSwiftコードです。

// ======================================== 特定項目全データを返す
func getDataAllIntF(rFldIdx:Int) -> [Int] {
    if cAryType[rFldIdx] != .ZmInt { zSendErrMsgF("ERROR:ZMR_GDAI:項目定義のデータ型がIntでない"); return [Int]() }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    return cAryDataInt[iDataIdx]
}
func getDataAllFloatF(rFldIdx:Int) -> [Float] {
    if cAryType[rFldIdx] != .ZmFloat { zSendErrMsgF("ERROR:ZMR_GDAF:項目定義のデータ型がFloatでない"); return [Float]() }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    return cAryDataFloat[iDataIdx]
}
func getDataAllBoolF(rFldIdx:Int) -> [Bool] {
    if cAryType[rFldIdx] != .ZmBool { zSendErrMsgF("ERROR:ZMR_GDAB:項目定義のデータ型がBoolでない"); return [Bool]() }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iAryBool: [Bool] = cAryDataBool[iDataIdx]
    return cAryDataBool[iDataIdx]
}
func getDataAllStrF(rFldIdx:Int) -> [String] {
    if cAryType[rFldIdx] != .ZmStr { zSendErrMsgF("ERROR:ZMR_GDAS:項目定義のデータ型がStringでない"); return [String]() }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    return cAryDataStr[iDataIdx]
}

どのデータ型でも、ほぼ同じです。最初にデータ型を検査してOKなら、DataIdxを求めて、該当する配列を返しています。

いつもなら、return文の前に一時変数を用意して、そこへ戻り値を入れます。でも今回は配列なので、配列がコピーされるという無駄な処理が生じるのを嫌い、返す配列を、return文の中に直接書いています。

あと戻り値のデータ型ですが、Otrional型にしたくなかったので、エラーの際にnilではなく、空の配列を返すようにしました。このエラーが出るのは、データ型を間違えたときだけ、つまり呼び出す側のコードを最初に動かしたときだけですから、データ型の間違いをエラーメッセージで気付くはずです。

全データを返す機能ですが、データ量が少ない場合の特別用であり、データ量が多い場合はお勧めできません。返す配列が大きくなるからです。このモデルクラスを実際にアプリで使ってみて、どのような機能が必要なのか検討し、この機能を使わなくて済むような新機能を追加しようと思っています。

 

このモデルクラス内に入れたデータを活用する目的の機能も、用意しておくと便利でしょう。とりあえず、1つのメソッドを作りました。

データ活用では、タブ区切りのテキストとして生成する機能が、かなり幅広く利用できると思います。そのまま表計算アプリのワークシートへペーストすれば、それぞれの値が個々のセルに入りますから。

というわけで、タブ区切りのテキストを生成して返すメソッドを用意しました。先頭に項目名を入れた、見出し行付きです。具体的には、以下のようなSwiftコードになりました。

// ======================================== データ利用
// タブ区切りのテキストを生成
func getTabTextF() -> String {
    var iStrOut: String = ""   // 出力用
    // テキストの生成:見出し
    for i in 0..<cFldCount {   // j=FldIdx
        iStrOut += cAryName[i]
        if i < (cFldCount - 1) {   // 行末だけタブ文字なし
            iStrOut += CODE_TAB
        }
    }
    iStrOut += CODE_LF
    // テキストの生成:データ
    for i in 0..<cRecCount {   // i=RecIdx
        for j in 0..<cFldCount {   // j=FldIdx
            let iDataIdx: Int = cAryDataIdx[j]
            switch cAryType[j] {
            case .ZmInt :
                let iInt: Int = cAryDataInt[iDataIdx][i]
                let iStr: String = String(iInt)
                iStrOut += iStr
            case .ZmFloat :
                let iFloat: Float = cAryDataFloat[iDataIdx][i]
                let iStr: String = String(stringInterpolationSegment:iFloat)
                iStrOut += iStr
            case .ZmBool :
                let iBool: Bool = cAryDataBool[iDataIdx][i]
                let iStr: String = String(stringInterpolationSegment:iBool)
                iStrOut += iStr
            case .ZmStr :
                let iStr: String = cAryDataStr[iDataIdx][i]
                iStrOut += iStr
            }
            if j < (cFldCount - 1) {   // 行末だけタブ文字なし
                iStrOut += CODE_TAB
            }
        }
        iStrOut += CODE_LF
    }
    return iStrOut
}

タブ区切りのテキストでは、データの並び順が大事です。表計算アプリへペーストする使い方が多いでしょうから、ワークシート上で使う際に一般的な並び方をしていなければなりません。複数レコードのデータなら、もっとも一般的な姿は、1行目に見出しが来て、2行目からデータが並び、1行分が1件のレコードという形でしょう。ですから、その形に合わせて、タブ区切りのテキストを生成します。

まず最初に、項目名をタブで区切った見出し行を生成します。行末には、タブを入れずに、LFコードを入れます。タブもLFも、別な共通ライブラリで定義してあり、その変数を使っています。

続いて、データを順番に追加します。4種類のデータがありますが、どれもStringに変換して入れます。1つのレコードが1行分となり、項目間はタブで区切り、行末はタブなしのLFです。これをレコードの数だけ繰り返します。

最後のレコードが処理し終わったら、テキストの生成も完了です。生成した文字列を呼び出し先へ返して、メソッドは終了します。

 

この機能の使い方ですが、次のようになるでしょう。画面上に、データをコピーするボタンを用意します。そのボタンがタップされたら、このメソッドを呼び出して、タブ区切りのテキストを得ます。それをクリップボードへコピーすれば、アプリ側の処理は完了です。

その際、終了のメッセージも大事ですね。クリップボードへコピーされたこととに加え、何件のレコードが含まれるか表示します。レコード件数が明らかになると、他のアプリへペーストしたとき、漏れなくデータが渡ったと確認できて助かりますから。

 

また長くなってきたので、ここで区切ります。続きは、次回の投稿にて。残りの機能(ファイルへの保存)と使用例を取り上げる予定です。

 

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

0 件のコメント:

コメントを投稿