このブログでは、いくつかの機能を実現する方法を取り上げました。その際には必ず、最初に大まかな設計をしています。その具体的な方法を紹介します。
この大まかな設計作業ですが、紙に書いているわけではありません。実は、Xcode上で行なっています。それも、ソースコードを編集するテキストエディタ上でです。「えっ」と思った方が、いるかもしれませんね。でも、いろいろと試した結果、この方法が一番効率的だと判断したわけです。あくまで、私にとって一番効率的ですが。
では、具体的な手順を見ていきましょう。
まず最初に行なうのは、入れ物の作成です。そう、Swiftのソースコードを入れるファイルです。Xcode上で新規のSwiftファイルを追加します。
作成時には、これから作成する機能に適したファイル名を付けます。このファイル名も、私なりにルール化していますから、そのルールに合わせた名前を決めます。環境設定の機能を受け持つライブラリのソースコードなら、「BC_Pref.swift」ですね。
そのソースコードを、最初はどこに作るのでしょうか。私の場合は、以前の投稿で紹介した、iOS実験専用アプリを使います。ライブラリ化するソースコードは必ず、このアプリ内で作ります。Xcodeのメニューから、新規Swiftソースコードを追加して、準備は完了です。
入れ物が用意できたので、大まかな設計を始めます。思いついた機能や工夫を、テキストとして打ち込みます。ソースコードとして処理されるので、当然、コメントとして入力します。形式としては、箇条書きのメモです。
言葉をひととおり入力したら、今度は全体の構成を考えます。クラスとして仕上げるなら、必要となるクラス変数やメソッドを洗い出して、全体の形を想像します。
全体の形がほぼ決まったら、その内容をコメントの形で入力します。大まかには、次のような感じになります。
// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?
// データ型の一致をどうやって調べる?
// ====================================== 必要な仕様
// 全体を1つのクラスとして作る
// データ型ごとにメソッドを用意する:Int, Float, String, Boolの4種類
// エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// ====================================== 構成要素
// クラス変数:
// メソッド:初期化
// メソッド:ファイルから読み込み、ファイルへ書き出し
// メソッド:値の取得
// メソッド:値の設定
ここで大事なのが、区切り線です。たくさんの文字を入力しても、どこが区切りなのか明確に示すために、区切り用のコメント行を挿入します。コメント行の最後に、それ以降で入力する内容の見出しを付けます。この見出しが、入力したテキストからはみ出ているように見えると、区切りごとの内容が見やすくなります。
区切りの先頭には、必ず「検討すべき点」を入れます。ここは、解決していない問題点や、疑問に思っていること、機能として含めようか迷っていることなど、決まっていない内容を書きます。今後判断しなければならない内容なら、何でも記述するのが基本です。
クラスとして実現する場合は、まず最初にクラスだけ作ります。その中に、構成要素として挙げたコメント行を入れます。この時点で、クラス内の構成要素の並び順も決まります。
このような作業を続けながらも、気付いた点をコメントとして追加していきます。コメントは少しずつ増えていき、それに比例して仕様も細かくなります。ここまでの状態では、次のような感じになっているでしょう。
// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?
// データ型の一致をどうやって調べる?:is演算子で大丈夫か?
// ====================================== 必要な仕様
// データ型ごとにメソッドを用意する:Int, Float, String, Boolの4種類
// エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// 設定値セットにバージョン番号を含める
// ====================================== 構成要素
class PrefController {
// クラス変数:設定値セット、保存ファイル名、バージョン番号
// メソッド:初期化
// メソッド:ファイルから読み込み(保存ファイルの読み込みも含む)
// メソッド:ファイルへ書き出し
// メソッド:値の取得(4種類)
// メソッド:値の設定(4種類)
}
クラス変数やメソッドのコメントをクラス内へ移動させたので、実際のコードを書き始められる状態となりました。ここからの進め方としては、コメントをさらに追加して仕様を細かく詰めるか、メソッドなどの具体的な入れ物(Swiftコード)を先に作るか、どちらでも好きな形で進められます。
これ以降の作業でも同じなのですが、「検討すべき点」や「必要な仕様」の中に含まれるコメント行は、不要になった時点でどんどんと削除していきます。この段階では、クラスを作ったので、「必要な仕様」に含まれる「全体を1つのクラスとして作る」の行を削除しました。
今回は、先にSwiftコードの入れ物を作ってみましょう。おそらくですが、その手順ほうが、動いたときの状態を想像しやすい人が多そうですから。この段階では、メソッド名は決めますが、引数などは最小限しか入れません。その結果、次のようなソースコードになります。
// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?:Dictionaryが使えそう
// データ型の一致をどうやって調べる?:is演算子で大丈夫か?:if letも使えそう
// ====================================== 必要な仕様
// エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// 設定値セットにバージョン番号を含める
// ====================================== 構成要素
class PrefController {
// クラス変数:設定値セット、保存ファイル名、バージョン番号
// メソッド:初期化
init() {
}
// メソッド:ファイルから読み込み(保存ファイルの読み込みも含む)
func readPrefFileF() {
}
// メソッド:ファイルへ書き出し
func writePrefFileF() {
}
// メソッド:値の取得(4種類)
func getPrefF() -> Int {
return 1 // dummy
}
func getPrefF() -> Float {
return 1.0 // dummy
}
func getPrefF() -> String {
return "dummy"
}
func getPrefF() -> Bool {
return false // dummy
}
// メソッド:値の設定(4種類)
func setPrefF(rData:Int) {
}
func setPrefF(rData:Float) {
}
func setPrefF(rData:String) {
}
func setPrefF(rData:Bool) {
}
}
このまま入力を進めると、追加したソースコードやコメント行が増えるほど、メソッドの区切りが見づらくなってしまいます。それを防ぐために、メソッドの種類ごとの境目に、区切り用のコメント行を挿入します。その結果、次のようになります。
// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?:Dictionaryが使えそう
// データ型の一致をどうやって調べる?:is演算子で大丈夫か?:if letも使えそう
// ====================================== 必要な仕様
// エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// 設定値セットにバージョン番号を含める
// ====================================== 構成要素
class PrefController {
// クラス変数:設定値セット、保存ファイル名、バージョン番号
// ==================================== 初期化
init() {
}
// ==================================== ファイルの読み書き
// ファイルから読み込み(保存ファイルの読み込みも含む)
func readPrefFileF() {
}
// ファイルへ書き出し
func writePrefFileF() {
}
// ==================================== 値の取得(4種類)
func getPrefF() -> Int {
return 1 // dummy
}
func getPrefF() -> Float {
return 1.0 // dummy
}
func getPrefF() -> String {
return "dummy"
}
func getPrefF() -> Bool {
return false // dummy
}
// ==================================== 値の設定(4種類)
func setPrefF(rData:Int) {
}
func setPrefF(rData:Float) {
}
func setPrefF(rData:String) {
}
func setPrefF(rData:Bool) {
}
}
見て分かるように、区切りコメント行の追加によって、前にあったコメント行の内容を吸収している箇所があります。記述する内容は、できるだけ区切り行に含めて、他のコメント行を減らすように心掛けます。
ここまでの段階でも、クラス内の大まかな構成が見えてきたと思います。ここからは、実際のコーディングを進める前に、細かな仕様を固める段階に入ります。大事なのは、クラス変数、メソッドの引数と戻り値、エラーチェックの内容、メソッド内に含める処理の4つです。これらを明らかにするように、細かな点を固めていきます。
まず最初に、クラス内の変数を決めます。この例では、キー付きの環境設定値セットを入れるために、データ型を決定しなければなりません。必要なら、簡単な実験を別にすることもあります。いろいろ調べた結果、今回はデータ型Dictionaryで大丈夫と結論付けました。
クラス内の変数が追加できたら、各メソッドに引数や戻り値を追加します。たまたま今回の例では、戻り値が追加済みなので、引数だけを追加することになります。さらに、エラーチェック内容と含める処理を、コメント行として追加します。
ここまでの状態で、次のようなソースコードになりました。
// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?:Dictionaryが使えそう
// データ型の一致をどうやって調べる?:is演算子で大丈夫か?:if letも使えそう
// 上記のデータ型チェックは、全データ型の組み合わせで結果を確認すること
// ====================================== 必要な仕様
// ====================================== 構成要素
class PrefController {
// クラス変数:設定値セット、保存ファイル名、バージョン番号
var dictPref : Dictionary<String,AnyObject> = [String:AnyObject]()
var prefFName : String = "dummy.plist"
var strVersion : String = ""
// ==================================== 初期化
init(_ rDictPref:Dictionary<String,AnyObject>, _ rFName:String, _ rVer:String) {
}
// ==================================== ファイルの読み書き
// ファイルから読み込み(保存ファイルの読み込みも含む)
// ファイルが存在するか、存在したらバージョン番号を調べる、一致したら値を採用
func readPrefFileF() {
}
// ファイルへ書き出し
// バージョン番号を追加する
func writePrefFileF() {
}
// ==================================== 値の取得(4種類)
// 共通エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
func getPrefF(rKey:String) -> Int {
return 1 // dummy
}
func getPrefF(rKey:String) -> Float {
return 1.0 // dummy
}
func getPrefF(rKey:String) -> String {
return "dummy"
}
func getPrefF(rKey:String) -> Bool {
return false // dummy
}
// ==================================== 値の設定(4種類)
// 共通エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
func setPrefF(rKey:String, _ rInt:Int) {
}
func setPrefF(rKey:String, _ rFloat:Float) {
}
func setPrefF(rKey:String, _ rString:String) {
}
func setPrefF(rKey:String, _ rBool:Bool) {
}
}
ここまで出来上がると、具体的な動きが想像しやすくなります。頭の中でプログラムを動かし、呼び出し側とのやり取りを空想実行してみます。
どの順番でメソッドが呼び出され、それぞれのメソッド内で何が行なわれ、処理結果として何が返されるのか。また、クラス内の変数が、どのように変化していくのか。さらには、間違った使い方をされたとき、クラッシュなどの大きな問題が発生しないのか。などなど様々な点で、仕様に欠点がないかを探します。
もし問題を発見したら、引数を加えたり変更したり、新しいメソッドを追加したり、エラーチェックの内容を書き加えたりします。変更した場合は、再び空想実行してみて、変更の正しさを確認します。これで大丈夫と思えるまで、以上のような検討を続けるわけです。
この段階で細かな仕様を固める理由は、全体を見通しやすいからです。引数や戻り値を含んだメソッド名だけが並び、ほとんどが画面内に収まるか、少しスクロールするだけで全部が見れます。そんな状態なので、相互の関係とかも含めた、全体での整合性などが思考しやすくなっています。
いろいろと考えていると、細かな疑問点や不明点が見付かるものです。
たとえば、取得メソッドでエラーが発生した場合、正しい値は返せません。どんな値を返すべきなのか、この段階で決めるのがベストだと思います。nilを返すのか、データ型に該当する何かの値を返すのか。検討して決めた内容を、該当する箇所へコメント行として追加します。次の例のように。
// 取得メソッドにコメント行を追加(該当部分だけ抜粋)
// ==================================== 値の取得(4種類)
// 共通エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// エラー発生時には、nilではなく、ありそうもない値を返す:-99999とか、"ERROR!!!"とか
func getPrefF(rKey:String) -> Int {
return 1 // dummy
}
func getPrefF(rKey:String) -> Float {
return 1.0 // dummy
}
func getPrefF(rKey:String) -> String {
return "dummy"
}
func getPrefF(rKey:String) -> Bool {
return false // dummy
}
具体的な戻り値も、思いついたのならコメント行に記述します。このような形で、細かな仕様を少しずつ明らかにしていきます。
すべての疑問点や不明点が解消され、何もなくなったら、設計段階が終了です。あとは、メソッドの中身をコーディングしていくだけです。
ここまでで気付いた疑問点や不明点を解消済みですから、コーディングを開始したら、一気に書いてしまえます。設計段階で深く考えているほど、新しい疑問点や不明点は出てきません。
逆に、設計段階の考えが不十分だと、疑問点や不明点が新たに出てきます。そのような場合には、まず最初に、ソースコードの先頭部分に付けた「検討すべき点」へ、出てきた疑問点や不明点を、コメント行として書き加えます。それから、解消する方法を考え始めます。
基本的には、開始したコーディングを一旦中断して、疑問点や不明点を解消するのが先です。解消方法によっては、メソッドなどの構成が影響を受けるので、無駄な作業を行わないためです。ただし、解消方法がすぐに出そうもないときは、気分転換も兼ねて、コーディングを続けるという選択肢もあります。後から余計な修正が入っても構わないと、割り切りながら。
コーディングしていると、共通の処理が見えてきます。その場合は共通処理を関数として追加し、無駄なコードを書かないように工夫します。また、定義値なども積極的に追加し、間接参照を利用したソースコードに仕上げます。
コーディングが終わった時点のソースコードを、ここに載せても構わないのですが、長いので省略します。そのソースコードは、実際の処理を書き加えたメソッド、メソッドが共通で使う関数、メソッドで使う定義値の変数などが、追記された状態となります。具体的なコードは、数回前の投稿に含まれますから、そちらを参照してください。
ひととおりの機能を作り終えたら、本格的な機能確認を行ないます。機能確認というのは、実装した機能が具体的にどう動くのか、確認するための試験実行です。SDKの多くは、細かな仕様を書いておらず、不明確なことが多くて困りものです(書くのが大変なので、気持ちは分からないではないですが)。実際の動きがどうなるか、実装したコードを実行して、確かめるしかありません。
今回の例では、データ型を調べるために、「as演算子」と「if let ... as?」の2つを用いました。それらが実際にはどう動くのか、実行して確かめる必要があります。このような作業を、私は開発ツールの機能確認と呼んでいます。
その結果ですが、機能拡張のライブラリ化の投稿でも書いたとおりです。2つの記述方法とも、データ型が一致しているか調べる動作ではなく、値が代入可能かを調べる動作と判明しました。
期待どおりの動作結果でなかった場合には、そのことをソースコードに残しておく必要があります。今回の例では、該当するソースコードの箇所に、以下のようなコメントを残しました。
// 実際の実行結果などはコメントで残す(コメント部分のみ抜粋)
// データ型の一致をチェックする機能の、重要な注意点
// is演算子、if let ... as?のどちらも、代入可能かで判定される実験結果になった
// Int, Float, Boolの全部の組み合わせで、相互に代入可能。そのため、同じデータ型と判定される
// 上記2種類の処理は、データ型が同じかではなく、代入可能で判定しているのだろうか?
// この実行結果は、Xcode 6.1.1、SDK iOS8.1にて確認
このように実行環境まで含め、少し長めのコメントになります。時間が経つと忘れがちな内容なので、細かく書いておくほど、後から助かるでしょう。
機能確認が終わり、その動作で問題があれば修正しなければなりません。逆に、その動作でも構わなければ、本格的なテストに移ります。テストの話は、長くなるので省略します。テストが終了すれば、機能的には出来上がりです。
私の場合は、iOS実験専用アプリを使って開発することが多く、テストのソースコードも、そのアプリに含めて保存します。定期的にバックアップしていますから、テストも含めて安全に保存されています。
最後の作業は、ソースコードの整理です。「検討すべき点」などの開発用のコメント行を削除して、仕上がりにふさわしいソースコードとして整えます。余計なコメントは削除して、必要最小限のコメントだけを残します。
私の書き方の特徴でもあるのですが、「ファイルの読み書き」や「値の取得(4種類)」といった、クラス内の区切り用のコメント行は削除しません。それが付いていたほうが、ソースコードの区切りが明確になって、時間が経ってから見たときに、内容を把握しやすくなります。とりあえず残すという位置付けではなく、絶対に必要な区切り線として位置付けていて、全部のソースコードに付けています。ただし、このブログに投稿するときは、ほとんど削除していますが。
ここで紹介したのは、ソースコードが1つだけの例です。複数のソースコードを作る場合でも、基本的な考え方は同じで、それぞれに「検討すべき点」などを作ります。
また、全体に関しての「検討すべき点」は、どれか1つのソースコードを選んで、それだけに「検討すべき点(全体編)」を付けます。もちろん使い方は同じです。
以上が、私が普段使っている開発方法です。1つのソースコードに、実際の命令ソースコードだけでなく、設計途中のメモをコメントとして加え、それを更新しながら、コーディングもやや並行気味に行ないます。コメントしたメモを命令コードに書き換え、コメントが減りながら命令コードが増えるという形です。最後は、最低限のコメントと、完成した命令コードが残ります。
ソースコードをやや並行気味に書いていますが、一番大事なのは、きっちりと設計すること。命令コードが大枠しかない段階、つまり全体を見渡しやすい段階で、空想実行しながら機能を深く掘り下げて、設計の質を高めることです。この段階で、さまざまな空想実行ができると、良いソースコードが作れます。意地悪な順序で実行させたり、無茶な引数を与えたり、いろいろと空想実行します。ただし、何にでも対応すると、余計なコードが増えますから、対応はほどほどにすべきでしょう。
別な面でも、ソースコードには気を配ります。区切り用のコメントで全体を見やすく整え、開発途中で気付いた注意点などもコメントとして残します。数年後の完全に忘れた頃に見ても、困らないようなソースコードになるようにも配慮します。将来の自分への親切ですね。これは、意外に大事です。
設計や実装の作業は、個人的な好みの部分が大きいでしょう。ここで紹介した方法は、その理由も説明しました。もしヒントとして利用できそうな部分があれば、自分なりのアレンジを加えながら、オリジナルの開発手順を作ってみてください。開発効率の向上だけでなく、出来上がったソースコードの質が上がるかも。