パッケージ設計とは
パッケージ設計は、クラスやインターフェースを「意味のあるまとまり」に分けて整理し、依存の向きと公開範囲をコントロールすることです。狙いは、変更の影響を局所化し、再利用とテストをしやすくすること。役割単位でグルーピングし、外から使われる“公開API”と内部実装を分けることで、壊れにくい構造を作ります。
目的と基本方針(重要)
パッケージ設計の目的は「関心の分離」と「依存の方向の固定」です。UI・アプリケーションロジック・ドメイン(ビジネスルール)・インフラ(DB/ファイル/ネット)などの関心ごとを分け、上位は抽象へ依存、下位は抽象を実装する向きを守ります。公開すべき最小限だけを外に見せ、内部詳細は隠すことで、変更の波及を防ぎます。
名前付けと構成の勘所
パッケージ名は「役割」や「機能」で切る
package by feature(機能別)を基本にし、機能内では「app」「domain」「infra」のような層をさらに分けると読みやすくなります。層全体でまとめる package by layer だけにすると巨大化しやすいので、機能を軸に切り、層は機能内で表現するのがおすすめです。
com.example.shop
└─ order
├─ app // ユースケース・サービス
├─ domain // 集約・エンティティ・ドメインサービス・ルール
└─ infra // リポジトリ実装・外部接続
クラス名とパッケージ名の整合
パッケージ名は「そこに何があるか」を予測できるようにします。order.domain に Order, OrderService, OrderRepositoryPort があるなら、infra に S3OrderRepository, JdbcOrderRepository のように“実装”が並ぶ形が自然です。
公開範囲とアクセス制御(重要な深掘り)
Java のアクセス修飾子とパッケージ境界を組み合わせ、外部から見せるものを最小限にします。public は外から見える入口に限定し、内部詳細は package-private(修飾子なし)や protected にしてパッケージ内へ閉じ込めます。内部ユーティリティやヘルパーを安易に public にすると、依存が外へ漏れ、将来の変更が難しくなります。
// domain: 外部に見せたい契約は public
package com.example.shop.order.domain;
public interface OrderRepositoryPort {
void save(Order order);
}
// 内部だけで使うヘルパーはパッケージ内へ閉じる
class OrderValidator { // package-private(修飾子なし)
boolean valid(Order o) { return o != null && !o.items().isEmpty(); }
}
Java依存の方向を固定する
上位(app, domain)は下位の“具体”に依存せず、抽象(ポート)に依存させます。下位(infra)はその抽象を実装して外部世界と接続します。この向きをパッケージ構造で表現し、循環依存を避けます。
// domain(抽象)
package com.example.shop.order.domain;
public interface OrderRepositoryPort {
void save(Order order);
}
// app(ユースケース)
package com.example.shop.order.app;
import com.example.shop.order.domain.Order;
import com.example.shop.order.domain.OrderRepositoryPort;
public class PlaceOrderUseCase {
private final OrderRepositoryPort repo;
public PlaceOrderUseCase(OrderRepositoryPort repo) { this.repo = repo; }
public void place(Order order) { repo.save(order); }
}
// infra(具体実装)
package com.example.shop.order.infra;
import com.example.shop.order.domain.Order;
import com.example.shop.order.domain.OrderRepositoryPort;
public class JdbcOrderRepository implements OrderRepositoryPort {
@Override public void save(Order order) { System.out.println("save JDBC"); }
}
Javaapp は domain の“契約”にのみ依存し、infra が契約を実装するため依存の向きが正しく保てます。
例題で体感するパッケージ分割
例 1: 機能別+層別で整理する
注文機能なら order.app にユースケース、order.domain にエンティティと契約、order.infra に外部接続の実装を置きます。機能ごとに閉じた世界ができるため、他機能への波及が減り、変更しやすくなります。
com.example.shop
└─ order
├─ app
│ └─ PlaceOrderUseCase
├─ domain
│ ├─ Order
│ └─ OrderRepositoryPort
└─ infra
└─ JdbcOrderRepository
例 2: 共通部品は“共通パッケージ”へ。ただし最小限に
複数機能で使う共通コードは common(または shared)へ。ただし「何でも共通」にすると依存が絡みます。機能から独立できる純粋な部品だけを置くのがコツです。
com.example.shared.time
└─ Clock // 抽象ポート
com.example.infrastructure.time
└─ SystemClock // 具体実装
テスト容易性を高めるパッケージ設計
パッケージ境界で抽象と具体を分けると、ユースケースやドメインテストにフェイク実装を注入でき、外部接続なしで検証できます。infra の結合テストは別で行い、境界をまたぐ統合は少数のシナリオに絞ります。公開範囲を最小化しておくと、テスト用フェイク以外の“不用意な依存”が入り込むのを防げます。
// テストでフェイクを注入
class FakeRepo implements OrderRepositoryPort {
@Override public void save(Order order) { /* in-memory */ }
}
Javaよくある落とし穴と回避(重要)
「層ごとに巨大パッケージ」は変更の波及が大きくなります。機能を軸に閉じた世界を作り、その中で層を分けると影響範囲を狭められます。「何でも public」は外部依存を増やし、将来の修正が困難になります。入口だけを public にし、内部は package-private に。循環依存は厳禁です。互いに参照し合いそうなら、抽象(ポート)を間に置いて依存の向きを揃えましょう。共通パッケージの拡張は慎重に。機能に引きずられる“なんちゃって共通”は依存地獄の入口です。
まとめと実務の指針
パッケージ設計は「機能単位で閉じた世界を作る」「抽象と具体を分けて依存の向きを固定する」「公開範囲を最小化する」の三本柱が要です。domain は契約を持ち、app は契約に依存し、infra は契約を実装する。共通は最小限に留め、循環を作らない。これだけで、変更は局所化され、テスト容易性が上がり、拡張に強いプロジェクトになります。
