2015年7月1日水曜日

共通モデルのライブラリ化を試作(内部変更編)

共通で使えるモデルクラスをライブラリ化する第1段として、記録カードのモデルクラスを作る話を紹介しました。今回は、その後の状況を報告します。

 

記録カードのモデルクラスが一段落したので、2つ目のモデルクラスを作りました。記録カードと少し似ている、キー付きカードのモデルクラスです。記録カードにキーが追加されたと考えて、ほぼ間違いありません。記録カードはArreyに相当し、キー付きカードはDictionaryに相当すると考えれば、中身を想像しやすいと思います。

記録カードを作った経験があるので、テストコードも含めて、おおよそ1/3の時間で作り終わりました。クラスに限らず何かのソフトを作る場合には、コーディングしている時間よりも、仕様を考えている時間のほうが多いです。

2つ目のキー付きカードは、記録カードという前例があります。そのため、どんな形で内部データを保存するのか、どんなメソッドがあればよいのか、どんな検査機能を付けなければならないのか、などで悩みませんでした。また、テストコードも似た形で作れるので、その点でも悩みませんでした。まさに、スイスイと作れたという感じです。

肝心なのは、内部データの持ち方ですね。最初からAnyかAnyObjectと決めていたので、どちらにするか決めるだけです。4つの基本データ型だけでなく、将来はクラスのインスタンスも持てるようにしたいはずです。ほとんどのクラスがNSObjectのサブクラスですから、それと相性の良いAnyObjectが適しているでしょう。というわけでAnyObject配列を選びました。

キー付きというモデルなのも関係しているのか、記録カードのモデルクラスよりも、少しスッキリした内部構造に仕上がりました。AnyObject配列を選んで正解だったようです。

 

2つ目のモデルクラスの結果が良かったので、記録カードのモデルクラスも、内部データを持つ配列をAnyObject型に変更してみました。

今までは、データ型ごとに配列を用意したので、配列が4つもあります。それを、1つのAnyObject配列に置き換えるだけです。余計なインデックス変換も不要になり、コードが少し短くなります。

変更前のモデルクラスは別名で保存しておき、変更作業を進めました。実際に作業してみると、30分ぐらいで終了です。内部の仕様だけが変更なので、テストコードはそのまま使えます。コンパイルエラーを全部消して実行すると、一発で通りました。変更完了です。

よく考えると、変更する箇所は多いものの、変更の内容自体は単純でした。4つの配列を消して、1つのAnyObject配列を追加します。インデックスを変換している箇所では、変換している1行を削除し、新しいインデックスに置き換えるだけでした。また、AnyObject配列からデータを読む箇所では、それぞれのデータ型へキャストする指定を追加します。

このようにパターン化できる作業しかないため、かなり慎重に進めても、30分ぐらいの時間で終わりました。当然ですが、一番最初に作ったのは、新しいAnyObject配列の追加と、インデックスの使い方を説明するコメントです。このコメントを見ながら作業するので、間違いを起こさず更新できたのでしょう。

 

そのコメント行を紹介します。まずは、変更前のコメントから。

// ======================================== 3つのインデックスがある
// RecIdx:レコードの並び順を示すインデックス
// FldIdx:項目(フィールド)の並び順を示すインデックス:この値からDataIdxを得て、それでデータへアクセスする
// DataIdx:データ配列内で、どの行(位置)に入っているかを示すインデックス:FldIdxをキーにして、cAryDataIdxから得る
// ============================== 次のように利用する
// cAryName[FldIdx] -> String (項目名)
// cAryType[FldIdx] -> ZmType (データ型)
// cAryDataIdx[FldIdx] -> DataIdx
// cAryDataXXX[DataIdx][RecIdx] -> Data (項目値:4種類のデータ型)
// ========================================

インデックスを変換するDataIdxが使われています。続いて、変更後のコメントです。

// ======================================== 2つのインデックスがある
// RecIdx:レコードの並び順を示すインデックス
// FldIdx:項目(フィールド)の並び順を示すインデックス
// ============================== 次のように利用する
// cAryName[FldIdx] -> String (項目名)
// cAryType[FldIdx] -> ZmType (データ型)
// cAryDataAny[FldIdx][RecIdx] -> AnyObject (項目値:中身は4種類のデータ型)
// ========================================

3つのインデックスから、2つのインデックスへと変更されたのが、もっとも大きな点でしょう。DataIdxがなくなり、FldIdxで直接指定できる形になりました。また、項目値のデータ型が、変数上はAnyObjectだけに変わっています。

 

変更したメソッドのうち、主なものも紹介します。まずは、getメソッドから。

// 値の取得(変更前)
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 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 iInt: Int = cAryDataAny[rFldIdx][rRecIdx] as! Int
    return iInt
}

変更後の処理では、インデックスを変換している行が消えて、メソッドが1行分だけ短くなりました。ソースコード全体を眺めても、たいして短くならなかったというのが感想です。内部構造としては、余計なインデックス変換がなくなった点が一番大きいでしょう。

AnyObject型の配列cAryDataAnyから、データを取り出す際にはキャストが必要です。でも、キャストのために数文字を追加するだけですから、あまり変わっていない印象ですね。

getメソッドと違って、setメソッドは変更ありません。Intなどのデータ型の値を、AnyObject配列へ入れるだけですから、特別な指定は不要です。

 

続いて、データを保存する配列の初期化です。

// データ用の配列の初期化(変更前)
// 項目ごとに配列を生成して、その位置(インデックス番号)を記憶する
private func setupArrayF() {
    for i in 0..<cFldCount {  // i=FldIdx
        switch cAryType[i] {
        case .ZmInt :
            cAryDataIdx.append(cAryDataInt.count)  // count値がインデックス値より1つ大きいので、作成前に保存する
            cAryDataInt.append([Int]())
        case .ZmFloat :
            cAryDataIdx.append(cAryDataFloat.count)
            cAryDataFloat.append([Float]())
        case .ZmBool :
            cAryDataIdx.append(cAryDataBool.count)
            cAryDataBool.append([Bool]())
        case .ZmStr :
            cAryDataIdx.append(cAryDataStr.count)
            cAryDataStr.append([String]())
        }
    }
}
// データ用の配列の初期化(変更後)
// データ保存用の配列を初期化して、項目ごとに配列を生成する
private func setupArrayF() {
    cAryDataAny = [[AnyObject]]()      // データ保存用の配列を初期化
    for i in 0..<cFldCount {  // i=FldIdx
        cAryDataAny.append([AnyObject]())    // 項目ごとに、空の配列を追加
    }
}

保存場所を指すインデックス値DataIdxを保存する必要がなくなったのと、配列が1つになったので、かなり短くなりました。ちなみに、これだけ短くなったのは、ここの1箇所だけです。

 

このような感じで、無事に内部データをAnyObject配列に更新できました。

ただ更新しただけでは、とくにメリットはありません。しかし、内部配列がAnyObject型になったことで、いろいろなデータ型を簡単に入れる準備ができました。4つのデータ型とは違うデータ型へも、苦労せずに対応できそうです。

もちろん、以前の内部配列の構造でも、新しいデータ型へは対応できます。AnyObject配列を追加して、そこへ入れれば良いだけです。でも、データ保存用の配列が少しずつ増えていくのは、あまり良い形とは言えません。スッキリした構造から、どんどんと離れていきますから。

今回の変更は、将来の機能拡張の準備として、大きな意味があったと思います。今後も、このモデルクラスを改良して、少しずつ良くしていく予定です。

 

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

0 件のコメント:

コメントを投稿