2015年6月25日木曜日

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

共通で使えるモデルクラスをライブラリ化する第1段として、記録カードのモデルクラスを作る話の続きです。クラスの大まかな説明が終わったので、具体的なSwiftコードの紹介に入ります。

 

ソースコードの中身の前に、そのファイル名から。古い投稿で紹介したように、私はファイル名に分類情報を入れています。先頭の2文字が分類コードです。ライブラリ用としては、View型のクラスはBV、それ以外のクラスがBCでした。今回作るモデルクラスは特別なので、新しい分類コードとしてBMを割り当てました。モデルクラスのライブラリを、今後も増やしていく予定だからです。

今回のモデルクラスは記録カードなので、ファイル名は「BM_RecCard.swift」としました。ファイル名を見るだけで、基本ライブラリ(B)のモデルクラス(M)に属する、記録カード(RecCard)であることが理解できます。また、ファイル名の一覧でも、モデルクラスのBMが連続して並ぶため、整理された状態で見えます。

 

ファイル名が決まったので、いよいよSwiftコードです。まずは入れ物となるクラスから。

ライブラリのクラス名はZで始めるルールを採用していて、今回はモデルクラスのRecCardなので、クラス名はZModelRecCardとしました。管理用とデータ用の配列に加え、レコード数の変数も必要です。それらを入れた状態で、次のようになりました。

// 記録カードのモデルクラス(これは入れ物で、この中に後述するコードを追加する)
class ZModelRecCard {
    // 主要な数
    var cFldCount: Int = 0                // 項目数
    var cRecCount: Int = 0                // レコード数
    // 管理用の配列
    var cAryName: [String] = [String]()   // 項目名
    var cAryType: [ZmType] = [ZmType]()   // 項目のデータ型の定義
    var cAryDataIdx: [Int] = [Int]()      // データ配列内のインデックス番号
    // データを入れる配列
    var cAryDataInt: [[Int]] = [[Int]]()         // データ配列
    var cAryDataFloat: [[Float]] = [[Float]]()
    var cAryDataBool: [[Bool]] = [[Bool]]()
    var cAryDataStr: [[String]] = [[String]]()
}

Optional型にしたくないので、配列も含めて変数宣言時に初期化しています。

このモデルクラスでは、主に3種類のインデックスを使い分けます。それぞれの意味を、きっちりと整理しておかなければなりません。作っている途中で間違えやすいですから。

そんなミスを防ぐために役立つのが、インデックスの名前と役割を書いたコメントです。どんな名前のインデックスがあり、それぞれが何を指しているのか整理した内容を、コメント行として変数の直前に入れます。以下のように。

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

このコメントの後半では、用意した配列とインデックスの関係を示しています。どの配列に、どのインデックスを使い、どんな値が得られるのか、利用例の形で説明しています。

各インデックスの説明文だけでは理解しづらいですが、配列とインデックスと取得値の関係を示されると、格段に理解しやすくなります。また、このコメント行を見ながらプログラミングすると、インデックスの使い方を間違う可能性が大きく減ります。ですから、インデックスと配列の利用例のコメント行は、最初に作るべきでしょう。

当然ですが、インデックスとして用いる変数名も、ここで用意したインデックスの名前を使います。そうしないと、正しいインデックスが使われているかどうか、後で目視検査できませんから。

 

データ型を指定する列挙型も、専用に作りました。こちらもZで始まる名前です。

// 項目の定義に使うデータ型
enum ZmType: Int {
    case ZmInt = 0
    case ZmFloat = 1
    case ZmBool = 2
    case ZmStr = 3
}

列挙型の値ですが、先頭にZmを付けています。Zmが付かない、ただのIntでも構わないのですが、検索するときに、関係のない多量のIntが含まれます。それを嫌ってZIntとしたのですが、大文字が続いて読みづらく感じます。仕方なく、Zmと2文字を加えた形に落ち着きました。

本音ではzIntとしたかったのですが、列挙型の値は大文字で始めるルールがあるようで、zIntは避けました。一応、Swiftのコーディングルールは、できるだけ守りたいですからね。

 

続いて、モデルクラスの初期化です。モデルの定義として必要な、2つの配列を受け取ります。項目名の一覧と、項目ごとのデータ型の一覧です。この2つの配列だけで、モデルクラスの定義が完了します。

まずデータ型の一覧ですが、上記ZmTypeの配列です。使いたい項目の数だけ、データ型を並べます。8項目を使いたい場合は、8つのデータ型を入れます。すべて同じデータ型でも構いません。並び順は自由で、自分が理解しやすい順番を選びます。

データを入れたり出したりする際に、並び順で決まったインデックス番号(FldIdx)で、目的の項目を指定するだけです。その点にだけ注意していれば、あとは自由に使えます。

もう片方の項目名は、処理の中では少ししか使っていません。項目名付きのデータを生成したいときなど、このクラスを参照するだけで済むようにと用意しただけです。ただし、1つだけ重要な役割があります。項目名とデータ型の数を比べて、一致しているか調べます。初期化データの作成ミスを発見するのが目的です。

以上のように考えて作った初期化処理が、次のようなSwiftコードです。

// 初期化:モデルの定義(名前とデータ型)を受け取る
init(_ rAryName:[String], _ rAryType:[ZmType]) {
    if rAryName.count == 0 { zSendErrMsgF("ERROR:ZMR_INI:項目名が空"); return }
    if rAryName.count != rAryType.count { zSendErrMsgF("ERROR:ZMR_INI:項目名とデータ型の数が不一致"); return }
    cAryName = rAryName
    cAryType = rAryType
    cFldCount = cAryType.count
    // 配列の準備
    setupArrayF()
}
// 項目ごとに配列を生成して、その位置(インデックス番号)を記憶する
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]())
        }
    }
}

項目名とデータ型の数が一致するかだけでなく、受け取った配列が空でないかも調べています。これらの検査を通ったら、クラス内の変数に代入するだけです。

続いて、配列の準備処理が続きます。cAryTypeに入っているデータ型を参照しながら、該当するデータ型の2次元配列に、初期化した1次元配列を追加していきます。これが済むと、データをappendできる状態になります。

このように、初期化で指定されたデータ型を見て、それに対応した1次元配列を追加するため、余計な配列は作りません。必要なものしか作らないように、つまり無駄なものを生まない形にしてあります。

以上の初期化処理が終わると、もうデータを追加できる状態になっています。あとは、データを追加して溜め込み、必要なときにデータを参照するだけです。

 

続いて、メソッドを順番に取り上げましょう。まずは、項目名とデータ型を扱うメソッドから。次のように、2つしか用意していません。

// ======================================== 基本情報
// 項目名の一覧を返す
func getNameF() -> [String] {
    return cAryName
}
// データ型の一覧を返す
func getTypeF() -> [ZmType] {
    return cAryType
}

これに説明は不要でしょう。保存していたままというか、初期化の際に受け取ったままを返します。

 

次は、レコード単位の処理です。ここが2番目に悩んだ点です(ちなみに、1番目に悩んだ点は、前の投稿で紹介したデータの持ち方)。

レコード内のデータ型の構成は、使うアプリごとに異なります。その状態で、共通のレコード用メソッドを作ると、AuyやAnyObjectを使うしかないのです。それ以外だと、データ型の組み合わせを全部用意するしかありません。これこそ非現実的でしょう。

発想を切り替えて、レコードの追加は次のようにしました。まずダミー値で、新規のレコードを追加します。その直後に、項目ごとの値を設定するメソッドで、すべての項目の値を設定します。1つの値だけを設定するメソッドなら、データ型を固定した形で作れます。データ型の種類が4つなので、項目の値を設定するメソッドは4つで済みます。

レコードの更新も同様です。レコード単位で更新する場合も、すべての項目を1つずつ更新して、レコード全体を更新します。削除だけは例外で、新しい項目値を用意する必要がないため、1つのメソッドで完了できます。

項目ごとに値を設定する方法は、少し面倒に感じるかもしれませんが、それほどではありません。アプリ側でデータを用意する処理では、項目ごとにデータを集めて、1つのレコードに整えます。その処理を変更して、1つのレコードに整えず、項目ごとのデータを集める処理内で、そのままデータを更新してしまえば良いのです。アプリ側の処理全体として比べると、あまり差がないはずです。

以上のように考えて、作成したのが次の3つのSwiftコードで、1つずつ順番に要点だけ解説します。

// ======================================== レコード処理
// レコード数を返す
func getRecCountF() -> Int {
    return cRecCount
}

レコード数を返す処理は、見たとおりです。モデルクラス内で持っている値を、そのまま返しています。

 

レコードを追加する処理では、ダミーの値を入れています。それぞれの保存先では、初期化処理の中で空の配列を追加しているため、appendメソッドで追加するだけで済みます。

// レコードを追加:追加したレコードのインデックス番号を返す
func addRecF() -> Int {
    for i in 0..<cFldCount {   // i=FldIdx
        let iDataIdx: Int = cAryDataIdx[i]
        switch cAryType[i] {
        case .ZmInt :
            cAryDataInt[iDataIdx].append(-999)
        case .ZmFloat :
            cAryDataFloat[iDataIdx].append(-999.9)
        case .ZmBool :
            cAryDataBool[iDataIdx].append(false)
        case .ZmStr :
            cAryDataStr[iDataIdx].append("empty")
        }
    }
    ++cRecCount
    return (cRecCount - 1)
}

一番の肝は、データ型を調べることと、保存先を求めることです。それを項目の数だけ実行しなければなりません。項目の数はcFldCountに入れてあるので、その数だけfor文で回します。このような、項目の数だけ実行するという処理が何度も登場します。このfor文は、このモデルクラスでの定番パターンと言えるでしょう。

また、このfor文の変数iは、FldIdxを意味しています。そのことを伝えるために、コメントを追加しています。このおかげで、インデックスの使い方を間違えるミスが減らせます。別な方法として、変数名をiではなく、iFldIdxと変える方法も考えられます。しかし、ループ処理の中でのiやjは特別な存在で、処理内容を理解しやすくする効果も持っています。iやjのままのほうが良いと考え、コメントで補足する方法を選びました。

データ型はcAryTypeに入っています。その内容を調べることで、データ型を特定できます。switch文で分岐させ、該当するデータ型の処理に進めます。

ダミー値の保存先は、FldIdx値から求めます。保存先といっても、保存されているのはインデックス番号DataIdxで、それはcAryDataIdxに入っています。ここに入っている値DataIdxは、それぞれのデータ型の配列で、何行目なのか示しているだけです。ですから、この値DataIdxを取り出して、データ用の配列を参照するために使う必要があります。保存先のDataIdxが明らかになると、appendメソッドが実行できます。

最後に、追加したレコード番号RecIdxを返しています。レコード追加の直後に項目の値を更新するので、追加したレコード番号が分かると非常に助かるからです。

 

レコードを削除する処理ですが、私の場合はほとんど使わないものの、とりあえず付けておきました。

// レコードを削除
func deleteRecF(rRecIdx:Int) {
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_DR:インデックスが範囲外"); return }
    for i in 0..<cFldCount {   // i=FldIdx
        let iDataIdx: Int = cAryDataIdx[i]
        switch cAryType[i] {
        case .ZmInt :
            cAryDataInt[iDataIdx].removeAtIndex(rRecIdx)
        case .ZmFloat :
            cAryDataFloat[iDataIdx].removeAtIndex(rRecIdx)
        case .ZmBool :
            cAryDataBool[iDataIdx].removeAtIndex(rRecIdx)
        case .ZmStr :
            cAryDataStr[iDataIdx].removeAtIndex(rRecIdx)
        }
    }
    --cRecCount
}

まず最初に、レコード番号が範囲外か調べて、範囲外ならエラーメッセージを出して終了します。

あとは全体的に追加の処理と似ていて、データを削除する部分だけが異なります。データが入っている配列で、appendではなくremoveAtIndexを実行します。最後にレコード数を減らして終了です。

 

ここまででも、かなり長くなってしまいました。続きは次回の投稿にて。

 

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

0 件のコメント:

コメントを投稿