Webエンジニア susumuis の技術ブログ

このブログの内容は個人の見解であり、所属する組織の公式見解ではありません

BirdieMartのアーキテクチャについて

お礼

BirdieMartプロジェクトに参加してくれたid:syachonosaruくん、早速ソースをダウンロードして、いじってくれて、どうもありがとう!

僕のほうも、ソース投げただけで、何も説明なしだったので、これまでフレームワークを作ってきて、想定していた使い方について説明したいと思います。

まず、コンセプトは、

です。

理由は、

  • データベースアクセスはSQLを直接書くことについて
    • うちの会社では、システム設計の時にテーブル設計までしてしまうのが普通で、そのために、ActiveRecordは使用しない。
    • ロジックをオブジェクティブにしなくても、単純なテーブルの出し入れで済む場合がほとんどで、そのため、SQLを書くだけで十分。よって、DAO的なフレームワークは使用しない。
    • SQLを1箇所にまとめると、SQLのバリエーションでメソッドの数が爆発的に増えてしまいがち、また、1箇所修正時、変更点が各画面に伝搬してしまう。それなら、画面ごとにSQLを書いてもらった方がシンプル。そのために、ロジックのコードにささっとSQLが書けるようなアーキテクチャが良い
  • もちろん、これらの状況が異なる場合も考慮して、選択できるようになっているのが理想!
  • ビジネスロジックは難しい文法を使わずに素直に書ける
    • ビジネスロジックは、デザインのロジックよりも簡単になることが多く、また後で修正が入りやすい箇所でもある。そのため、複雑な文法で楽をするよりも、冗長でもシンプルな方が好まれる。従って、構造化プログラミング的に書けた方が良い。
  • もちろん、これも状況が異なる場合に、選択できるようになっているのが理想!
  • デザインとロジックのプログラマーの分業が可能について
    • プログラミングの難易度や、プログラマーのキャリアに対する考え方で、分離した方がいいのではないかと思う。前者は生粋のプログラマーで、プログラミング技術に興味があるタイプで、後者はシステムの「仕様」を考える、いわゆるSEタイプではないだろうか。
  • ロジックのコードは、Wicketとは完全に分離をする件について
    • ビジネスロジックは、本来、ServletWicketなどのアーキテクチャに依存しない、独立した概念である。だから、分離が可能なはず。
    • そして、これをPOJOのように独立させることで、画面の完成を待たずに単独でテストをすることができる。
    • ただし、POJOにこだわることはなくて、よりビジネスルールの記述に特化したベースクラスを継承してプログラミングするスタイルにすれば望ましいのではないだろうか。
  • デザインのコードはWicketコンポーネントを多用して再利用性を高める
    • 言うまでもなくこれがWicketの特性なのだから、活用しないのはもったいない。

以上をふまえてここまで作ってきたアーキテクチャを簡単に紹介します。

現在のアーキテクチャ

まず、SQL処理を円滑にするために、独自のデータモデルを作りました。

DataBean

これは、Mapをラップしています。
なぜこのクラスを作ったかというと、毎回Mapと入力するのを楽するだけでなく、データベースにSQLでアクセスすることを前提に考えたので、すべてのデータは文字列として表現可能のはずです。*1

また、これは、適切な型にキャストしながら取得できる便利なメソッドを持っています。

  • 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なので、対外的なインターフェースとしては、List>として表現できます。
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を使うため、ビューやページング機能を取り除いたところ、メッセージとパラメータのみしか残りませんでした。