Java | オブジェクト指向:パッケージ設計

Java Java
スポンサーリンク

パッケージ設計とは

パッケージ設計は、クラスやインターフェースを「意味のあるまとまり」に分けて整理し、依存の向きと公開範囲をコントロールすることです。狙いは、変更の影響を局所化し、再利用とテストをしやすくすること。役割単位でグルーピングし、外から使われる“公開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"); }
}
Java

app は 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 は契約を実装する。共通は最小限に留め、循環を作らない。これだけで、変更は局所化され、テスト容易性が上がり、拡張に強いプロジェクトになります。

タイトルとURLをコピーしました