「サービス層」「DAO層」はアンチパターンだ

転職して、Java(Jersey)とScala(Play)で書かれたWebアプリケーションをメンテを1年半ほどする機会があったのですが、Java系で特によく使われている(ような気がする) 「Controller / Service / Daoの3層に分ける設計」アンチパターンであると思えてならないので、思っているところを書き出してみます。

そもそも「サービス層」「DAO層」とは何のことか

MVCパターンには色々ありますが、私が疑問に思っているのは、

  • 単にSQLを実行するだけの DAO層
  • DAOを呼び出して、ビジネスロジックを実装する サービス層
  • サービスを呼び出し、HTTPサーバーとしての振る舞いを実装する リソース層

という3層に分ける方法です。

*1。
# リソース層
class AccountResource {
    @Resource AccountService service; // DIでインスタンスが自動で代入される

    @Description("新規会員登録")
    @POST
    public HttpResponse register(@FormParam String userId, @FormParam String email, @FormParam String name) {
        // パラメータのバリデーションはResource層で行う
        ValidationResult v = validateRegisterParam(userId, email, name);
        if (v.isSuccess()) {
            return new RegisterValidationErrorResponse(v); // バリデーションエラ―時はサービス層を呼び出さずに、レスポンス
        }

        service.register(userId, email, name); // サービス層の呼び出し
        return HttpResponse.ok;
    }

    @Description("会員検索")
    @POST
    public HttpResponse search(@FormParam name) {
        if (StringUtils.isBlank(name)) {
            return HttpResponse.badRequest;
        }
        List<Account> accounts = accountService.register(param); // サービス層の呼び出し
        return new AccountsResponse(accounts);
    }

    // 他にも色々なメソッドを定義する・・・
}


# サービス層
class AccountService {
    @Resource AccountDao dao; // DIでインスタンスが自動で代入される
    @Resource EmailAddress emailAddressDao; // DIでインスタンスが自動で代入される

    public void register(String userId, String email, String name) {
        dao.insert(userId, name);
        emailAddressDao.insert(userId, email); // 歴史的な理由で accounts テーブル以外にも会員情報が保存されている
    }

    public List<Account> search(String name) {
        List<Account> accounts = new ArrayList<Account>();
        for (AccountRecord r : dao.selectByName(name)) {
            accounts.push(Account.createFromRecord(r));
        }
        return accounts;
    }    

    // 他にも色々なメソッドを定義する・・・
}

class AccountDao {
    AccountRecordMapper mapper;

    public int insert(String userId, String name) {
        return conn.executeUpdate("insert into accounts(id, name) values (?, ?)", userId, name);
    }

    public List<AccountRecord> insert(String userId, String name) {
        return conn.select("select * from accounts where name like '%' || ? || '%'", mapper, name);
    }
}
-->

アンチな点1:分割するルールがはっきりしない

  • サービスはドメインモデルの要素(『会員』とか『メッセージ』とか『注文』とか)ごとにクラスを作る
  • DAOはDBのテーブルごとにクラスを作る

のような、サービス・DAOのクラスを分割する(明示的 or 暗黙的な)ルールが、プロジェクトごとにあるはずです。

しかし、実際にはルールに沿わないサービス・DAOがはびこることになりがちです。

  • 「AccountServiceで『初回ログインメッセージ』を作成している」とか、
  • 「AccountDaoでは、accountテーブルとaccount_info テーブルをupdateしている」とか

また、理屈の上ではDAOやサービスは、どのようなクラス構成でも良いのです。

  • 「DAOは1クラスのみ、サービスも1クラスのみ」という一枚岩の構造でもよいし、
  • 「DAOはSQL文1つごとにクラスを作る、サービス『会員登録』『会員検索』『注文作成』『注文確定』のような1つ1つの操作ごとにクラスを作る」という極端に細かいな構造であったとしても実装できます。

アンチな点2:インターフェースがバラバラになりがち

分割する単位があいまいであるということは、どこにどんな機能があるかもあいまいになります。

例えば、

  • AccountService で会員を会員IDで取得するメソッドは findById
  • OrderService で注文を注文番号で取得するメソッドは findByOrderId

のような、一貫性がないメソッド名であっても、コンパイルは通ってしまいます。

また、コードレビューの際にも気づかれにくくなりがちです(違うサービスに違うメソッドがあるのは当たり前なので)。

これも、コードを書いたりメンテするときの負担になります。

アンチな点3:機能の共通化がしにくい

ActiveRecordなどのライブラリでは、

  • バリデーション
  • エラーメッセージ
  • 保存時に関連するテーブルも更新する

といった、どのモデルにも共通して必要になる処理を提供しています。

しかし、サービス・DAOの枠組みでは、このような共通処理の提供は考えにくくなるように感じます。

なんとなれば、サービス・DAOは「抽象的なモデル」であるというよりは、 「具体的な処理」の集まりに過ぎないからです(だから、分割するルールがはっきりしない)。

そのため、例えばバリデーションであれば、「整合性の条件を宣言的に記述し、保存時に検証する」というより、「insert メソッドの冒頭で assert 文でチェックする」というような発想になりがちです。

アンチな点4:リソース層がボイラーテンプレートになる

リソース層でデータを変換したり直接DAOを呼び出したりしない、「お行儀の良い」実装をしたアプリでは、リソース層のメソッドは、

  • リクエストパラメータを取り出し、
  • パラメータのバリデーションをして、
  • パラメータをサービスのメソッドに渡し、
  • メソッドの戻り値をもとにレスポンスを作る

という作りに、自然に収斂します。リソース層は、HTTPの世界と、プログラムの世界をつなぐだけの、ボイラーテンプレートになってしまうのです。本当は、そんなものは自動生成するべきなのです。

アンチな点5:何かやった気になる

このように、「サービス層」「DAO層」はかなり大雑把な分割方法なのですが、 その割に、「MVCパターン」「ビジネスロジックとWEBロジックの分離」に従った実装をしている気になりがちです。

代替案と、「サービス層」「DAO層」をあえて使うべき時

要するに「サービス層」「DAO層」は、単にメソッドの集まりにすぎず、構造化を支援する仕組みがないのです。だから、クラスを分割する基準で悩みます。抽象化されていないので、バリデーションをしにくかったり、ボイラーテンプレート書かざるを得なかったりするのです。

しかし、一定規模のアプリケーションになれば、何らかのオブジェクト指向的な構造が必要になってくるはずです。

例えば、Ruby on Rails では、DBのテーブルをActiveRecordで抽象化しており、バリデーションやエラーメッセージなどの便利機能を提供しています。よく「既存のテーブルのJOINが必要だからActiveRecordが使えない」と言われるのですが、その場合でもクエリを「ActiveModelを継承したクラス」でラップすることもできます。そうすれば、ActiveRecordと同様に、便利機能が享受出来るようになります。

もしも、アプリケーションがプログラマーが1人で片手間に作るような規模のものであれば「サービス層」「DAO層」でも、十分かもしれません。

でも、プロトタイプと思って作ったものが、いつの間にかサービス基盤になってしまうのが世の常なので、最初からオブジェクト指向的なことを考えて作ったほうが良いと思います。

*1:実際に動作するものではありません。クラス名とかを間違えると恥ずかしいので。