Hatena::ブログ(Diary)

じゅんいち☆かとうの技術日誌 このページをアンテナに追加 RSSフィード Twitter


はてなブログに引っ越しました。

2010-09-27

[] ウェブアプリケーションの構造について

日経ソフトウエア 11月号の特集2で「最新Eclipseで良いJavaプログラムを書こう」に関連する話題として、さらに視野を広げて実用的なウェブアプリケーションでのレイヤー構造とかドメインオブジェクトの関係はどうなるのか?という点について解説してみたいと思います。(まだ日経ソフトウエア11月号を手にしていない方はぜひ買ってくださいw)

結論から先に出しますが、ドメイン駆動設計では一般論として下図のようなレイヤー構造やオブジェクトの関連が提唱されています。

f:id:j5ik2o:20100928014857p:image

ドメイン層のオブジェクトについては変わりないのですが、ドメイン以外のレイヤーに新しく2つのサービスが登場しているので、まずそこから簡単に説明します。

ドメイン層以外のサービス

実はサービスはドメイン層だけではなく、アプリケーション層とインフラストラクチャ層にも存在する場合があります。その役割を以下にまとめてみました。

レイヤーオブジェクト役割
アプリケーション サービス アプリケーション固有のサービスを提供するクラス。ドメインオブジェクトドメイン層のサービスを使って、アプリケーション固有の振る舞い(ビジネスロジック)を実装します。多くの場合はビジネスロジックアプリケーションのサービスクラスに実装して、UI層から利用することになります。
インフラストラクチャ サービス インフラストラクチャ固有のサービスを提供するクラス。たとえば、データベースネットワークに対する振る舞いを提供するサービスです。たとえば、データベースの特定のテーブルに検索や更新系のクエリを送信するサービス。*1ネットワークであればサーバクライアントの機能を提供するサービスなどが考えられます。

ドメイン層のオブジェクトは、使いようによっては様々な要件をアプリケーションとして実装できる汎用的な部品群です。そのため、アプリケーション固有な要件はドメイン層ではなく、アプリケーション層のサービスに実装します。ドメインは汎用的に、アプリケーションはそれらを使ってより具体的に表現します。また、ドメイン層はビジネスにおける問題領域を扱うことが責務なので、データベースネットワークについてはインフラストラクチャ層からサービスとしてドメイン層にインターフェイスを提供するようにします。このようにレイヤーの責務を逸脱しなければ、ドメイン層の見通しがよくなり、わかりやすくて理解しやすい設計に近づけることができます。

DXO(Data eXchange Object)とは

上図ではUI層のモデルと、ドメイン層のエンティティ、インフラストラクチャ層のエンティティは、いずれも似たような情報を使います。たとえば、顧客の名前などを保持した情報です。その情報を画面から受け取ってDBに保存したり、DBから検索して画面に表示したりします。(上図のDXOの矢印の部分です。矢印の配置が悪いのですが、変換対象はドメイン層のエンティティやバリューオブジェクトになります)似たような情報になるはずなのに、レイヤーがわかれて別々のオブジェクトを定義しているのはなぜでしょうか?

その理由を、典型的なユーザ登録画面の内部を例に考えてみたいと思います。

仮に以下のようなクラス郡がある前提とします。具体的なソースは下部にあります。

大まかな処理内容を以下に示します。

UI
  • ユーザ登録画面で入力された情報はRegisterUserFormに格納される
  • 次にRegisterUserAction#doRegisterメソッドが呼ばれます。引数には1のRegisterUserFormが渡される。
  • RegisterUserFormDxo#convertUserメソッド引数にRegisterUserFormを指定し、Userに変換する。この際、RegisterUserFormの姓と名、姓かなと名かなの独立しているプロパティを、Nameに変換している。また、RegisterUserForm#getRegisterDateは画面の都合上、文字列の型ですが、ドメイン上では扱いやすくするためにjava.util.Dateに変換している。
  • UserRegisterService#registerメソッド引数に渡し、呼び出す。
アプリケーション
ドメイン
  • UserRepositoryInDB#storeメソッド引数にUserを指定して呼び出す。
  • UserRepositoryInDB#storeメソッド内では、UserDxo#convertUserTableメソッドでUserTable, UserDxo#convertUserProfileTableでテーブルクラスに変換する。特にconvertUserProfileTableメソッドでは、Userが持つそれぞれのNameを姓と名、姓かなと名かなに変換している。また、UserProfileTable#setRegisterDateは、Timestamp型なので変換が必要となる。
  • さらにUserDao, UserProfileDaoを使って、データベースに対して登録処理を行ないます。

レイヤーが異なるとそれぞれのオブジェクトの持つプロパティの形式や型が変わってくることがわかったと思います。このようなデータの変換処理を行うオブジェクトのことをDXOと呼びます。

レイヤが異なる毎に、RegisterUserForm -> User -> UserTable, UserProfileTableの3つのクラスが登場しました。なぜならば、同じ情報でもレイヤーの目的に応じて形を変えて存在するからです。レイヤーを無視して、RegisterUserFormをドメインに持ち込んだり、UserProfileTableをドメインに持ち込んだりしていては、ドメイン層は混乱します。この説明では登録処理ですが、編集処理の場合だと UserProfileTable -> User -> RegisterUserForm のDXOを行って、画面に情報を表示する必要があります。つまり逆の流れの変換処理もあります。*2

それぞれのレイヤーの責務に応じたモデルの型を扱うことで、レイヤーとモデルの混同を防止できます。たとえ、同じプロパティメソッドを持っていても、レイヤーが異なると概念が別なので、型としては異なるべきだと考えます。そうすることでレイヤーの独立性を維持できると考えています。

ドメイン駆動設計も設計手法の一つに過ぎないので、お作法を守りながらもより良い設計を求めていく姿勢が大事だと思う今日このごろです。何事も「守破離」ですね。

あわせて読みたい

http://d.hatena.ne.jp/daisuke-m/20091110/1257838467

UI

// HTML上のユーザ登録フォームを表すクラス
public class RegisterUserForm {

    public String getFirstName(){/*省略*/};

    public String getFirstKanaName(){/*省略*/};

    public String getLastName(){/*省略*/};

    public String getLastKanaName(){/*省略*/};

    public String getRegisterDate(){/*省略*/};

}

// ユーザ登録画面のコントローラクラス。
public class RegisterUserAction {

    private UserDxo userDxo = new UserDxo();

    // 登録ボタンを押された時の処理
    public String doRegister(RegisterUserForm form){
        // DXOしてサービスを呼び出す
        userRegisterService.register( userDxo.convertUser( form ) );
    }

}

// RegisterUserFormをUserに変換するためのDXO
public class RegisterUserFormDxo {

    public User convertUser( RegisterUserForm form ){
        // DXO
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        Date registerDate = sdf.parse( form.getRegisterDate() );
        User user = new User( User.newIdentity(),
                        new Name( form.getFirstName(), form.getFirstKanaName() ),
                        new Name( form.getLastName(), form.getLastKanaName() ), registerDate );

        return user;
    }

    // ドメインオブジェクトをフォームに変換する場合、編集画面などで利用できる。
    public RegisterForm convertForm( User user );

    // 既存のインスタンスに変換する場合はこのようなインターフェイスになる。
    public void convert( RegisterForm form, User user );
    public void convert( User user, RegisterForm form );

}

アプリケーション

// アプリケーション固有の処理(ビジネスロジック)を扱うサービス。
public class UserRegisterService {

    private UserRepositoryInDB userRepository = new UserRepositoryInDB();

    public void register(User user){
       // ビジネスロジック 前
       userRepository.store( user );
       // ビジネスロジック 後
    }

}

ドメイン

// 名前とかな名を扱う”名前”というバリューオブジェクト
public class Name implements ValueObject<Name> {

    public Name(String name, String kanaName){/*省略*/};

    public String getName(){/*省略*/};
    
    public String getKanaName(){/*省略*/};

    public static String newIdentity(){/*省略*/};

}

// ユーザを識別するエンティティ
public class User implements Entity<User, String> {

    public User(String identity, Name firstName, Name lastName, Date regsiterDate){/*省略*/};

    public String getIdentity(){/*省略*/};

    public Name getFirstName(){/*省略*/};

    public Name getLastName(){/*省略*/};

    public Date getRegisterDate(){/*省略*/};

}

// UserをUserTableやUserProfileTableに変換するためのDXO
public class UserDxo {

    public UserTable convertUserTable( User user ){
        UserTable ut = new UserTable(user.getIdentity());
        return ut;
    }

    public UserProfileTable convertUserProfileTable( User user ){
        UserProfileTable upt = new UserProfileTable(user.getIdentity());
        upt.setFirstName(user.getFirstName().getName());
        upt.setFirstKanaName(user.getFirstName().getKanaName());
        upt.setLastName(user.getLastName().getName());
        upt.setLastKanaName(user.getLastName().getKanaName());
        upt.setRegisterDate(user.getRegisterDate().getTime());
        return upt;
    }

}

// ドメインにおいて汎用的な永続化を扱うリポジトリ。
public class UserRepositoryInDB implements Respository<User, String> {

    private UserDxo userDxo = new UserDxo();

    public void store( User user ){
         if ( isExist( user ) == false ){
             userDao.insert( userDxo.convertUserTable( user ) );
             userProfileTableDao.insert( userDxo.convertUserProfileTable( user ) );
         } else {
             // 略
         }
    }

}

*1:DAO=データアクセスオブジェクトもサービスの一つと考えられます

*2:この例では手動でオブジェクト間のデータの詰め替え作業を行っていますが、commons-beanutilやSeasarS2DxoやS2BeanUtilsを使えば手間がかなり軽減できます。ご参考までに。

アーキテクトKアーキテクトK 2010/09/29 23:31 気持ちはわかるのですが、こんなFATな設計を実装できるほど予算のあるプロジェクトなんて今どきないんですよね…
そんな時代だし、設計思想や言語だってどんどん変わって行っちゃうからこそ、設計的にも言語的にもライトなものが全盛なんじゃないかと思うのですが、いかがでしょうか。

もうすこし現実的な「現代版ドメイン駆動設計」を披露していただけると嬉しいです。

j5ik2oj5ik2o 2010/09/29 23:49 上記のサンプルソースはイメージをつかんでもらうためのモデルなんでそれをすべて作らないといけないかというそうではないです。YAGNIでよいと思います。

とはいえ、DDDをもう少しライトにする方法論は必要ですね。DXO、ファクトリ、リポジトリなどはフレームワークを応用すればもう少し楽に書けるかもと思っているところです。

> もうすこし現実的な「現代版ドメイン駆動設計」を披露していただけると嬉しいです。
ありがとうございます。ちょっと考えてみますね。

lesamoureuseslesamoureuses 2012/03/20 00:41 こんにちはこんにちは。
あの、記事の本筋と逸れるのですが、
わざわざuserTableとuserProfileTableという風に二つに分けているのはなぜなのでしょうか?
何か明確な理由があったら倣いたいなと思いまして。

j5ik2oj5ik2o 2012/03/22 18:56 UserTableにユーザプロファイル情報を含めることはできます。しかし、カラムの数が多い場合は目的の情報を探すのが困難になるので、個人情報に関連する情報はプロファイルという別のテーブルに切り出した方が管理しやすいというモチベーションですね。タンスの中に服がたくさん入っていて、その中から靴下を探すのはたいへんだけど、下着は下着、靴下は靴下の棚に分けて管理した方が扱いやすいのです。

lesamoureuseslesamoureuses 2012/03/23 09:27 なるほど、ありがとうございます。
「永続化するときになるべくオブジェクトに沿らないといけない」みたいなイメージがあったのですが、そこはDXOがうまいことやってくれるから気にしなくていいのですね。
ありがとうございますありがとうございます。

ななしななし 2017/08/17 18:00 何を持って FAT なのかってことだと思うので、人によっては FAT に見えるのは理解できます。
でもその FAT って部分的にトラウマ(色眼鏡)が含まれていることってありえませんか?

個人的には FAT には見えないですね。私の理解では、個別のソースはかなりすっきりすると思います。

実は FAT ってコードだけでなく設計やマネジメントなど全般的に考えないといけないテーマかもしれませんね。

どこかにゆがみがあるから、FAT に見えてしまうと思います。