ここでは、アプリケーションの作成の一例として、家計簿のWebアプリケーションを題材にして、開発プロセスを説明したいと思います。データ中心指向的なアプローチで進みます。
(詳細には立ち入らず要点をさらっと流していきます。また機能面についてのみ進めます。)
まずは、どんなアプリケーションを作成するのかを明確にします。アプリケーションの概要、目的、特徴を列挙します。
家計の単位は、グループとします。グループは階層的に構成できるようにします。一つの家計に対して複数のユーザが記録することができるようにします。
現金や預金などの勘定・資産を表すものとして「容器」というものを定義します。容器とはお金を入れる器を意味します。家計簿の記入は、容器(財布・口座・カード)に対して、入金・出金・移動のイベントを登録することによって行います。容器は、自由にいくつも管理することができます。また、科目も自由に登録でき、階層的に構築することできます。
定期的な出費は、予定入出金としてあらかじめ登録しておくことで、記帳を容易にします。
また、任意の時点での残高を参照でき、未来の日を指定すると、予定入出金から、将来の残高予測を行うことができます。入出金記録を様々な形で分析し、統計を取ることができるようにします。
このような機能に関する要件、そしてまた運用する環境、非機能要件(性能・セキュリティ・保守)等をまとめて要件を定義していきます。要件はしばしば表にしてまとめ、開発を請け負う側がそれに対して対応可能かどうか(コンプライアンスリスト)を提示します。
そして設計フェーズに入っていきます。
仕様を明確にするために典型的なユースケースを考慮します。これはユーザがどのようにシステムを利用するかのシミュレーションです。
またユースケース図を描いて、アクターとシステムの概要・境界線を明確にします。
ユーザの通常のオペレーションとしては、
といったところでしょう。
次にデータモデルを作成します。これはアプリケーションで管理するデータの構造を明らかにすることです。データの構造が少しでも重要なアプリケーションでは、構造をはっきりさせる必要があります。特にデータ中心のアプリケーションであれば、そこが肝といっていいでしょう。データモデルはアプリケーションの骨子で、データモデルを見ればアプリケーションの概要が読めて取れます。
エンティティを割り出し、エンティティ間の関連、主要な属性を明らかにしていきます。最初は概念モデルからはじめます。
さらにそれを詳細化して論理モデルとします。
要件から多くのエンティティの候補が出てきますが、それらをどのようにまとめるかがポイントです。ここでのデータモデルの設計が後の実装の効率に大きな影響を及ぼします。ここでは容器をサブタイプに分割し、物理モデルでもそのまま継承構造を使用しましたが、これは例として行いましたが実際には一テーブルにした方がその後の実装では利点が多いと思います。
データモデルを構築する際、現実に存在するものをエンティティとするとうまく行かないことがあります。これはオブジェクト指向やデータモデルのなんちゃって本が役に立たないところです。それよりも何を管理する必要があるかという視点の方が重要です。
ここでのコンセプトは、クレジットカード、プリペイドカード、銀行口座、財布、借金、すべてを同様に扱うことにあります。借金、クレジットカードは基本マイナスになります。
友人への貸し出し
⇒友人という容器を作り、そこに移動するイメージ
株、共済も同様に一つの容器への移動とします。ただし、複式簿記対応は全く考慮しないので、評価益、評価損などの計算は考慮しません。保険や年金などは、掛け捨て同様扱いとし、つまり出費とします。
属性の詳細を決めます。必須か、型は、レングスは? 可能な文字、最大、最小値
参照制約:登録する際にマスターから選択するようにします。ただし取引先の登録は、マスターにあるものだけではなく自由テキストを許すようにします。
一意制約:各マスターの名前は、階層構造があるものを除いて一意にします。
親テーブルのレコードが削除された時のカスケードについて検討します。また削除の仕方として、論理削除を行うか物理削除を行うかを決めます。
論理削除を行うとき、その複雑さは一気に増します。アプリケーションの開発の複雑さが見た目ではわからないのは、こういう点にあります。論理削除は仕様も実装も複雑になるのでできればしない方がいいのですが、マスター削除などの問題もあって、現実のところ、論理削除にせざるを得ないときがあります。
しかし、単に誤削除を防ぎたいからなどの、ゴミ箱的な発想で要求するなら、その要求は退けた方がいいです。もう一回登録すればいいようなつまらないことに莫大な工数が費やされるのはやめにしましょう。論理削除すればゴミがたまることになります。それをどうするかというだけで検討・実装・テストの工数がかかります。誤削除を恐れるのなら別の形でログをきちんと残すようにした方がいいです。トリガーで削除したレコードをログに出力するようにしておきます。
論理削除に関する問題はこちらで述べています。
盛り込める機能、あるいはかけられる工数は限られているため機能を制限します。ユーザビリティを考慮して制限することもあります。
入金用出金用で科目を分ける。⇒今回は出金のみ
分割払い、リボルビングは厄介ですが、利息を別に扱うか、利息込みの値段を書くことにします。便利にするのであれば、何回払いか、利息はいくらかを入力すれば自動的に毎月の出費に加えられるというのがいいですが、今回は省略します。受取利息も支払利息同様別個に扱います。
予定入出金は自動的に入力されるのが便利ですが、今回は省略します。
各処理の中でのデータの流れを明確にするにはDFDを描きます。
複数の処理がそれぞれ独立しているのでない限り、またデータモデルだけで仕様を読み取れるなら不要です。流れを見たいときには有用です。
次にユーザ・インターフェース(UI)を規定します。データモデルを考慮するより先にユーザインターフェースを決める場合もあります。UIはユーザが実際に操作する画面であるため、具体的なイメージがつかみやすく、仕様を具体化していくためにも、早めに手をつけた方がいいです。GUIを多く使ったアプリケーションでは、外部仕様の大半はUIを規定することで決まることが多いのです。
UMLではユーザインターフェースは後回しにされています。ドメイン分析に比べてUIは変わりやすいというのがその根拠のようですが、変更になるのは瑣末なところで、重要な部分は変わらないし、仕様を明確にするためにも使った方がいいです。
どういう画面構成にするか、一連の操作を行う場合に複数の画面で処理するか、単一の画面で処理するかなど決めることはいろいろあります。
何らかの登録を行う場合、一画面の入力を少なくして、ウィザード形式で行う場合もあります。検索画面と結果表示画面を別々にする場合もあれば、同一にする場合もあります。
入力に対する確認画面を必要とするかどうか、それとも即処理をするかどうか、それともポップアップで簡単な確認を取るか。
完了画面を必要とするか、それとも処理後一覧などへ遷移させるか。
こうした検討から画面遷移図を作成します。画面遷移図は、アプリケーションの外部仕様面の全体像を提供し、また各画面間の関連をつかむのに非常に重要なものです。
さらに各画面の詳細に入っていきます。
入力フォームでは、一括入力形式にするかか単一入力形式とするか。
検索フォームでは、何を検索キーとするか。
一覧ビューでは、どの列を表示するか。
統計ビューでは、どのような統計を可能とするか。合計、平均、最大値、回数など。そしてグルーピングの方法、ソートの方法、絞込みなど。また結果の表示の仕方として、方法:数値、棒グラフ、円グラフ、積み立て棒グラフを使うなど。
このようなことを規定するために画面定義書を作成します。画面定義書では、画面のラフスケッチ、表示される項目(基本的に動的に変更される部分だけでよい)の定義、入力フォームの場合各項目のバリデーション定義、各コントロール(ボタンやプルダウンなど)をクリック時のイベント、その画面を表示させるための処理概要、その次の画面に行く際の処理概要等について記述します。
画面のラフスケッチは最初はExcelで十分です。これはあくまでも個人のメモ書きか社内での検討材料であって、客の打ち合わせには、htmlを使って書いた方がいいかもしれません。あとでhtmlの紙芝居、プロトタイプを作ったときにキャプチャして差し替えた方がいいでしょう。
デザインは無視して、簡単な構造を書きます。表形式で1セル1項目書き、幅は適宜調整、ボタンやテキストボックスはは罫線で囲み、プルダウンは▼を使い、リンクはアンダーバーを使います。セルを方眼紙のようにしてする方法もあり、その方が、柔軟ですが、罫線を描くのが先のより少し手間がかかります。
このように定義して行く中で詳細な仕様を決定していきます。
入出金一覧画面では、ある容器についてFromかToを指定すると、入金か移動、もしくは出金か移動のみで、入出金すべての検索ができないため、すべてを選択して、FromとTo両方同じ容器を指定した場合、その容器に関するすべての入出金を表示するように変更しました。この辺はUIを簡易な状態にしながら様々な条件に対応できるようにするための工夫となります。
Webアプリケーションでは、最初にhtmlで紙芝居を作るのは大きな利点があります。JSPでは最終的にhtmlになるので、実際の画面に等しいものを見せながら仕様の確認ができ、そのhtmlを流用してJSP化すればすぐに開発に利用することができます。
外部仕様が明確になればテストケースも作成できます。テストを実装より先に作る利点は、実装すべき仕様を明確にできる点で、開発者はそれに沿って実装を行います。欠点は、実装時に様々な条件が追加された際にテスト仕様書を再度作成しなおす必要があることです。実装後であれば、実装されている機能・条件をすべて網羅できますが、外部仕様から漏れていることに気づかないこともあります。
テストの自動化ができるものと手動でなければできないものとを分離しておくと、テスト漏れを防ぐことができます。
まずシステムの全体の構造を決めます。
そして、アプリケーションの基本的なクラス構成を決めます。しばしば何の言語・DB・フレームワークを使うかなどは、内部設計の段階に入るずっと前に、ユーザ側の要件の中ですでに決まっていることもあります。
ここでは、実装は、Tomcat + Struts + DbUtil + MySQL(Oracle)
開発は、Eclipse + JUnitで行っていきます。
アプリケーションのレイヤー構成は、
Presentation + Control + Logic + DAOという構成にします。Logicが薄ければControlとDAOをつなげてもよくします。例外を認めるなら、どうやって例外パターンを実装するのかも明確にします。機能によって膨大なロジックを必要とするので、そういったときに、一つのクラス、メソッドが肥大化しないように、外に逃がせる仕組みを作っておきます。レイヤーにこだわりすぎると、中継以外何もしていないクラスが増えることになるので注意が必要です。ただし、途中でこの構成が変わると大変なのでアプリケーションの規模を考慮して設計することが重要です。
データベースの物理設計、ソースのコーディングに入る前に様々な規則を決めておく必要があります。命名規則、様々なコーディング規則など。この辺については、こちらで議論しています。
・パラメータをどのように使うか
パラメータとして使用する文字についてもなるべく規定しておきます。例えば登録・更新・削除を指示するパラメータとしてcrud=cなどといった形にします。
・マジックナンバー
DBに格納する値の意味を規定します。数値とアルファベットとどちらがいいのか。アルファベットならわかりやすそうですが、あまり長くするのはスペースの無駄なので略号を使うことになり、そうするとわかりにくくなります。
例えば入出金種別では
0 出金 1 入金 3 移動
とするか、
O 出金 I 入金 M 移動
とするかを規定します。このような定数値は、DBそしてプログラムですべて統一するようにします。ばらばらだと後で厄介になります。
どの開発環境を使うのか。ツールは何を使うのか。どのような手順でビルドを行い、単体テストはどのようにするのか。ライブラリは何を使い、フレームワークに従ってどのように実装するのか。サーバへどのようにディプロイするのか。成果物をどう管理するのか。共同開発の際のソースの管理は?バージョン管理は?
開発を始めるに当たって決めることはたくさんあります。チームで開発を行う場合、個々がばらばらなことをしないようにきちんと決めておく必要があります。
必ず開発手順書を作成します。そこにはゼロからの開発環境のセットアップ方法から、実装のための規則、統一的な記述方法なども記しておきます。
業務アプリケーションの作りは大体似たようなものです。テーブルに対して、検索、一覧表示、個別参照、登録、更新、削除を行うので、一つ一連の流れについてサンプルを作ってしまうと、他のテーブルについても同様に行えばいいので、開発が楽で、統一された形で行うことができます。
コピー&ペーストはよくないといわれますが、結局コピー&ペーストは使用します。サンプルに対してSedを使って別のページに置換し、ちょっと修正すれば、簡単にプログラムが出来上がってしまうことがあります。効率化できるところは効率化します。ただし、元のサンプルが不適切だと後で修正箇所が多くなります。
テストの自動化を行うためには、Viewとロジックは分離させ、Viewの中にロジックを含まないようにします。分離しているとロジック単体でテストをすることができます。ロジックをPOJOにするとアプリケーションサーバを起動せずにテストができます。StrutsMockTestCaseを使うと、MockのRequest、Response、Sessionオブジェクトが提供されるため、同じようにアプリケーションサーバを起動せずにテストができます。
ユーザの誤操作や悪意あるユーザへの対応を行います。特にWebアプリケーションではブラウザの特性を押さえておく必要があります。
・ブラウザ誤動作対策@二度押し防止(登録・更新・削除を二度押しする)⇒トークンの使用
Aブラウザback対策(完了ページにまた戻ろうとする)⇒トークンの使用
B直接ページに来る(リストの後に編集画面、確認画面と来ることを想定しているときに、編集画面、確認画面のURLの直打ちあるいはブックマークによってきた場合)⇒エラーページに送り、メニューに戻す。
・不正な文字へのサニタイジングhtmlでテキスト表示する際に実体参照文字へ変換
SQLインジェクションに対してはPreparedStatementを使用。PreparedStatementを使用しない場面では、シングルコーテーションを変換
・パラメータ改ざん対策サーバ側のセッションにグループIDを保持し、DB検索時にグループIDを付加して、自分のグループ以外のものが参照できないようにする。
Javascriptとサーバサイド両方で行う。データベースへ接続して確認するなど、サーバサイドでしかできないものもありますが、基本的に両方で行います。しかし、Strutsの制限から、javascriptでValidationが提供されないものもあります。javascriptを自作すればできますが、工数がかかるため、サーバサイドでしか行わないものもあります。
具体的なソースコードの実装をする前に、細かい仕様を規定しておきます。以下は検討しドキュメントに含める一例です。
・残高---手動による残高入力と計算残高
残高を入力した日付以降の残高は、それをもとに計算されます。
・時刻
時刻を省略した入力は、00:00:00とします。
・論理削除問題
マスターが論理削除された場合の、一覧・参照・更新画面の表示について規定します。
ユーザ画面:グループ(親が削除されていた場合は表示しない)
入出金画面:容器、取引先、科目、利用者(一覧や削除確認では表示する。更新画面では選択されている場合のみ表示、登録画面では表示しない)
クレジット画面:銀行口座 (参照されている場合削除できない)
このアプリケーションでは、統計計算や残高計算は算出が結構複雑になります。そのため、どのような手順で算出しているのかアルゴリズムを明確にしておきます。
容器テーブルに入出金額を格納しているわけではなく、それとは別の入出金テーブルに記録しているため、残高の算出の仕方は少し厄介なことになりました。
この辺の計算手順は、テーブルをどう設計するかで変わってきます。一般的に言って入力のしやすさとデータの加工のしやすさは反比例します。入れるのは容易いが出すのが難しい、あるいは入れるのは難しいが出すのは簡単です。どこかにツケは回ってきます。
また様々なルールも明確にします。ここでは、入出金一覧で簡単に現在残高を入力できるボックスを作ったのですが、訂正する手段はないので、最初の登録から1時間以内に登録した場合は、そのデータを更新したものと見なすなどのルールを加えました。
・階層構造のアルゴリズム
Oracleのconnect by prior を使えば、一切処理を書くことなく、SQLだけでできてしまうのですが、他のDBを使う場合、また階層数が未定の場合、自分で実装する必要があります。
外部設計、基本的なクラス構成、コーディング形式、定数などのルールをを決めたら、個々のクラス設計、メソッド設計は実装者に任せられます。複雑なロジックは事前に見通しを立てて設計し、またドキュメント化する必要がありますが、それ以外は通常のプロジェクトでは最初に個々の設計を行いません。
テストファースト、インターフェースの規定をしてから実装に取り掛かるのが理想ですが、実装しながらテストも作成し、コメントも後からつけ、プログラムに関するドキュメントはJavadocで出力するということも少なくありません。
こうして設計が終わったあと、実装をやりつつ内部設計を行い、テストフェーズを経て晴れてリリースとなります。
出来上がったアプリケーション(α版)は以下で見れるようにしています。
http://hoge.gotdns.com/acctbook/ユーザID | user1 |
パスワード | user1 |
管理者用ID | admin01 |
不具合を発見された方は報告いただけると助かります。