BirdieMartのアーキテクチャについて
お礼
BirdieMartプロジェクトに参加してくれたid:syachonosaruくん、早速ソースをダウンロードして、いじってくれて、どうもありがとう!
僕のほうも、ソース投げただけで、何も説明なしだったので、これまでフレームワークを作ってきて、想定していた使い方について説明したいと思います。
まず、コンセプトは、
- データベースアクセスはSQLを直接書く
- ビジネスロジックは難しい文法を使わずに素直に書ける
- デザインとロジックのプログラマーの分業が可能
- ロジックのコードは、Wicketとは完全に分離をする
- デザインのコードはWicketのコンポーネントを多用して再利用性を高める
です。
理由は、
- データベースアクセスはSQLを直接書くことについて
- もちろん、これらの状況が異なる場合も考慮して、選択できるようになっているのが理想!
- ビジネスロジックは難しい文法を使わずに素直に書ける
- ビジネスロジックは、デザインのロジックよりも簡単になることが多く、また後で修正が入りやすい箇所でもある。そのため、複雑な文法で楽をするよりも、冗長でもシンプルな方が好まれる。従って、構造化プログラミング的に書けた方が良い。
- もちろん、これも状況が異なる場合に、選択できるようになっているのが理想!
- デザインとロジックのプログラマーの分業が可能について
- ロジックのコードは、Wicketとは完全に分離をする件について
以上をふまえてここまで作ってきたアーキテクチャを簡単に紹介します。
現在のアーキテクチャ
まず、SQL処理を円滑にするために、独自のデータモデルを作りました。
DataBean
これは、Map
なぜこのクラスを作ったかというと、毎回Map
また、これは、適切な型にキャストしながら取得できる便利なメソッドを持っています。
- DataBean#getDataAsInt(String) : intとしてキーに割り当てられた値を取り出す
- DataBean#getDataAsBigDecimal(String):同じくBigDecimalとして値を取り出す
- DataBean#getDataAsDate(String):同じくDateとして値を取り出す
- DataBean#getDataAsCsv(String):カンマ区切りでセットされたデータを分割してCollection型として取り出す
こんな感じです。
Wicketとの連携では、CompoundPropertyModelが相性良いです。
PropertyModelはMapのキーにもアクセスできることを利用しています。*2
ListDataBean
これは、List
DataBeanはMap
SQLのselect文の結果を効率的に格納するために作られています。そのために、ResultSetをパラメータにして初期化することもできます。
ISimpleStatement stmt = newSimpleStatement(dbManager); stmt.append("select * from item"); ResultSet rs = stmt.executeQuery(); ListDataBean list = new ListDataBean(rs, 1, 10); // 1行目から10行目まで
Excelのようなスプレッドシートのバックエンドでも使えることを想定していて、行番号決めうちでアクセスすると、都合に合わせて行をaddしてくれます。
ListDataBean list = new ListDataBean(); list.setListData(10, "NAME", "山田");
このようにすると、裏で、list.add(new DataBean())を10回繰り返した後、list.get(10).setData("Name", "山田")を実行します。
フレームワークのしくみ
フレームワークはstaticメソッドで大半の機能を提供します。
これは、グローバス関数そのものです。
フレームワークのクラスをstaticインポートすれば、クラス名なしでいきなり関数が使えます。*3
例:
import static jp.sourceforge.birdiemart.core.Birdie.*; // 〜省略〜 public ListBean getList() { DBManager dbManager = getDBManager(); // 省略 }
static関数を使っていつつ、拡張性も考慮しました。
フレームワークのstatic関数のほとんどは、設定されたインターフェイスに処理を委譲しています。
インターフェイスの設定は初期化時に行います。Wicketの場合は、Application#init()で行います。
また、Webアプリケーションでは、基本的に1スレッド=1リクエストと考えて良いので、スレッドローカルでリクエストの情報を格納しています。
// RequestCycle内 BirdieConfig.setProcesingRequest(getWebRequest().getHttpServletRequest()); // 主にログ出力で使われる BirdieConfig.setProcesingResponse(getWebRequest().getHttpServletRequest()); // 主にログ出力で使われる BirdieConfig.setDefaultDataSourceNameOfThisThread("hogehoge"); // getDBManager()としたときの、データベース接続先ここを動的にすればASP化できる!
ビジネスロジックの記述
ビジネスロジックはPOJOでもいいのですが、よくよく考えると、ビジネスロジックの記述で必要なことは
- パラメータ入力
- 処理
- メッセージ出力
だと思います。
そこで、メッセージング(error, warn, info)担うIMessageBeanというインターフェイスを作り、パラメータ入力を担うDataBeanのインターフェイスを抽出したIDataBeanを作り、両者をMixinした感じでBusinessBean*4を作りました。
public class BusinessBean implements IDataBean, IMessageBean { private MessageBean message; private DataBean data; // 省略 }
ここで悩みどころ:
ビジネスビーンのメッセージと、WicketのComponent#error(Serializable)などとのうまい連携が思いつきません。
Behiviorを作って、beforeRenderにメッセージをコピーする処理を書いてみたのですが、うまくいきませんでした、これは、BehiviorのbeforeRenderがComponentのonBeforeRenderよりも後に処理されるからです。
仕方がないので、各イベントで、ユーティリティーメソッドを使って、メッセージの転送を行うアイディアしか今のところありません。
TableBean
データベースを前提にしているので、データベースの定義を見に行ってモデルが作られるのが理想だと考えました。
その役割を果たすのがTableBeanです。
これは、
newTableBean("テーブル名")
とやると、データベースの定義を見に行って、列名や列の制約を設定してくれます。
そして、TableBean#check()メソッドを実行すると、テーブルに入れることができない値がある場合はエラーメッセージを返してくれます。
TableBean tableBean = newTableBean("ITEM"); tableBean.setData("ITEM_CD", "1"); tableBean.setData("ITEM_NAME", "ボールペン"); String[] error = tableBean.check(); if (error != null) { error(error); }
今は、これは、メッセージのリストをreturnするようになっているのですが、考えてみたらこれはBusinessBeanのロジックの一種のようにも見えます。なので、TableBeanをBusinessBeanのサブクラスとして再設計するのもありかなと思っています。
そうすれば、errorをnullチェックしなくても、以下のように書けます。
TableBean tableBean = newTableBean("ITEM"); tableBean.setData("ITEM_CD", "1"); tableBean.setData("ITEM_NAME", "ボールペン"); if (tableBean.check()) { error(tableBean.getError()); }
*1:※本当はDBにはオブジェクトを入れることもでき、実際はPreparedStatementなどで、SQLに値を直接記入することはあまりしないですが、便宜上すべてSQLを経てデータを出し入れすることに制限します
*2:じつは、僕がこの仕事に着手する前のDataBeanは、Mapとの互換性がありませんでした。そこで僕が最初にした仕事は、PropertyModelを改造してDataBeanに対応するものを作ったのですが、後にDataBeanを改造した方が良いことに気づき、何度か修正を経て今の形になりました
*3:これも、元々は継承前提で、ベースクラスのメソッドといて定義されていました。そのため、POJOからフレームワークにアクセスするためには、非常に遠回りをする必要がありました。Java5からstaticインポートがサポートされたので、これを使って、どんなクラスからも、フレームワークを使いやすいようにしました
*4:実は元々はBusinessBeanは、僕がこの仕事に取りかかる前の、社内フレームワークの根幹部分でした。BusinessBeanは、データやメッセージの他に、ページング情報や処理モード、現在のリクエストなどを持った重量級のベースクラスで、これのサブクラスにビジネスロジックを記述するだけでなく、フォワード先のJSP上に渡すことによって、JSTLのようなタグライブラリを使わずにメソッド経由で様々な表示を行うビュー層のヘルパーAPIも備えていました。僕もこれを使ってプログラムを書いていた者の一人として、この機構はカッコ良くはないけど、とても使いやすいと思っていました。今回Wicketを使うため、ビューやページング機能を取り除いたところ、メッセージとパラメータのみしか残りませんでした。