じゃんけんアドベントカレンダー の 12 日目です。
前回 で DAO の整理が一段落しました。
今回は JankenService クラスを修正して、ファットな状態を解消していきます。
現状
リファクタリングを始める前に JankenService クラスの現状を確認すると、以下のようになっています。
public class JankenService {
private TransactionManager tm = ServiceLocator.resolve(TransactionManager.class);
private JankenDao jankenDao = ServiceLocator.resolve(JankenDao.class);
private JankenDetailDao jankenDetailDao = ServiceLocator.resolve(JankenDetailDao.class);
/**
* じゃんけんを実行し、勝者を返します。
*/
public Optional<Player> play(Player player1, Hand player1Hand,
Player player2, Hand player2Hand) {
return tm.transactional(tx -> {
// 勝敗判定
Result player1Result;
Result player2Result;
if (player1Hand.equals(Hand.STONE)) {
// プレイヤーがグーの場合
if (player2Hand.equals(Hand.STONE)) {
player1Result = Result.DRAW;
player2Result = Result.DRAW;
} else if (player2Hand.equals(Hand.PAPER)) {
player1Result = Result.LOSE;
player2Result = Result.WIN;
} else {
player1Result = Result.WIN;
player2Result = Result.LOSE;
}
} else if (player1Hand.equals(Hand.PAPER)) {
// プレイヤーがパーの場合
if (player2Hand.equals(Hand.STONE)) {
player1Result = Result.WIN;
player2Result = Result.LOSE;
} else if (player2Hand.equals(Hand.PAPER)) {
player1Result = Result.DRAW;
player2Result = Result.DRAW;
} else {
player1Result = Result.LOSE;
player2Result = Result.WIN;
}
} else {
// プレイヤーがチョキの場合
if (player2Hand.equals(Hand.STONE)) {
player1Result = Result.LOSE;
player2Result = Result.WIN;
} else if (player2Hand.equals(Hand.PAPER)) {
player1Result = Result.WIN;
player2Result = Result.LOSE;
} else {
player1Result = Result.DRAW;
player2Result = Result.DRAW;
}
}
// じゃんけんを生成
val playedAt = LocalDateTime.now();
val janken = new Janken(null, playedAt);
// じゃんけんを保存
val jankenWithId = jankenDao.insert(tx, janken);
// じゃんけん明細を生成
val jankenDetail1 = new JankenDetail(null, jankenWithId.getId(), player1.getId(), player1Hand, player1Result);
val jankenDetail2 = new JankenDetail(null, jankenWithId.getId(), player2.getId(), player2Hand, player2Result);
val jankenDetails = List.of(jankenDetail1, jankenDetail2);
// じゃんけん明細を保存
jankenDetailDao.insertAll(tx, jankenDetails);
// 勝者を返却
if (player1Result.equals(Result.WIN)) {
return Optional.of(player1);
} else if (player2Result.equals(Result.WIN)) {
return Optional.of(player2);
} else {
return Optional.empty();
}
});
}
}
1 つのメソッドが 80 行以上もあり、読み進めるのはなかなか大変です。
このように JankenService がファットになっている原因は、このクラスが複数の役割を持っていることです。
詳しくは以前書いた記事「「ビジネスロジック」とは何か、どう実装するのか」で解説していますが、ビジネスロジックは実は 2 種類あります。
- じゃんけんの勝敗判定などの「エンタープライズビジネスルール」
- 処理の流れの実現やトランザクション管理といった「アプリケーションビジネスルール」
の 2 種類です。
トランザクションスクリプトとドメインモデル
現状の JankenService では、エンタープライズビジネスルールとアプリケーションビジネスルールを両方持っており、別途作成した Player や Janken などの入れ物クラス (DTO) を使いながらビジネスロジック全体を実現しています。
このようなビジネスロジックの実装パターンをトランザクションスクリプトパターンと呼びます。
一方、サービスクラスが担うのはエンタープライズビジネスルールだけにし、Model クラスにデータとアプリケーションビジネスルールを持たせることもできます。
この実装パターンをドメインモデルパターンと呼びます。
現状 JankenService がファットなのはトランザクションスクリプトパターンを採用していることが原因なので、ドメインモデルパターンに徐々にリファクタリングしていきます。
JankenService のリファクタリング
では、JankenService をリファクタリングしていきます。
1. Hand に win メソッドを持たせる
JankenService に書かれている処理の中で、何がエンタープライズビジネスルールかを考えると、まず思いつくのは「グーがチョキに勝ち、チョキがパーに勝ち、パーがグーに勝つ」というじゃんけんのルールです。
この勝敗判定のルールを JankenService から他のクラスに移動しようと思います。
ルールの移動先はどこがいいでしょうか ?
JankenService のコードをもう一度振り返ると、該当箇所は以下のようになっています。
public class JankenService {
private TransactionManager tm = ServiceLocator.resolve(TransactionManager.class);
private JankenDao jankenDao = ServiceLocator.resolve(JankenDao.class);
private JankenDetailDao jankenDetailDao = ServiceLocator.resolve(JankenDetailDao.class);
/**
* じゃんけんを実行し、勝者を返します。
*/
public Optional<Player> play(Player player1, Hand player1Hand,
Player player2, Hand player2Hand) {
return tm.transactional(tx -> {
// 勝敗判定
Result player1Result;
Result player2Result;
if (player1Hand.equals(Hand.STONE)) {
// プレイヤーがグーの場合
if (player2Hand.equals(Hand.STONE)) {
player1Result = Result.DRAW;
player2Result = Result.DRAW;
} else if (player2Hand.equals(Hand.PAPER)) {
player1Result = Result.LOSE;
player2Result = Result.WIN;
} else {
player1Result = Result.WIN;
player2Result = Result.LOSE;
}
} else if (player1Hand.equals(Hand.PAPER)) {
// プレイヤーがパーの場合
if (player2Hand.equals(Hand.STONE)) {
player1Result = Result.WIN;
player2Result = Result.LOSE;
} else if (player2Hand.equals(Hand.PAPER)) {
player1Result = Result.DRAW;
player2Result = Result.DRAW;
} else {
player1Result = Result.LOSE;
player2Result = Result.WIN;
}
} else {
// プレイヤーがチョキの場合
if (player2Hand.equals(Hand.STONE)) {
player1Result = Result.LOSE;
player2Result = Result.WIN;
} else if (player2Hand.equals(Hand.PAPER)) {
player1Result = Result.WIN;
player2Result = Result.LOSE;
} else {
player1Result = Result.DRAW;
player2Result = Result.DRAW;
}
}
:
登場する型は Hand と Result です。
このどちらかに勝敗判定を持たせるのであれば、例えば hand1.wins(hand2) のように、hand1 が hand2 に勝つかどうかを判断できると自然ではないでしょうか。
そこで、列挙型 Hand に wins というメソッドを持たせてみます。
@AllArgsConstructor
@Getter
public enum Hand {
STONE(0, "STONE"),
PAPER(1, "PAPER"),
SCISSORS(2, "SCISSORS");
public static Hand of(int value) {
return Arrays.stream(Hand.values())
.filter(h -> h.getValue() == value)
.findFirst()
.orElseThrow();
}
private int value;
private String name;
public boolean wins(Hand other) {
switch (this) {
case STONE:
return other.equals(SCISSORS);
case PAPER:
return other.equals(STONE);
case SCISSORS:
return other.equals(PAPER);
default:
throw new IllegalStateException("Invalid hand. this = " + this);
}
}
}
すると、JankenService は以下のようになります。
public class JankenService {
:
public Optional<Player> play(Player player1, Hand player1Hand,
Player player2, Hand player2Hand) {
return tm.transactional(tx -> {
// 勝敗判定
Result player1Result;
Result player2Result;
if (player1Hand.wins(player2Hand)) {
player1Result = Result.WIN;
player2Result = Result.LOSE;
} else if (player2Hand.wins(player1Hand)) {
player1Result = Result.LOSE;
player2Result = Result.WIN;
} else {
player1Result = Result.DRAW;
player2Result = Result.DRAW;
}
:
勝敗判定の部分は行数にして 1/3 以下と、まあまあコンパクトになりました。
2. 「じゃんけんする」メソッドを作成する
勝敗判定のコードはある程度改善されましたが、JankenService のコードはまだまだ長いです。
続いて、「じゃんけんする」という意味のメソッドを作ってみようと思います。
じゃんけんをすると結果として Janken クラスと JankenDetail クラスが得られるというイメージで、ひとまず Janken クラスの static メソッドとして実装します。
結果として Janken と JankenDetail を返すため、まずは Janken が JankenDetail を保持するように変更します。
@AllArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class Janken {
private Long id;
private LocalDateTime playedAt;
private JankenDetail detail1;
private JankenDetail detail2;
}
続いて「じゃんけんする」メソッドを Janken クラスに実装すると、以下のようになりました。
@AllArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class Janken {
public static Janken play(Player player1, Hand player1Hand,
Player player2, Hand player2Hand) {
// 勝敗判定
Result player1Result;
Result player2Result;
if (player1Hand.wins(player2Hand)) {
player1Result = Result.WIN;
player2Result = Result.LOSE;
} else if (player2Hand.wins(player1Hand)) {
player1Result = Result.LOSE;
player2Result = Result.WIN;
} else {
player1Result = Result.DRAW;
player2Result = Result.DRAW;
}
// じゃんけん明細を生成
val detail1 = new JankenDetail(null, null, player1.getId(), player1Hand, player1Result);
val detail2 = new JankenDetail(null, null, player2.getId(), player2Hand, player2Result);
// じゃんけんを生成
val playedAt = LocalDateTime.now();
return new Janken(null, playedAt, detail1, detail2);
}
private Long id;
private LocalDateTime playedAt;
private JankenDetail detail1;
private JankenDetail detail2;
}
JankenService は以下のようになります。
public class JankenService {
private TransactionManager tm = ServiceLocator.resolve(TransactionManager.class);
private JankenDao jankenDao = ServiceLocator.resolve(JankenDao.class);
private JankenDetailDao jankenDetailDao = ServiceLocator.resolve(JankenDetailDao.class);
/**
* じゃんけんを実行し、結果を保存して、勝者を返します。
*/
public Optional<Player> play(Player player1, Hand player1Hand,
Player player2, Hand player2Hand) {
return tm.transactional(tx -> {
// じゃんけんを実行
val janken = Janken.play(player1, player1Hand, player2, player2Hand);
// じゃんけんとじゃんけん明細を保存
val jankenWithId = jankenDao.insert(tx, janken);
jankenDetailDao.insertAll(tx, List.of(jankenWithId.getDetail1(), jankenWithId.getDetail2()));
// 勝者を返却
if (jankenWithId.getDetail1().getResult().equals(Result.WIN)) {
return Optional.of(player1);
} else if (jankenWithId.getDetail2().getResult().equals(Result.WIN)) {
return Optional.of(player2);
} else {
return Optional.empty();
}
});
}
}
もとのコードと比べると、かなり整理されたと思います。
今回は「じゃんけんする」メソッドを static メソッドとして実装しましたが、専用のクラスを作成して非 static なメソッドとして実装することもできます。
自動テストでモックしたい場合などは、非 static なメソッドで実装した方が扱いやすいです。
3. 勝者を取得する処理をクラスに持たせる
JankenService の整理を続けます。
JankenService にはまだ Model に移動できる処理が残っています。
// 勝者を返却
if (jankenWithId.getDetail1().getResult().equals(Result.WIN)) {
return Optional.of(player1);
} else if (jankenWithId.getDetail2().getResult().equals(Result.WIN)) {
return Optional.of(player2);
} else {
return Optional.empty();
}
上記の勝者を返却する処理は、勝者が誰なのかを判定しています。
このような判定処理も Model に移動して整理することができます。
Janken クラスに勝者のプレイヤー ID を返すメソッドを実装すると、以下のようになります。
@AllArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class Janken {
:
public Optional<Long> winnerPlayerId() {
if (detail1.getResult().equals(Result.WIN)) {
return Optional.of(detail1.getPlayerId());
} else if (detail2.getResult().equals(Result.WIN)) {
return Optional.of(detail2.getPlayerId());
} else {
return Optional.empty();
}
}
}
このように、あるデータを扱う処理をそのクラスに持たせるのがオブジェクト指向プログラミングであり、ドメインモデルパターンの基本的な方針です。
さらに、このコードの中では detail1.getResult().equals(Result.WIN)
のようにして、Getter で取得した値に対する操作を実施しています。
Getter を使っている箇所は、そのデータを持っているクラスのメソッドに置き換えることができます。
JankenDetail クラスに isResultWin というメソッドを持たせると、Janken クラスは以下のようになります。
@AllArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class Janken {
:
public Optional<Long> winnerPlayerId() {
if (detail1.isResultWin()) {
return Optional.of(detail1.getPlayerId());
} else if (detail2.isResultWin()) {
return Optional.of(detail2.getPlayerId());
} else {
return Optional.empty();
}
}
}
その他細々した修正も入れ、最終的に JankenService は以下のようになりました。
public class JankenService {
private TransactionManager tm = ServiceLocator.resolve(TransactionManager.class);
private JankenDao jankenDao = ServiceLocator.resolve(JankenDao.class);
private JankenDetailDao jankenDetailDao = ServiceLocator.resolve(JankenDetailDao.class);
private PlayerDao playerDao = ServiceLocator.resolve(PlayerDao.class);
/**
* じゃんけんを実行し、結果を保存して、勝者を返します。
*/
public Optional<Player> play(long player1Id, Hand player1Hand,
long player2Id, Hand player2Hand) {
return tm.transactional(tx -> {
val janken = Janken.play(player1Id, player1Hand, player2Id, player2Hand);
val jankenWithId = jankenDao.insert(tx, janken);
jankenDetailDao.insertAll(tx, jankenWithId.details());
return jankenWithId.winnerPlayerId()
.map(playerId -> playerDao.findPlayerById(tx, playerId));
});
}
}
JankenService の役割は、
- 「じゃんけんを実行し、結果を保存して、勝者を返す」という処理の流れの実現
- トランザクション管理
だけになり、非常にスッキリしました。
4. Service を ApplicationService にリネーム
ここで、Service というクラス名のサフィックスを ApplicationService にリネームしようと思います。
public class JankenApplicationService {
:
ApplicationService は DDD の用語で、今回実装したような、処理の流れやトランザクション管理を担当するクラスのことです。
クリーンアーキテクチャに登場する UseCase と同じです。
UseCase という単語の方が分かりやすいため、UseCase と名付けたほうが良いという方もいらっしゃいます。
Service という単語をやめたのは、Service には ApplicationService と DomainService の 2 種類があるためです。
今回 static メソッドとした「じゃんけんする」メソッドを専用のクラスを作成して移動した場合、そのクラスは DomainService の 1 種です。1
なお、static メソッドや DomainService は、その処理を置く適切なクラスが見つからないときの最終手段なので、乱用しないようご注意ください。
モデルで表現するということ
この記事でのトランザクションスクリプトパターンからドメインモデルパターンへの変更は以上になります。
ここで、Janken クラスをもう一度振り返ってみます。
@AllArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class Janken {
public static Janken play(Long player1Id, Hand player1Hand,
Long player2Id, Hand player2Hand) {
// 勝敗判定
Result player1Result;
Result player2Result;
if (player1Hand.wins(player2Hand)) {
player1Result = Result.WIN;
player2Result = Result.LOSE;
} else if (player2Hand.wins(player1Hand)) {
player1Result = Result.LOSE;
player2Result = Result.WIN;
} else {
player1Result = Result.DRAW;
player2Result = Result.DRAW;
}
// じゃんけん明細を生成
val detail1 = new JankenDetail(null, null, player1Id, player1Hand, player1Result);
val detail2 = new JankenDetail(null, null, player2Id, player2Hand, player2Result);
// じゃんけんを生成
val playedAt = LocalDateTime.now();
return new Janken(null, playedAt, detail1, detail2);
}
private Long id;
private LocalDateTime playedAt;
private JankenDetail detail1;
private JankenDetail detail2;
public List<JankenDetail> details() {
return List.of(detail1, detail2);
}
public Optional<Long> winnerPlayerId() {
if (detail1.isResultWin()) {
return Optional.of(detail1.getPlayerId());
} else if (detail2.isResultWin()) {
return Optional.of(detail2.getPlayerId());
} else {
return Optional.empty();
}
}
}
例えば、このクラスの最後に書かれている winnerPlayerId というメソッドは、フィールドとして保持するデータをもとに「勝者は誰か」という判断を定義しています。
これは、勝者という概念の定義をモデルで「表現」していると考えることができます。
モデルでの「表現」というのは、クラス間の関係もそうですし、メソッド 1 つ 1 つもそうです。
引数のないコンストラクタを設けないことや Setter を設けないことも、そのクラスがとりうる状態やその変化がありえるかを「表現」していることになります。
(もちろん、プログラミングのプラクティスとしてのメリットもあります)
オブジェクト指向モデリングというと「現実をそのまま表現する」といったことに注目されがちですが、このような 1 つ 1 つの「表現」の積み重ねも重要だと思います。
そもそもモデリングというのは「現実世界の事象をある視点で抽出して表現すること」であり、モデリングした時点で現実そのままであることは原理的にありえません。
自分もまだまだ悩むことばかりですが、現実の事象やルールなどをうまく「表現」できるよう練習していきたいと思っています。
オブジェクト指向モデリングの参考記事・書籍
さて、今回はリファクタリングしていくアプローチでボトムアップ的にモデルを組み立てていきました。
(実際には、トップダウン的な考え方とボトムアップ的な考え方を繰り返すことになると思います)
今回のようなボトムアップ的なモデルの作り方については、例えば以下の記事や
以下の書籍が参考になります。
また、関連書籍は以下の記事にもまとめているので、よろしければ参照ください。
課題
今回で ApplicationService がかなり整理されましたが、以下のような課題が残っています。
- Janken と JankenDetail の整合性がアプリケーション層に依存している
- モデルの他のクラスを参照で持つのか ID だけ持つのか統一されていない
- Janken と JankenDetail の ID が NULL の場合がある
- 「じゃんけんする」メソッドがあまり整理されていない
順に簡単に解説していきます。
Janken と JankenDetail の整合性がアプリケーション層に依存している
1 つ目の課題です。
JankenApplicationService で Janken と JankenDetail を保存する箇所は以下のようになっています。
val jankenWithId = jankenDao.insert(tx, janken);
jankenDetailDao.insertAll(tx, jankenWithId.details());
このコードでは、Janken と JankenDetail を同時に保存することがアプリケーション層に依存しています。
Janken と JankenDetail が同時に保存されるべきものならば、そのことはドメイン層で表現されるべきであり、アプリケーション層に依存して挙動が変わるべきではありません。
また、今回は実装していませんが、Janken と JankenDetail の読み込みコードを想像すると、例えば以下のようになります。
val tmpJanken = jankenDao.findById(jankenId).get()
val jankenDetails = jankenDetailDao.findByJankenIdOrderById(tx, jankenId);
val janken = new Janken(tmpJanken.getId(), tmpJanken.getPlayedAt(), jankenDetails.get(0), jankenDetails.get(1);
このコードをアプリケーション層に書いてしまうと、Janken クラスを構築して JankenDetail を持たせることがアプリケーション層の実装次第になってしまいます。
モデルの他のクラスを参照で持つのか ID だけ持つのか統一されていない
2 つ目の課題です。
Janken クラスのコードは以下のようになっています。
@AllArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class Janken {
:
private Long id;
private LocalDateTime playedAt;
private JankenDetail detail1;
private JankenDetail detail2;
:
JankenDetail クラスのコードは以下のようになっています。
@AllArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class JankenDetail {
private Long id;
private Long jankenId;
private Long playerId;
private Hand hand;
private Result result;
:
Janken クラスは JankenDetail を参照として持っていますが、JankenDetail から Janken、Player に対しては ID だけを保持しています。
ここも Janken や Player を参照として持った方が楽な気がしますが、どうでしょうか ?
ここまで挙げた 2 つの課題については、**「集約」と「Repository パターン」**が関わってきます。
次回、これら 2 つを「集約」と「Repository パターン」で解決しようと思います。
Janken と JankenDetail の ID が NULL の場合がある
続いて、3 つ目の課題です。
「じゃんけんする」コードを見てみます。
:
public class Janken {
public static Janken play(Long player1Id, Hand player1Hand,
Long player2Id, Hand player2Hand) {
:
// じゃんけん明細を生成
val detail1 = new JankenDetail(null, null, player1Id, player1Hand, player1Result);
val detail2 = new JankenDetail(null, null, player2Id, player2Hand, player2Result);
// じゃんけんを生成
val playedAt = LocalDateTime.now();
return new Janken(null, playedAt, detail1, detail2);
}
:
DB の自動採番機能で ID を採番しているため、この時点では jankenId と jankenDetailId が NULL になっています。
よく言われることですが、NULL は基本的に避けるべきものです。
また、今回については、NULL の代わりに Optional を使えば解決するという問題でもありません。
Optional を使えば NULL よりはまだ良いですが、結局取り出し箇所で例外処理する必要が出てきてしまいます。
これは以前も出した ID の採番に関する問題で、後日取り組む予定です。
「じゃんけんする」メソッドがあまり整理されていない
最後に、4 つ目の課題です。
再度「じゃんけんする」メソッドを見てみます。
:
public class Janken {
public static Janken play(Long player1Id, Hand player1Hand,
Long player2Id, Hand player2Hand) {
// 勝敗判定
Result player1Result;
Result player2Result;
if (player1Hand.wins(player2Hand)) {
player1Result = Result.WIN;
player2Result = Result.LOSE;
} else if (player2Hand.wins(player1Hand)) {
player1Result = Result.LOSE;
player2Result = Result.WIN;
} else {
player1Result = Result.DRAW;
player2Result = Result.DRAW;
}
// じゃんけん明細を生成
val detail1 = new JankenDetail(null, null, player1Id, player1Hand, player1Result);
val detail2 = new JankenDetail(null, null, player2Id, player2Hand, player2Result);
// じゃんけんを生成
val playedAt = LocalDateTime.now();
return new Janken(null, playedAt, detail1, detail2);
}
private Long id;
private LocalDateTime playedAt;
private JankenDetail detail1;
private JankenDetail detail2;
:
もともとの JankenService のコードと比べるとだいぶよくなりましたが、まだ多くの処理をしていたり、整理されていないと感じます。
例えば、
- 勝敗判定の処理を他の適切な箇所に移動する
- 引数に playerId と hand が繰り返されているので、例えば playerId と hand を持つ HandSelection (手の選択) クラスを作って扱う
- JankenDetail を List で保持し任意長を扱えるようにする
といった修正の方針が考えられます。
また、static メソッドから他のクラスへの移動も実施したいところです。
このあたりのブラッシュアップについては、アドベントカレンダーの最後のほうで余裕があれば実施しようと思います。
なお、他にもモデルに ValueObject などをたくさん導入することもできますが、すでに解説記事や書籍が多数あるので、このアドベントカレンダーではあまり導入しないつもりです。
現時点のコード
以上で、トランザクションスクリプトからドメインモデルへの変更を実施し、モデルの課題をまとめました。
現時点のコードの構成を図示すると、以下のようになっています。
ディレクトリ構成も掲載しておきます。
$ tree app/src/main/
app/src/main/
├── java
│ └── com
│ └── example
│ └── janken
│ ├── App.java
│ ├── application
│ │ └── service
│ │ ├── JankenApplicationService.java
│ │ └── PlayerApplicationService.java
│ ├── domain
│ │ ├── dao
│ │ │ ├── JankenDao.java
│ │ │ ├── JankenDetailDao.java
│ │ │ └── PlayerDao.java
│ │ ├── model
│ │ │ ├── Hand.java
│ │ │ ├── Janken.java
│ │ │ ├── JankenDetail.java
│ │ │ ├── Player.java
│ │ │ └── Result.java
│ │ └── transaction
│ │ ├── Transaction.java
│ │ └── TransactionManager.java
│ ├── infrastructure
│ │ ├── csvdao
│ │ │ ├── CsvDaoUtils.java
│ │ │ ├── JankenCsvDao.java
│ │ │ ├── JankenDetailCsvDao.java
│ │ │ └── PlayerCsvDao.java
│ │ ├── jdbctransaction
│ │ │ ├── InsertMapper.java
│ │ │ ├── JDBCTransaction.java
│ │ │ ├── JDBCTransactionManager.java
│ │ │ ├── RowMapper.java
│ │ │ ├── SimpleJDBCWrapper.java
│ │ │ └── SingleRowMapper.java
│ │ └── mysqldao
│ │ ├── JankenDetailMySQLDao.java
│ │ ├── JankenMySQLDao.java
│ │ └── PlayerMySQLDao.java
│ ├── presentation
│ │ ├── controller
│ │ │ └── JankenController.java
│ │ └── view
│ │ └── View.java
│ └── registry
│ └── ServiceLocator.java
└── resources
└── view
├── invalid-input.vm
├── result.vm
├── scan-prompt.vm
└── show-hand.vm
20 directories, 33 files
コードは GitHub の この時点のコミット を参照ください。
次回のテーマ
今回の記事の中で
- Janken と JankenDetail の整合性がアプリケーション層に依存している
- モデルの他のクラスを参照で持つのか ID だけ持つのか統一されていない
- Janken と JankenDetail の ID が NULL の場合がある
- 「じゃんけんする」メソッドがあまり整理されていない
といった課題があることを挙げました。
次回は**「集約」と「Repository パターン」を導入**することで、1 つ目と 2 つ目を解決しようと思います。
それでは、今回の記事はここまでにします。最後まで読んでくださりありがとうございました。
じゃんけんアドベントカレンダー に興味を持ってくださった方は、是非購読お願いします。
-
場合によっては Janken クラスのファクトリのように考えることもできるかもしれません ↩