コンパイルエラーがあるままコミットされとった・・・

複数人で開発をしている時にテストコードにコンパイルエラーがあるコードをリポジトリからチェックアウトしてしまった時の対応方法で悩んだのでここに書いておく

TDDの手順としては下のような感じになる

  1. テストコードを書く
  2. コンパイルエラーを解消する
  3. テストが成功するように実装
  4. リファクタリング

で、テストコードにコンパイルエラーがあることに気がつくのは3の時

この時にどう対応するのが良いのか?

気がついた人はこんなことをすべき

  1. テストのコンパイルを通るように修正する
  2. コンパイルが通るようになったコードをプッシュする
  3. テストが通らないことを周知する
  4. テストのコンパイルが通らなくした人を探し出す
  5. テストのコンパイルが通らなくした人に修正を依頼する

下のようなフローも考えたんだけど、3にかかる時間が判らないなら作業が止まってしまうのでこのフローはダメな気がする。

  1. テストのコンパイルを通るように修正する
  2. テストのコンパイルが通らなくした人を探し出す
  3. テストのコンパイルが通らなくした人に修正を依頼する

本来論でいえば、CIでテストをスキップするのではなくテストを行ってプッシュした人にメッセージが飛ぶように設定しておくといいがそうではない場合もあるみたいなんでこんなことを書いてみた

ValueObjectの重複

意味的な面でValueObjectを作る指針を出している場合、明らかに同じものを別のValueObjectとしてあげる上げる時がある。

意味的な面での重複

営業部門から見ると、SoldDateだったり DeliveredDateだったりするけどこれは経理的に見ると計上日RecordedDateになる。
で、実際このValueObjectの中に書かれているメソッドはほとんど無い。
疑わしければ別のValueObjectにするというのは一見それで良いのだが、こんな感じで別々の概念となってしまい、同じコードを2か所に書くことになって修正も2か所しないといけなくなる。
DDDでいう変更するコードが一か所になって便利って話とは矛盾することになる。

疑わしさ

疑わしければ別のValueObjectにするというのは意外と壁が高くて、疑わしくない証明するのは悪魔の証明だ。つまり、一度別のValueObjectが出来たらそれは統合できないということを示す。
今回の例では、営業部門と経理部門という二つの部門をまたぐ比較的遠いシステムを上げた。しかし、ユースケースごとに担当者分けをした場合など、別ドメインだという認識で同じ実態のValueObjectが出来てしまう。
これはナンセンスだ!

まとめ

どこか実装する前にドメインをメンバー内で周知し統合するような手順が必要なのではないか?

toString()を誰が持つのか

javaをしてればObject#toString()があるのをご存知だと思います。

今回DDDでモデルの中にtoString()があるのに違和感を感じる件について言ってみたいと思います。

モデル内のEntityやValueObjectにCommons-langとかにあるToStringBuilder#reflectionToString(Object, ToStringStyle)なんかを使ってちゃっちゃと表示するという場合に下みたいなのを書くことになる。

@Override
public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}

そうすると、AbstractなEntityやValueObjectを作りたくなってくる。だけどDDDではこれはドメインモデルを汚染する行為という話だ。

そうすると各クラスにこれを手書きするのか?そういう疑問も湧いてくる。

これは書かないというのがDDDとしては正しいようだ。

Sysout.out.println( AnyToStringBuilder.reflectionToString(anyObject));

のような感じがDDDを厳密に考えた時には正しいアプローチのようだ。これでEntityやValueObjectが綺麗に保たれるって仕組みだ。

1:0...1の関連[DDD]

リポジトリを設定するのは誰なのか

Hoge案件をHuga承認するというイベントがあった場合、1:0...1の関係を現すのに下(コード例:1)のような感じにしている。

コード例:1

public HogeEntiry {
    private HogeId hogeId; 
    private FugaEntity fugaEntity;

    public HogeEntity(){}

    public HogeEntity(HogeId hogeId, FugaEnrity fugaEntity){
        this.hogeId = hogeId;
        this.fugaEntity = fugaEntity;
    }

    // ... ゲッターは省略
}

そこで承認時のメソッドをモデル追加する

その中では承認を行った際にFugaEntityをpersistするためのFugaEntityRepositoryをどこで設定すべきなのか?

コード例:2

public HogeEntity {

    //FugaEntity用のリポジッリ
    private FugaEntityRepository fugaEntiryRepositoty;
 
    public HogeEntiry(HogeId hogeId, FugaEnrity fugaEntity, FugaEntityRepository fugaEntityRepositoty){
        this.hogeId = hogeId;
        this.fugaEntity = fugaEntity;
        this.fugaEntityRepositoty fugaEntityRepositoty; 
    }
    // ... 

    /**
     * 承認する
     * <pre>
     * 既に承認済みでも再承認を受け付けないが例外判定は省略
     * </pre>
     * @param approvedUser 承認者;
     * @param approvedDateTime 承認日時;
     */
 public void approbe(User approvedUser ,ApplobedDate approvedDateTime){
       FugaEntity fugaEntty = new fugaEntity(approvedUser,approvedDateTime);
       fugaEntityRepository.store(fugaEntity);
       this.fugaEntity = fugaEntity;
    }
}

結論

サービスで実装する。リポジトリの設定漏れが起きるのでエンティティにはリポジトリを持たない。コード例:3
トランザクションスクリプト的で気持ち悪いと思う場合、コード例:4の様にHogeEntitiesRepositoryのメソッドで頑張る様にする。

コード例:3

@Service
public HogeService {
    private HogeEntitiesRepository hogeEntitiesRepository;
    private FugaEntitiesRepository fugaEntitiesRepository;
    public void approve(HogeId hogeId,
        User approvedUser,
        ApplobedDate approvedDateTime){
        HogeEntity hogeEntity = hogeEntitiesRepository.findById(hogeId);
        hogeEntity.assets(HogeSpesification.APPROVE_SPEC);
        FugaEntity approve = new FugaEntity(
            hogeId,
            approvedUser,
            approvedDateTime);
        );
        fugaEntitiesRepository.store(approve);
    }

}

コード例:4

@Service
public HogeService {
    private HogeEntitiesRepository hogeEntitiesRepository;
    public void approve(HogeId hogeId,
        User approvedUser,
        ApplobedDate approvedDateTime){
        HogeEntity hogeEntity = hogeEntitiesRepository.findById(hogeId);
        hogeEntity.assets(HogeSpesification.APPROVE_SPEC);
        FugaEntity approve = new FugaEntity(
            hogeId,
            approvedUser,
            approvedDateTime);
        );
        hogeEntity.approve(approve);
        hogeEntitiesRepository.store(hogeEntity);
    }
}

JavaでDDDをしてみてる

JavaでDDDをする機会に恵まれたので、その時に思ったことを書いていく。

環境

  • Java 8
  • Spring Boot
  • MyBatis

感想

DDDをやる上で業務の関心事をEntity、ValueObject、ServiceとRepositoryで表現する。
おそらく設計時間でいうとDDDを検討している時間が一番長いと思うんだけど、この時間よりも実装のMyBatisのMapping用のxmlを作成するのがぶっちぎりで時間がかかる。
さらにMapping用xmlは手作りすると間違える事が多いのだけど、この間違いの原因把握に時間がかかる。設計にかかる時間を1だとするとMapping用xmlをちゃんと作るには5倍ほどの時間がかかる。

これではDDDでdomainのモデルをxp的に成長させると考える事はできないと思う。entityにフィールドを追加した時にコンパイルエラーが出てくれる方が容易に修正できる。
つまり道具立としてMyBatisは誤りだと考える。

例えば、DbFluteの様なORMを使ってるDaoをRepositoryの中で使用してそのフィールドを移送するユーテリティーがあったほうが効率は上がる。IDEでは赤い波線が出てくれる。そうすることによってdomainモデルを成長させていく事に注力できる。実際DbFluteはDbレイアウトを親とするのでDDDには不向きだと思う。なんかあったらいいのに