■概要
アプリケーションを作っていると、1つのクラスに「画面からの入力チェック」「業務のルール」「データベースへの保存」がすべて詰め込まれてしまうことがある。
こうなると、どこに何が書いてあるのか分かりにくくなり、画面やDBを少し変えただけで業務ルールまで書き換えが必要になってしまう。
これを防ぐ代表的な設計手法が3層アーキテクチャ(3-Layer Architecture)である。
処理を役割ごとに次の3つの「層」に分けて作る。
| 層 | 役割 | 例 |
|---|---|---|
| プレゼンテーション層 | 利用者との受付窓口。入力の受付・形式チェック・型変換・結果の表示 | 画面、API、コンソール出力 |
| ドメイン層 | アプリの中心となる業務ルール(ビジネスロジック) | 「18歳未満は登録できない」「同じメールアドレスは登録できない」 |
| データアクセス層 | データの置き場所とのやり取り。保存・検索 | DBへのSQL発行、ファイル読み書き |
3層に分けるときのポイントは次の3つ。
- チェックの種類で置き場所を分ける
- 形式チェック(
"abc"は年齢として数字ではない)→ プレゼンテーション層 - 業務ルールチェック(15歳は登録できない)→ ドメイン層
- 業務ルールをドメイン層にまとめておけば、画面がWebでもスマホでもバッチでも、同じルールが必ず適用される
- 形式チェック(
- 依存の向きを「プレゼンテーション層 → ドメイン層 ← データアクセス層」にする
- 素朴に作ると「プレゼンテーション層 → ドメイン層 → データアクセス層」となり、DBの都合が変わるたびに業務ルールまで影響を受ける
- そこで、保存の窓口となるインターフェース(
MemberRepository)をドメイン層に置き、データアクセス層がそれを実装する - これを依存性逆転の原則(DIP)と呼ぶ
- 使う部品は外から受け取る(依存性の注入:DI)
MemberServiceは実装クラス(InMemoryMemberRepository)を一度も名指しせず、コンストラクタで受け取る- 将来本物のDBを使う場合も、
MemberRepositoryを実装した別クラスを作って差し替えるだけで、ドメイン層は1行も変えなくてよい
■実装するサンプル処理の概要
「会員登録」と「会員一覧の表示」を、3層に分けて実装する。
- プレゼンテーション層(
presentationパッケージ)MemberController… 入力値(すべて文字列)を受け取り、氏名・メールアドレスの形式チェックと、年齢の文字列→int変換を行う。結果やエラーメッセージを表示する
- ドメイン層(
domainパッケージ)MemberService… 業務ルール(18歳未満は登録不可、メールアドレス重複禁止)をチェックし、問題なければ保存を依頼するMember… 会員を表すエンティティ(recordで定義)MemberRepository… 保存・検索の窓口となるインターフェースBusinessRuleException… 業務ルール違反時に投げる例外(非検査例外)
- データアクセス層(
dataaccessパッケージ)InMemoryMemberRepository…MemberRepositoryの実装。学習用にLinkedHashMapを DB テーブル代わりにし、発行されるであろう SQL をログ出力する
ThreeLayerProcessingMain… 各層の部品を下の層から順に組み立て、次の4ケースを実行する- 正常な登録(2件)
- 入力の形式エラー(年齢が
"abc"/氏名が空)→ プレゼンテーション層で検出され、下の層は呼ばれない - 業務ルール違反(15歳/メールアドレス重複)→ ドメイン層で検出され、INSERT はされない
- 会員一覧の表示
▼実行環境
- Java:バージョン17(
recordを使用するため Java 16 以降が必要)
■クラス図
ThreeLayerProcessingMainが「組み立て係」として、InMemoryMemberRepository→MemberService→MemberControllerの順に生成し、下の層の部品を上の層のコンストラクタに渡す(DI)- Spring Framework などを使う実務では、この組み立てをフレームワークが自動で行ってくれる
MemberController(プレゼンテーション層)はMemberService(ドメイン層)に業務処理を依頼し、BusinessRuleExceptionを catch してエラーメッセージを表示するMemberServiceはMemberRepository(interface)にだけ依存し、業務ルール違反の場合はBusinessRuleExceptionを throw するInMemoryMemberRepository(データアクセス層)は、ドメイン層のMemberRepositoryを implements している- 矢印がデータアクセス層からドメイン層に向いている点がポイント。ドメイン層はデータアクセス層のことを何も知らない
■フローチャート
全体の流れ(ThreeLayerProcessingMain.main)
- データアクセス層の
InMemoryMemberRepositoryを生成する - ドメイン層の
MemberServiceを、リポジトリを渡して生成する - プレゼンテーション層の
MemberControllerを、サービスを渡して生成する - ケース1~4を順に実行する
会員登録(MemberController.register → MemberService.register → InMemoryMemberRepository)
- プレゼンテーション層で入力の形式をチェックする
- 氏名が空、メールアドレスに
@が無い、年齢が数値に変換できない場合は、その場でエラーを表示して終了する(ドメイン層は呼ばない)
- 氏名が空、メールアドレスに
- ドメイン層で業務ルールをチェックする
- 18歳未満なら
BusinessRuleExceptionを投げる(データアクセス層は呼ばない) - データアクセス層の
findByEmailで重複を確認し、既に登録済みならBusinessRuleExceptionを投げる
- 18歳未満なら
- すべてのルールをクリアしたら、データアクセス層の
saveで ID を採番して保存する - プレゼンテーション層に戻り、登録完了(会員ID)またはエラーメッセージを表示する
会員一覧表示(MemberController.showAll)
- プレゼンテーション層がリクエストを受け付け、ドメイン層の
findAllを呼ぶ - ドメイン層はリポジトリの
findAllをそのまま呼ぶ(将来「退会済みは除く」などのルールが増えたらここに書き足す) - データアクセス層は内部の Map を直接渡さず、新しい
Listにコピーして返す(防御的コピー) - プレゼンテーション層が
String.formatで見やすく整形して表示し、最後に件数を表示する
■サンプルコード
以下に今回実装するサンプルコードを示す。
・src/main/java/ThreeLayerProcessingMain.java
import dataaccess.InMemoryMemberRepository;
import domain.MemberRepository;
import domain.MemberService;
import presentation.MemberController;
/**
* 3層アーキテクチャ(3-Layer Architecture)の学習用サンプル。
*
* <p>3層アーキテクチャとは、アプリケーションの処理を役割ごとに3つの「層」に分けて作る設計手法です。
* 役割ごとに分けることで「どこに何が書いてあるか」が分かりやすくなり、
* 1つの層を変更しても他の層への影響を小さく抑えられます。
*
* <h2>3つの層と、このサンプルでの担当クラス</h2>
* <pre>
* [プレゼンテーション層] presentation パッケージ
* MemberController … 入力の受付・形式チェック・結果表示
* |
* | 呼び出す
* v
* [ドメイン層] domain パッケージ
* MemberService … 業務ルール(年齢制限・重複禁止)
* Member … 会員エンティティ
* MemberRepository … 保存の窓口(インターフェース)
* ^
* | 実装する(implements)
* |
* [データアクセス層] dataaccess パッケージ
* InMemoryMemberRepository … データの保存・検索
* </pre>
*
* <p>処理の流れ(会員登録の場合):
* <ol>
* <li>プレゼンテーション層が入力を受け取り、形式をチェックして int などに変換する</li>
* <li>ドメイン層が業務ルールをチェックする</li>
* <li>データアクセス層がデータを保存する</li>
* <li>結果が逆順に戻ってきて、プレゼンテーション層が画面に表示する</li>
* </ol>
*
* <p>このクラス(main)は、各層の部品を組み立てて動かす「組み立て係」です。
* Spring Framework などを使う実務では、この組み立てをフレームワークが自動で行ってくれます。
*/
public class ThreeLayerProcessingMain {
public static void main(String[] args) {
// ===== 部品の組み立て =====
// 下の層から順に作り、上の層へ渡していく。
// ドメイン層は MemberRepository(インターフェース)型で受け取るので、
// 将来DB版の実装に変えたくなったら、この1行を差し替えるだけで済む。
MemberRepository repository = new InMemoryMemberRepository(); // データアクセス層
MemberService service = new MemberService(repository); // ドメイン層
MemberController controller = new MemberController(service); // プレゼンテーション層
// ===== ケース1:正常に登録できる =====
System.out.println("===== ケース1:正常な登録 =====");
controller.register("山田太郎", "yamada@example.com", "30");
System.out.println();
controller.register("佐藤花子", "sato@example.com", "25");
System.out.println();
// ===== ケース2:プレゼンテーション層で弾かれる(入力の形式エラー) =====
// ドメイン層・データアクセス層まで処理が届かないことをログで確認してみよう。
System.out.println("===== ケース2:入力の形式エラー(プレゼンテーション層で検出) =====");
controller.register("鈴木一郎", "suzuki@example.com", "abc");
System.out.println();
controller.register("", "noname@example.com", "40");
System.out.println();
// ===== ケース3:ドメイン層で弾かれる(業務ルール違反) =====
System.out.println("===== ケース3:業務ルール違反(ドメイン層で検出) =====");
// 年齢制限違反:データアクセス層は呼ばれない
controller.register("高橋次郎", "takahashi@example.com", "15");
System.out.println();
// メールアドレス重複:重複確認のためにデータアクセス層の SELECT は呼ばれるが、INSERT はされない
controller.register("山田花子", "yamada@example.com", "28");
System.out.println();
// ===== ケース4:一覧表示 =====
System.out.println("===== ケース4:会員一覧の表示 =====");
controller.showAll();
}
}
・src/main/java/presentation/MemberController.java
package presentation;
import domain.BusinessRuleException;
import domain.Member;
import domain.MemberService;
import java.util.List;
/**
* 【プレゼンテーション層】利用者からの入力を受け取り、結果を表示するクラス。
*
* <p>プレゼンテーション層の役割:
* <ul>
* <li>利用者との「受付窓口」です。Webアプリなら画面やAPI、今回はコンソール出力が該当します。</li>
* <li>入力値の<b>形式</b>チェック(空でないか、数字かどうか)や、型の変換(文字列→int)を行います。</li>
* <li>結果やエラーを、利用者に分かりやすい形に整えて表示します。</li>
* <li>業務ルール(年齢制限など)は書きません。それはドメイン層の仕事です。</li>
* </ul>
*
* <p>「形式チェック」と「業務ルールチェック」の違い:
* <pre>
* 形式チェック(ここ) : "abc" は年齢として数字じゃない → 画面の入力ミス
* 業務ルール(ドメイン層): 15歳は登録できない → 業務上の決まり
* </pre>
* 業務ルールをドメイン層にまとめておけば、画面がWebでもスマホでもバッチでも、同じルールが必ず適用されます。
*/
public class MemberController {
/** 業務処理を依頼する相手(ドメイン層)。 */
private final MemberService memberService;
/**
* @param memberService 会員サービス
*/
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
/**
* 会員登録リクエストを処理する。
*
* <p>画面のフォームから送られてくる値は、基本的にすべて「文字列」です。
* そのため引数もあえて全部 String にして、Webフォームからの入力を模しています。
*
* @param name 氏名(入力値)
* @param email メールアドレス(入力値)
* @param ageInput 年齢(入力値。文字列のまま受け取る)
*/
public void register(String name, String email, String ageInput) {
System.out.println("[Presentation] 会員登録リクエスト受付: name=" + name
+ ", email=" + email + ", age=" + ageInput);
// ---------- (1) 入力の形式チェック ----------
// isBlank() は「空文字、またはスペースだけ」の場合に true を返す(Java 11 以降)。
if (name == null || name.isBlank()) {
showError("氏名を入力してください。");
return; // エラーなのでここで処理を終える(ドメイン層は呼ばない)
}
if (email == null || !email.contains("@")) {
showError("メールアドレスの形式が正しくありません。");
return;
}
// ---------- (2) 型の変換(文字列 → 数値) ----------
int age;
try {
// Integer.parseInt は数字以外の文字列を渡すと NumberFormatException を投げる。
age = Integer.parseInt(ageInput);
} catch (NumberFormatException e) {
showError("年齢は数字で入力してください。(入力:" + ageInput + ")");
return;
}
// ---------- (3) ドメイン層に業務処理を依頼 ----------
try {
Member member = memberService.register(name, email, age);
// ---------- (4) 結果を利用者向けに整形して表示 ----------
System.out.println("[Presentation] 登録が完了しました。会員ID: " + member.id());
} catch (BusinessRuleException e) {
// ドメイン層が「業務ルール違反」と判断した場合は、そのメッセージを画面に出す。
showError(e.getMessage());
}
}
/**
* 会員一覧を表示する。
*/
public void showAll() {
System.out.println("[Presentation] 会員一覧リクエスト受付");
List<Member> members = memberService.findAll();
// 表示の形を整えるのはプレゼンテーション層の仕事。
// String.format は「%d(整数)」「%s(文字列)」の部分に、後ろの引数を順番に埋め込んでくれる。
// 例えば年齢を「30歳」と表示する、といった見せ方の工夫もここで行う。
for (Member member : members) {
System.out.println(String.format(" 会員ID:%d 氏名:%s メール:%s 年齢:%d歳",
member.id(), member.name(), member.email(), member.age()));
}
System.out.println(" (全 " + members.size() + " 件)");
}
/**
* エラーメッセージを表示する共通処理。
*
* @param message エラーメッセージ
*/
private void showError(String message) {
System.out.println("[Presentation] エラー: " + message);
}
}
・src/main/java/domain/MemberService.java
package domain;
import java.util.List;
/**
* 【ドメイン層】会員に関する「業務ルール(ビジネスロジック)」をまとめたクラス。
*
* <p>ドメイン層の役割:
* <ul>
* <li>アプリの中心となる「業務のルール」を書く場所です。</li>
* <li>例:「18歳未満は会員登録できない」「同じメールアドレスで二重登録できない」</li>
* <li>画面の見た目(文字の表示方法)やデータの保存方法は知りません。</li>
* </ul>
*
* <p>このクラスはデータの保存に {@link MemberRepository}(インターフェース)だけを使い、
* 具体的な実装クラス(dataaccess パッケージのクラス)を一度も名指ししていません。
* 実物はコンストラクタで外から受け取ります(これを「依存性の注入(DI)」と呼びます)。
*/
public class MemberService {
/** 会員登録できる最低年齢。マジックナンバー(意味の分からない数値の直書き)を避けるため定数にする。 */
private static final int MIN_AGE = 18;
/** データの保存・取得を任せる相手。final にすることで、後から別のものに差し替えられないようにする。 */
private final MemberRepository memberRepository;
/**
* コンストラクタ。使うリポジトリを外から受け取る。
*
* @param memberRepository 会員リポジトリ(実装は何でもよい)
*/
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
/**
* 会員を登録する。
*
* <p>業務ルールを順番にチェックし、すべて満たした場合だけ保存を依頼します。
* ルール違反があれば {@link BusinessRuleException} を投げて処理を中断します。
*
* @param name 氏名
* @param email メールアドレス
* @param age 年齢
* @return 登録された会員
* @throws BusinessRuleException 業務ルールに違反した場合
*/
public Member register(String name, String email, int age) {
// ルール1:年齢制限。18歳未満は登録できない。
if (age < MIN_AGE) {
throw new BusinessRuleException(MIN_AGE + "歳未満の方は会員登録できません。(入力:" + age + "歳)");
}
// ルール2:メールアドレスの重複禁止。
// findByEmail は Optional を返すので、isPresent() で「見つかったかどうか」を判定できる。
if (memberRepository.findByEmail(email).isPresent()) {
throw new BusinessRuleException("メールアドレス「" + email + "」は既に登録されています。");
}
// すべてのルールをクリアしたので、保存はデータアクセス層にお任せする。
// ドメイン層は「保存して」とお願いするだけで、どこにどう保存されるかは気にしない。
return memberRepository.save(name, email, age);
}
/**
* 全会員を取得する。
*
* <p>今回は特別な業務ルールがないので、リポジトリの結果をそのまま返しています。
* 将来「退会済みの会員は除く」などのルールが増えたら、ここに書き足します。
*
* @return 会員一覧
*/
public List<Member> findAll() {
return memberRepository.findAll();
}
}
・src/main/java/domain/MemberRepository.java
package domain;
import java.util.List;
import java.util.Optional;
/**
* 【ドメイン層】会員データの「保存・取得」の窓口を表すインターフェース。
*
* <p>「リポジトリ(Repository)」は、データの保管庫という意味です。
* ドメイン層は「会員を保存したい」「メールアドレスで探したい」という<b>やりたいこと</b>だけを
* ここに宣言し、<b>どうやって</b>保存するか(DB?ファイル?メモリ?)は決めません。
*
* <p>なぜインターフェースをドメイン層に置くのか?
* <pre>
* 素朴に作ると依存の向きはこうなります:
* プレゼンテーション層 → ドメイン層 → データアクセス層
* これだと、DBの都合が変わるたびにドメイン層(業務ルール)まで影響を受けてしまいます。
*
* そこで「インターフェースはドメイン層が持ち、データアクセス層がそれを実装する」形にすると:
* プレゼンテーション層 → ドメイン層 ← データアクセス層
* となり、一番大事な業務ルールが、DBなどの技術的な詳細に振り回されなくなります。
* (これを「依存性逆転の原則(DIP)」と呼びます)
* </pre>
*/
public interface MemberRepository {
/**
* 会員を新しく保存し、IDが採番された会員を返す。
*
* @param name 氏名
* @param email メールアドレス
* @param age 年齢
* @return IDが付与された会員
*/
Member save(String name, String email, int age);
/**
* メールアドレスで会員を探す。
*
* <p>戻り値の {@link Optional} は「値があるかもしれないし、無いかもしれない」を表す箱です。
* null を返すよりも「見つからない場合がある」ことが型で明確になり、
* null チェック漏れによるエラー(NullPointerException)を防ぎやすくなります。
*
* @param email メールアドレス
* @return 見つかった会員(見つからなければ空の Optional)
*/
Optional<Member> findByEmail(String email);
/**
* 登録済みの全会員を返す。
*
* @return 会員の一覧
*/
List<Member> findAll();
}
・src/main/java/domain/Member.java
package domain;
/**
* 【ドメイン層】会員を表す「エンティティ」。
*
* <p>エンティティとは、業務の世界に登場する「モノ」や「人」をプログラムで表現したものです。
* ここでは「会員」を表しています。
*
* <p>ポイント:
* <ul>
* <li>このクラスは画面(プレゼンテーション層)やデータベース(データアクセス層)のことを一切知りません。</li>
* <li>そのため、画面がWebからスマホアプリに変わっても、DBが変わっても、このクラスは書き換え不要です。</li>
* </ul>
*
* <p>Java 16 以降で使える {@code record} を使っています。
* record は「値を入れて取り出すだけのクラス」を短く書くための構文で、
* コンストラクタ・getter({@code id()} や {@code name()})・equals・toString を自動で作ってくれます。
*
* @param id 会員ID(データアクセス層で採番される)
* @param name 氏名
* @param email メールアドレス
* @param age 年齢
*/
public record Member(int id, String name, String email, int age) {
}
・src/main/java/domain/BusinessRuleException.java
package domain;
/**
* 【ドメイン層】業務ルールに違反したときに投げる例外。
*
* <p>例:「18歳未満は登録できない」「同じメールアドレスは登録できない」など。
*
* <p>{@link RuntimeException} を継承しているので「非検査例外」になります。
* 非検査例外は、呼び出し側で {@code throws} や {@code try-catch} を書くことが強制されません。
* 今回はプレゼンテーション層でまとめて catch し、エラーメッセージを画面に表示します。
*/
public class BusinessRuleException extends RuntimeException {
/**
* @param message 利用者に伝えるエラーメッセージ
*/
public BusinessRuleException(String message) {
// 親クラス(RuntimeException)のコンストラクタにメッセージを渡す。
// あとで getMessage() で取り出せるようになる。
super(message);
}
}
・src/main/java/dataaccess/InMemoryMemberRepository.java
package dataaccess;
import domain.Member;
import domain.MemberRepository;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* 【データアクセス層】会員データをメモリ上に保存するリポジトリの実装。
*
* <p>データアクセス層の役割:
* <ul>
* <li>データベースやファイルなど「データの置き場所」とのやり取りを担当します。</li>
* <li>業務ルール(年齢制限など)は一切書きません。言われた通りに保存・検索するだけです。</li>
* </ul>
*
* <p>本来はここで SQL を発行して DB に保存しますが、学習用に環境構築なしで動くよう、
* {@link Map} を「なんちゃってDBテーブル」として使っています。
*
* <p>ドメイン層の {@link MemberRepository} を {@code implements}(実装)しているのがポイントです。
* 将来本物のDBを使うときは、例えば {@code JdbcMemberRepository} という別クラスを作り、
* 同じく MemberRepository を実装すれば、ドメイン層を1行も変えずに差し替えられます。
*/
public class InMemoryMemberRepository implements MemberRepository {
/**
* 会員データの保存先(DBのテーブル代わり)。キーが会員ID、値が会員。
*
* <p>{@link LinkedHashMap} は「入れた順番」を覚えてくれる Map です。
* 普通の HashMap だと順番がバラバラになるため、一覧表示の順序を安定させる目的で使っています。
*/
private final Map<Integer, Member> table = new LinkedHashMap<>();
/** 次に割り当てる会員ID。DBの「自動採番(AUTO_INCREMENT)」の代わり。 */
private int nextId = 1;
@Override
public Member save(String name, String email, int age) {
// IDを採番してから会員オブジェクトを作る。nextId++ は「今の値を使ってから1増やす」という意味。
Member member = new Member(nextId++, name, email, age);
table.put(member.id(), member);
// 実際のDBアクセスのイメージが湧くように、発行されるであろうSQLをログとして表示する。
System.out.println(" [DataAccess] INSERT INTO member VALUES ("
+ member.id() + ", '" + name + "', '" + email + "', " + age + ")");
return member;
}
@Override
public Optional<Member> findByEmail(String email) {
System.out.println(" [DataAccess] SELECT * FROM member WHERE email = '" + email + "'");
// Stream API を使った検索。
// values() : Map に入っている全会員を取り出す
// stream() : 1件ずつ流れ作業で処理できる形にする
// filter(...) : 条件(メールアドレスが一致)に合うものだけ残す
// findFirst() : 最初の1件を Optional に入れて返す(無ければ空の Optional)
return table.values().stream()
.filter(member -> member.email().equals(email))
.findFirst();
}
@Override
public List<Member> findAll() {
System.out.println(" [DataAccess] SELECT * FROM member");
// 内部の Map をそのまま渡すと、呼び出し側に中身を書き換えられてしまう恐れがある。
// そこで新しい List にコピーして返す(防御的コピー)。
return new ArrayList<>(table.values());
}
}
■実行結果
===== ケース1:正常な登録 =====
[Presentation] 会員登録リクエスト受付: name=山田太郎, email=yamada@example.com, age=30
[DataAccess] SELECT * FROM member WHERE email = 'yamada@example.com'
[DataAccess] INSERT INTO member VALUES (1, '山田太郎', 'yamada@example.com', 30)
[Presentation] 登録が完了しました。会員ID: 1
[Presentation] 会員登録リクエスト受付: name=佐藤花子, email=sato@example.com, age=25
[DataAccess] SELECT * FROM member WHERE email = 'sato@example.com'
[DataAccess] INSERT INTO member VALUES (2, '佐藤花子', 'sato@example.com', 25)
[Presentation] 登録が完了しました。会員ID: 2
===== ケース2:入力の形式エラー(プレゼンテーション層で検出) =====
[Presentation] 会員登録リクエスト受付: name=鈴木一郎, email=suzuki@example.com, age=abc
[Presentation] エラー: 年齢は数字で入力してください。(入力:abc)
[Presentation] 会員登録リクエスト受付: name=, email=noname@example.com, age=40
[Presentation] エラー: 氏名を入力してください。
===== ケース3:業務ルール違反(ドメイン層で検出) =====
[Presentation] 会員登録リクエスト受付: name=高橋次郎, email=takahashi@example.com, age=15
[Presentation] エラー: 18歳未満の方は会員登録できません。(入力:15歳)
[Presentation] 会員登録リクエスト受付: name=山田花子, email=yamada@example.com, age=28
[DataAccess] SELECT * FROM member WHERE email = 'yamada@example.com'
[Presentation] エラー: メールアドレス「yamada@example.com」は既に登録されています。
===== ケース4:会員一覧の表示 =====
[Presentation] 会員一覧リクエスト受付
[DataAccess] SELECT * FROM member
会員ID:1 氏名:山田太郎 メール:yamada@example.com 年齢:30歳
会員ID:2 氏名:佐藤花子 メール:sato@example.com 年齢:25歳
(全 2 件)
ケース1では、ドメイン層の重複チェックで SELECT が発行され、問題がないので INSERT されて会員IDが採番された。
ケース2では、ログに [DataAccess] が1行も出ていない。
年齢が数字でない・氏名が空といった形式エラーはプレゼンテーション層で弾かれ、ドメイン層・データアクセス層まで処理が届いていないことが分かる。
ケース3では、15歳の登録はドメイン層の年齢チェックで弾かれたため、データアクセス層は呼ばれていない。
メールアドレス重複の場合は、重複確認のための SELECT は発行されているが、INSERT はされていない。
どちらも業務ルール違反としてドメイン層が BusinessRuleException を投げ、プレゼンテーション層がそのメッセージを表示している。
ケース4では、正常に登録できた2件だけが一覧表示された。
このように、各層が自分の役割だけを担当しているため、「入力チェックを直したい」ならプレゼンテーション層、「年齢制限を20歳に変えたい」ならドメイン層、「本物のDBに保存したい」ならデータアクセス層と、修正箇所がすぐに分かる。
コメント