今とある新規社内システムの設計と開発を担当しています。
ソフトウェアアーキテクチャはおいらが考えました。
簡単に言うとこんな感じです。
- UI層/サービス層/データアクセス層の3レイヤーに分ける
- 開発言語はC#、UIはASP.NET、データアクセスはADO.NETを採用 (バージョンはいずれも2.0)
- データのやり取りはデータベーススキーマから自動生成した型付けデータセットを使う
MVCで言うところのModelが型付けデータセットで、ViewとControllerがASP.NETです。
サービス層は基本的にこの型付けデータセットを出し入れします。
つまりテーブルの行単位でデータをやりとりします。
で、こういうスタイルのアーキテクチャに慣れていない開発者には、どうもこれが非効率で面倒なアーキテクチャに見えるようです。
まあ確かに言わんとすることは分からんでもありません。
例えば本のISBN、タイトル、出版社名、コメントをGridViewに一覧表示する必要があったとします。
単純に考えると下のようなSQLを発行してGridViewにバインドすればほぼ終わりです。
SELECT A.isbn, A.book_title, B.publisher_name, A.book_comment FROM book A INNER JOIN publisher B ON B.publihser_id = A.publisher_id WHERE A.book_price < @book_price
サービスのAPIはこんな感じです。
public DataTable GetBookInfo(int price);
こう書けば画面に必要なデータがすべて取れるところを、型付けデータセットを使うとこんなクエリを発行する必要があります。
-- bookテーブルを取得(全カラム) SELECT DISTINCT A.isbn, A.book_title, A.book_price, A.publisher_id, A.book_comment FROM book A INNER JOIN publisher B ON B.publihser_id = A.publisher_id WHERE A.book_price < @book_price -- publisherテーブルを取得(全カラム) SELECT DISTINCT B.publisher_id, B.publisher_name, B.publisher_location FROM book A INNER JOIN publisher B ON B.publihser_id = A.publisher_id WHERE A.book_price < @book_price
この場合のサービスのAPIはこんな感じです。
public BookStoreDataSet GetBookInfo(int price);
クエリを2回発行していますし、UIでは使わない列まで取得しています。
さらにこれをGridViewに表示する場合、テーブル(コード上はDataTable)間のリレーションを考慮してGridViewにバインドしなければならないため、UI層ではもうひと手間必要になります。
ところで今度はデータを更新する場合を考えます。
たとえばコメントを更新する場合はこんな感じになります。
型付けデータセットを使わない場合
// UI側のロジック int isbn = this.GetIsbn(); string comment = this.GetComment(); this.service.UpdateBookComment(isbn, comment);
-- SQL UPDATE book SET book_comment = @book_comment WHERE isbn = @isbn
型付けデータセットを使う場合
// UI側のロジック bookStoreDataSet.book.isbn = this.GetIsbn(); bookStoreDataSet.book.book_comment = this.GetComment(); this.service.UpdateBook(bookStoreDataSet); // bookテーブルを更新する共通メソッド
-- SQL -- 自動生成されたUPDATE文を使用するので省略
更新時は型付けデータセットの方が若干有利に見えます。
というのもデータを格納する入れ物(データセット)も更新用のロジック/SQLも自動生成されたものを再利用できるからです。
さて、おいらはなぜ一見メリットが少なそうに見える型付けデータセットを採用したのでしょうか?
カラム名やカラムのデータ型をコンパイラがチェックしてくれるので、コードの安全性が増すというのも大きな理由の一つですが、それ以外にも以下のような理由があります。
- 画面ごとに専用の取得ロジック、更新ロジックを用意していると個別最適にはなるが、結局何も再利用できない。また開発者間で実装のスタイルがバラけてしまい、他人のコードが理解しづらい。
- 画面は変わりやすい。個別最適なアーキテクチャの場合、画面で必要な情報が増えたり減ったりすると、その都度取得ロジックや更新ロジックを変更する必要がある。つまり、UI層だけでなく、サービス層やデータアクセス層の変更も必要になる。
- たとえば、上の例で出版社の所在地(publisher_location)を追加表示する必要が出てきても、サービス層はすでにpublisherテーブルの全カラムを返却しているので変更の必要がない。つまりUI層の変更だけで改造が完了する。
まとめると、「テーブルスキーマをシステムのデータモデルと見なし、システム全体をモデルに依存する形で設計する。モデルは比較的安定しているので変更が発生しにくく、処理の再利用もしやすくなる」ということです。
こうした設計が変更に強く、保守性や拡張性の高いシステムの構築に繋がっていくわけです。
ただし、考慮点が全くないわけではありません。
まず、UI層は相対的に地位が低いため、UIはデータモデルと画面上の入出力項目をせっせこせっせこと変換する必要があります。よって、UI層のロジックは若干複雑化する可能性があります。
それからADO.NETの型付けデータセットに限って言うと、データの取得に関してはさすがに非効率な感が否めません。
テーブルごとに似たようなSELECT文を何度も発行するのはハッキリ言ってイケてないです。
画面ごとに開発者がSELECT文を作らなければいけない点もいただけません。
できればADO.NETではなく、オープンソースのO/Rマッピングツール等を活用して、SELECT文も自動生成できるようにした方が良いと思います。
(詳しくは知りませんが、最近のADO.NETでリリースされたEntity Frameworkなんかを使うとこのへんの問題が解決するのでしょうか?)
本当に小規模なシステムであれば個別最適なアーキテクチャでもさほど変わりはないかもしれませんが、システムが大きくなればなるほど、個別最適の観点からはNGでも保守性や拡張性を考慮した全体最適なアーキテクチャ(= モデルを中心としたアーキテクチャ)を採用するメリットが大きくなります。
・・・とまあ、こんな感じです。
ここまで長々と説明してきましたが、う〜んなかなか難しいですね。
メリットはあるはずなのに、どうもそのメリットを強調するような文章にならないのがもどかしいです。
このあたりをうまく説明できないと、レガシーなスタイルに慣れきっている人たちから「個別最適なアーキテクチャ」へ引き戻そうとするプレッシャーを掛けられるんですよね〜。
まだまだ自分の中でも完全に消化しきれていない部分があると思うので、今後も要精進っちゅーことですね。
なお、この話に関連するネタが以下のエントリにありますので、興味がある方はご参考までにどうぞ。