2015年6月23日火曜日

共通モデルのライブラリ化を試作(全体検討編)

アプリを作る際には、扱う情報ごとにモデル(MVCのM)のクラスを用意して、アプリ内の構造をスッキリさせます。オブジェクト指向では一般的な考え方でしょう。

情報ごとにモデルを作るのですが、同じようなパターンの情報も意外に多くあります。それらを毎回作っても構わないのですが、もしライブラリ化できれば、クラスを毎回作らずに済むのでは、と考えるようになりました。

もちろん、ライブラリ用として用意するモデルクラスが、1つで済むとは考えていません。よく使いそうなモデルを選び、おそらく数種類のモデルをライブラリ化します。その結果、半分ぐらいのモデルクラスを作らなくて済むと予想しています。

というわけで、試しに作ってみました。予想外に苦労したので、その過程も含めて紹介します。

 

モデルクラスをライブラリ化と言っても、どんな感じなのか想像できない人もいると思います。そこで、大まかな姿を最初に紹介します。それが想像できないと、これからの話が意味不明に感じるかもしれませんので。

ライブラリ化するのですから、モデルクラスで扱うデータの種類は、それぞれのアプリで異なります。まずは基本的なデータ型である、Int、Float、Bool、Stringを考えます。アプリ側では、この中のどれをどんな組み合わせで使うのか、モデルクラスのインスタンス生成時に指定します。データベースのフィールド定義と似たようなものです。このような形で作ると、モデルごとに異なるデータ型の組み合わせに対応でき、より多くのモデルで利用できるように仕上げられます。

インスタンス生成でデータ型の定義が終わったら、どんどん使い始めます。モデルクラスに用意されたメソッドで、新しいデータを追加したり、前に追加したデータを取得したり、データの一部を更新したりします。つまり、データ型の組み合わせ設定を用意し、それでインスタンス生成した後は、ただ使うだけなのです。

ライブラリ化したクラスで、付加機能を最初から用意すれば、さらに使いやすくなります。たとえば、入力したデータをタブ区切りのテキストに変換してくれる機能を付けると、そのデータをアプリ側でコピーして、他のアプリ(表計算アプリなど)へ持っていけます。また、少し前に紹介したファイル保存の機能に対応させておけば、そのメソッドを呼び出すだけでファイルやネットへの保存もできます。わざわざ作る必要はありません。

おそらく一部のアプリでは、特別に追加したい機能も出てくるでしょう。その場合は、ライブラリ化したモデルクラスのサブクラスを作って、必要な機能を追加する方法が可能です。独自要望の多くは、この方法で何とかなると思います。

 

ライブラリ化すると良いことが多いものの、上手に作らないと、デメリットも生じます。その点も最初から考えておかなければなりません。

真っ先に思い浮かんだのが、データ型の検査です。複数のデータ型を自由に組み合わせる場合、AnyやAnyObjectなどの汎用データ型に入れて扱いたくなります。モデルクラス内だけでなく、getなどのメソッドでも汎用データ型を使うと、もう大変です。コンパイラのデータ型検査機能が無効になり、実行時エラーの続出といった悲惨な状況へ向かうかもしれません。できる限り汎用データ型を使わず、データ型検査機能を有効に働かせることが大事です。

と書きましたが、実際に作るとなると大変です。なにしろ、データ型を自由に組み合わせて使うわけですから、汎用データ型を使わずに作ると、組み合わせ数が膨大になりそうです。どうすれば良いのでしょう。

ライブラリ化する際には、ライブラリではない形で作るよりも多くのエラー検査機能を入れます。でもモデルクラスの場合、アクセスが多いことも考えられ、検査が多いと処理が遅くなってしまいます。どの程度の検査を入れるのか、注意深く決める必要があるでしょう。現実的には、検査内容は最低限だけに絞り、あまり入れない方向で進めると思います。

 

以上の点を踏まえた上で、いよいよ具体的なモデルクラスの話に移りましょう。

どんなモデルを最初に作るかは、決めてあります。もっとも簡単そうなモデルを選びました。記録カードを扱うようなモデルをです。

そのカードは記入内容が予め決められていて、用意された欄にどんどん記入し、カードが増えていくような感じです。言ってみれば、カード型データベースからデータベース機能を抜いたようなモデルでしょう。一般的には、記録カードと呼ばれるものです。

もう少し詳しく書きましょう。まず最初に、カードに記載する内容を決めます。何個の項目を含み、それぞれの項目のデータ型が何なのかを決めるわけです。それぞれの項目に名前も必要でしょう。

具体的には、次のような感じになります。ゲームなどのイベントで、参加者の得点を記録するカードの情報を考えます。会員番号はInt、氏名はString、得点はFloat、特別会員はBoolという感じです。これを参加した人の数だけ、カードへ毎回入力します。どんどん入力すると、参加人数と同じ数のカード情報が保存されます。

別な例として、アクセスログなんかも考えられます。日付はIntに変換、時刻もIntに変換、IPアドレスはStringに変換、アクセス種類はString、アクセス内容もString、エラー有無はBoolとかです。

たいていの情報は、4つの基本データ型に変換して保持できます。それ以外のクラス・インスタンスも扱えるようにするかは、4つのデータ型で作った後に検討することにしました。まずは、4つのデータ型で使えるように仕上げるのが先決です。

 

今回は、どんな内部構造で作ったら良いのか、まったく想像できませんでした。そのため、とりあえず作りながら考えることとしました。どんなメソッドが必要なのかとか、何も考えずに作り始めたわけです。

真っ先に検討したのが、クラス内でのデータの持ち方です。複数の項目があって、それぞれデータ型が異なります。また、どの項目なのか外部から指定できる必要もあるでしょう。項目番号で指定するのか、名前で指定するのか。

最初は、項目番号で指定することを考えました。いろいろなデータ型を入れるのですが、データ型を検査するためには、AnyやAnyObjectを使えません。そのデータ型の配列を用意するしかないでしょう。そして、用意した個々の配列を管理するための親配列も追加しました。親配列は[Any]で用意し、その要素にデータ型の配列([Int]や[String])を入れるという形です。

データ型の配列は追加などがあるので、Varで初期化します。昔のソースコードは決して残ってないので、肝心の部分だけ思い出しながら書くと、次のような感じだったと思います。なお、変数名は完全に忘れたのでデタラメです。

// 親配列に、データ型の配列を入れる(思い出しながら書いたので、一部が間違っているかも)
var cAryMain: [Any] = [Any]()
// 子配列を作って、親配列に入れる
var iAryInt: [Int] = [Int]()
var iAryFloat: [Float] = [Float]()
cAryMain.append(iAryInt)     // 最初の項目はInt型
cAryMain.append(iAryFloat)   // 2番目の項目はFloat型

ここまでは問題なく作れました。さっそく次のような形でデータを追加しようとしたところ(これも思い出しながら書いているので、一部が間違っているかもしれません)、appendの行がコンパイルエラーになりました。

// データ型の配列に要素を追加したいが、コンパイルエラーに(同じく、一部が間違っているかも)
let iInt: Int = 100
(cAryMain[0] as! [Int]).append(iInt)  // この行がエラーに

英語のエラーメッセージから推測すると、どうやら配列[Any]に入れてしまった配列[Int]では、appendなどが使えなくなるようです。間違った使い方を防止するために、安全装置的な検査が組み込まれているのでしょうか。これでは、まったく使い物になりません。

諦めきれないので、もう少し粘ってみました。もしかして、参照渡しでアクセスすると、コンパイルエラーが出ない可能性があるかなと。そこで、cAryOya[0]を関数の引数として渡し、inoutを指定してみました。残念ですが、結果は同じでした。

他にも、思い付いた方法で試してみたのですが、最終的にはコンパイルエラーが出てしまいます。やはり、どこかで安全装置が働いて、許してくれないのでしょう。変数[Int]を変数[Any]に入れる方法は、完全に諦めました。

ここまでの段階で、他のメソッドなども少し作っていました。中心となる構造が使用不可になったため、すべて最初からやり直しです。

 

気を取り直して、もっと確実な方法で作ることにしました。データを入れる配列[Int]や配列[Float]などは、配列[Any]に入れず、配列[Int]や配列[Float]のままで扱うことにしました。これが一番確実な方法ですから。

ただし、そのままだと何番目の項目がどこに入っているか不明なので、参照インデックスの役目を持つインデックス用配列[Int]を用意します。このインデックス用配列に、それぞれのデータ配列のインデックス番号を記録しておきます。これとは別にデータ型を示す配列変数も用意します。これら2つが管理用の配列で、両方の値を参照すれば、どのデータ型なのかと、どのデータ位置に保存されているのかが分かるというわけです。

具体的に仕上がったコードは、次のようになりました。

// 管理用の配列
var cAryType: [ZmType] = [ZmType]()  // 項目のデータ型の定義
var cAryDataIdx: [Int] = [Int]()     // データ配列内のインデックス番号
// データを入れる配列
var cAryDataInt: [[Int]] = [[Int]]()        // データ配列
var cAryDataFloat: [[Float]] = [[Float]]()
var cAryDataBool: [[Bool]] = [[Bool]]()
var cAryDataStr: [[String]] = [[String]]()

この中のZmTypeは、データ型を指定するために作った列挙型です。決められたデータ型しか指定できないようにするためです。

ここで、データ型の定義として「Int,Float,Int,String」が指定されたとします。するとcAryDataIdxには、[0, 0, 1, 0]の値が入ります。意味としては「Int型のインデックス0、Float型のインデックス0、Int型のインデックス1、String型のインデックス0」となります。データ型の定義にIntが2つ含まれるので、Intだけはインデックス0とインデックス1が作られたわけです。

4つのデータ型の配列は、どれも2次元で作りました。それぞれ複数の項目で使われる可能性があるからです。「Int型のインデックス0」で使うときは、cAryDataInt[0][0], cAryDataInt[0][1], cAryDataInt[0][2], ...とアクセスし、「Int型のインデックス1」で使うときは、cAryDataInt[1][0], cAryDataInt[1][1], cAryDataInt[1][2], ...とアクセスします。

どのデータ配列もデータ型が指定されているので、データ型が決まった形で使われます。また、すべてのメソッドを、データ型を指定した形で作ります。結果として、コンパイラがデータ型を検査できる形に仕上がりました。設計上の一番大事な点を、何とかクリアーできました。

 

キリが良さそうなので、ここで一旦区切ります。具体的なSwiftコードは、次回の投稿で紹介します。今回の内容は全体設計という形ではないため、「全体設計編」ではなく「全体検討編」としました。

 

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

0 件のコメント:

コメントを投稿