勢いだけで書いたよ!
大体タイトルの通り!
いきなりだけど、勉強会で話を聞いたり twitter を見たりしてると、とりあえず見つけた名詞をクラスにして周辺のデータっぽいやつを全部そこに放り込んでいるエンティティがよくある気がするよ。
僕も初学者の頃書いたw
早速コード
public enum UserStatus {
Applying, // 申請中
Using, // 利用中
Leaved, // 退会済
BlackListed, // ブラックリスト
Erased, // 抹消済
}
(たった)これくらいの状態を持つUser
ってクラスを1つで表現してしまった!
「DDD だから User ってクラスを作って、処理はそこに書くんだ〜」って段階によくあると思う。
きっと最初は状態が少なかったんだけど、分析だかサービス成長だかがしていく間にヤベェことになってしまったんだね、わかるよ。ウンウン
public class User {
private UserId userId;
private UserCourse userCourse;
private UserStatus userStatus;
private ApplyDate applyDate;
private Optional<UseStartDate> useStartDate;
private Optional<LeaveDate> leaveDate;
private Optional<EraseDate> eraseDate;
public User apply(UserCourse userCourse) {
return new User(
new UserId(), userCourse, Applying, new ApplyDate(), Optional.empty(), Optional.empty(), Optional.empty()
);
}
public User useStart() {
if (userStatus != Applying)
throw new RuntimeException("not applying");
return new User(
userId, userCourse, Using, applyDate, Optional.of(new UseStartDate()), leaveDate, eraseDate
);
}
public User courseChange(UserCourse userCourse) {
if (userStatus != Using)
throw new RuntimeException("not using");
return new User(
userId, userCourse, userStatus, applyDate, useStartDate, leaveDate, eraseDate
);
}
public User leave() {
if (userStatus != Using)
throw new RuntimeException("not using");
return new User(
userId, userCourse, Leaved, applyDate, useStartDate, Optional.of(new LeaveDate()), eraseDate
);
}
public User blackListIn() {
if (userStatus != Using)
throw new RuntimeException("not using");
return new User(
userId, userCourse, BlackListed, applyDate, useStartDate, leaveDate, eraseDate
);
}
public User blackListOut() {
if (userStatus != BlackListed)
throw new RuntimeException("not black listed");
return new User(
userId, userCourse, Using, applyDate, useStartDate, leaveDate, eraseDate
);
}
public User erase() {
if (userStatus != BlackListed)
throw new RuntimeException("not black listed");
return new User(
userId, userCourse, Erased, applyDate, useStartDate, leaveDate, Optional.of(new EraseDate())
);
}
}
使うコードはこんな
public class UserService {
public void leave(UserId userId) {
User usingUser = userRepository.find(userId);
User leavedUser = usingUser.leave();
userRepository.leave(leavedUser);
billing.stop(leavedUser.getLeaveDate().get());
...
}
}
うへぇ〜辛ェ〜
コードが長い!
コンパイル通っても実行例外が超出そう!
ってかドメインロジックってナニ!
これほとんど DTO なんじゃあないのォ〜?
状態ごとにクラスを分けよう!
実装は割愛するけど、状態ごとにクラスを分けよう。
ポイントは2つ!
- ステータスで頑張らずにもっと細かく分ける
- 特定の状態でしか持たない値は極力持たない、持つときは必ず持つようにする
-
Optional
を極力排除するということ
-
主なメリットは!
- ステータス不正で実行してしまう実行例外がなくなる
-
leavedUser.getLeaveDate().get()
みたいな実行例外の可能性がなくなる - 特定の状態でできる処理が明瞭になる
- 何から何に遷移するかが明瞭になる
簡単だけど超効果あるからやってみるべし!
もちろんテク先行ではなくて、モデリングありきだよ
例で状態を用いたけど、何で分けるかはモデリング次第。
ユースケースとかビジネス影響度とか、きっとまちまちのはず。
それから、この分割は「これが DDD だぞ!」ではなくて「ただの起点」だよ!
こうやってモデルを小さくして、そこから分析をしてロジックを選り分けて集めるんだ!
例えば今回の例だと、LeavedUser
を取っ掛かりに請求停止(billing.stop(...)
の箇所)のモデリングをする、とか。
ちょっと気をぬくとエンティティを作ったつもりでもただの DTO になってしまうので、気をつけよう!
ちなみに、2年弱前に似た様な記事を書いてるよ、そちらもよければどうぞ
同じテーブルの値でも違うクラスを用意すると良い感じ#ケース2-1つのクラスの状態更新をする