🍇

10.2 クラスの関連と依存~Java Basic編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
  • 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
  • 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
  • 今後、フリーランスエンジニアとしてのキャリアを検討している方
  • Chat GPT」のエンジニアリングへの活用に興味のある方
  • Oracle認定Javaプログラマ」の資格取得を目指している方
  • IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

10.2 クラスの関連と依存

チャプターの概要

このチャプターでは、クラスとクラスの間に「関連」や「依存」があるケースについて学びます。

10.2.1 クラスとクラスの関連

クラス間の関係性のパターン

これまでのレッスンでは、作成したクラスはフィールドとしてプリミティブ型やJava SEクラスライブラリが提供する型(String型)を保持していましたが、個々のクラスは独立した存在でした。
実際のオブジェクト指向開発では、開発者が作成した複数のクラス同士に何らかの関係性があるケースが殆どです。例えば「社員」と「部署」を考えると、「社員は部署に所属している」という関係性になりますが、このようなクラス間の関係性を「関連」と呼びます。また「取引」と「顧客」「商品」を考えると、「取引するためには顧客と商品が必要である」という関係性になりますが、このようなクラス間の関係性を「依存」と呼びます。
まずこのレッスンでは、クラス間の「関連」について説明し、次のレッスンで「依存」について取り上げます。

クラス間の関連

クラス間に関連がある場合、両クラスの間には「~は~を持っている」という関係(has-a関係)が成り立ちます。例えば「社員」と「部署」の場合は、「部署は社員を持っている」という関係が成り立ちます。
オブジェクト指向モデリングでは、関連の中でも特に両者が「全体と部分」の関係性にある場合、これを「集約」と呼びます。また「集約」の中には「自動車とエンジン」のように、「ある部分が欠けると全体が成り立たない」というような強い関係性がある場合、これを「コンポジッション」と呼びます。ただしこのようなモデリング技法については本コースの範囲を超えるため、本コースでは「関連」で一本化します。

話をJavaに戻します。2つのクラス、FooとBarがあり、両者に関連がある場合、FooはフィールドとしてBarへの参照を保持し、FooからBarにはフィールド(またはゲッター)を介してアクセスします。
このようなクラスの関連をクラス図で表すと、以下のようになります。

【図10-2-1】クラス図(2つのクラスの関連)
image.png

クラス図では、関連は実線で表します。

方向と多重度

2つのクラス間に関連がある場合、関連に付随する考え方として、方向や多重度があります。
方向とは、2つのクラス間をそれぞれどういう方向にアクセスできるか、という考え方です。例えば2つのクラスFooとBarがあり、その実装においてFooはBarへの参照をフィールドとして保持している(逆にBarはFooへの参照を持っていない)のであれば、方向という観点では「FooからBarへの単方向の関連」となります。
また多重度とは、自分と相手がお互いにインスタンスをいくつ保持するか、という考え方です。例えば「社員」と「部署」では、「1つの部署には複数の社員が所属するが1人の社員は1つの部署にしか所属できない」という関係性が成り立つとすると、社員の多重度は多となり、部署の多重度は1となります。このような関係性にある場合、社員と部署は「多対1の関係にある」となります。
なおこのような「社員」と「部署」を、本コースでは「会社モデル」と呼称し、この後の説明の中でも例として用います。

関連の実装(単方向)

前項で取り上げた「会社モデル」を題材に、「社員」を表すEmployeeクラスと「部署」を表すDepartmentクラスを作成してみましょう。ここでは「社員」から「部署」に対して、単方向の関連を持つものとします。
Employeeクラス、Departmentクラスのコードを、順に示します。

pro.kensait.java.basic.lsn_10_2_1.Employee
public class Employee {
    private int id; // ID
    private String name; // 名前
    private Department department; //【1】関連先のDepartmentクラス
    private int salary; // 月給
    // コンストラクタ
    public Employee(int id, String name, Department department, int salary) {
        this.id = id;
        this.name = name;
        this.department = department;
        this.salary = salary;
    }
    // アクセサメソッド
    ........
}
pro.kensait.java.basic.lsn_10_2_1.Department
public class Department {
    private int id; // ID
    private String name; // 名前
    private String location; // 場所
    // コンストラクタ
    public Department(int id, String name, String location) {
        this.id = id;
        this.name = name;
        this.location = location;
    }
    // アクセサメソッド
    ........
}

「社員」は「部署」への関連を持つため、EmployeeクラスのフィールドとしてDepartment型を定義します【1】。
このようなEmployeeクラスとDepartmentクラスの関連をクラス図で表すと、以下のようになります。EmployeeからDepartmentに対して、実線で矢印を引く形になります。

【図10-2-2】会社モデルのクラス図(1)
image.png

このモデルに対して、インスタンスは以下のように生成します。

snippet_1 (pro.kensait.java.basic.lsn_10_2_1.Main_1)
Department sales = new Department(1, "企画部", "東京本社"); //【1】
Employee alice = new Employee(1, "Alice", sales, 300000); //【2】
Employee bob = new Employee(2, "Bob", sales, 420000); //【3】
Employee carol = new Employee(3, "Carol", sales, 380000); //【4】

ここではまず、企画部という部署のインスタンスを生成しています【1】。次に3人の社員、Alice、Bob、Carolのインスタンスを生成します【2、3、4】。3人とも、企画部に所属しているものとします。ここで注目してもらいたいのが、3つのEmployeeインスタンスを生成するとき、コンストラクタに【1】で生成したDepartmentインスタンス(変数sales)を指定している、という点です。このようにして、EmployeeとDepartmentを関連付けます。

それでは、snippet_1で生成されたEmployeeインスタンス(変数alice)から、Departmentインスタンスに対してアクセスしてみましょう。

snippet_2 (pro.kensait.java.basic.lsn_10_2_1.Main_1)
String deptName = alice.getDepartment().getName();

ここでは、EmployeeのゲッターによってまずDepartmentインスタンスを取得し、そのgetName()メソッドを呼び出すことで、Aliceが所属する部署の名前を取得しています。このコードを実行すると、変数deptNameには"企画部"が格納されます。

関連の実装(双方向)

既出の「会社モデル」は「社員」から「部署」への単方向でしたが、ここでは双方向でのアクセスが可能になるように修正します。すなわち「社員」から「部署」にもアクセスできるが、同時に「部署」から「社員」にもアクセスできる、というケースです。「部署」から「社員」にアクセスできるように、Departmentクラスのコードを、以下のように修正します。

pro.kensait.java.basic.lsn_10_2_1.Department
public class Department {
    private int id; // ID
    private String name; // 名前
    private String location; // 場所
    private Employee[] employees; //【1】関連先のEmployeeクラス配列
    // コンストラクタ
    public Department(int id, String name, String location) {
        this.id = id;
        this.name = name;
        this.location = location;
    }
    // アクセサメソッド
    ........
}

Departmentクラスには、フィールドとしてEmployeeの配列を定義しました【1】。このモデルでは「社員」と「部署」は「多対1」の関係であり、「1つの部署には複数の社員が所属する」ので、Employeeは配列(またはコレクション)にする必要があります。
今度はクラス図は、以下のようになります。EmployeeとDepartmentに対して、実戦で双方向の矢印を記述します。

【図10-2-3】会社モデルのクラス図(2)
image.png

このモデルに対して、インスタンスは以下のように生成します。

snippet_1 (pro.kensait.java.basic.lsn_10_2_1.Main_2)
Department sales = new Department(1, "企画部", "東京本社");
Employee alice = new Employee(1, "Alice", sales, 300000);
Employee bob = new Employee(2, "Bob", sales, 420000);
Employee carol = new Employee(3, "Carol", sales, 380000);
Employee[] employees = {alice, bob, carol}; //【1】
sales.setEmployees(employees); //【2】

4行目、すなわちCarolのEmployeeインスタンスを生成する処理までは、前項のMain_1クラス#snippet_1と全く同様です。ただし今回のケースは「社員」と「部署」が双方向の関連を持ちます。双方向の関連を実現するために、お互いをお互いのコンストラクタの引数に宣言することも、コード上は可能です。ただし、実際にお互いをコンストラクタによってインスタンス生成しようとすると、行き詰まりが起きてしまいます。まず「社員インスタンスを作るためには部署インスタンスが必要」です。一方で「部署インスタンスを作るためには社員インスタンスが必要」となり、要は、どちらのインスタンスを先に作るべきかという、言うなれば「鶏と卵」のようなジレンマが発生します。
ではどうすれば良いかと言うと、まずEmployeeからDepartmentへの関連の作り方から説明します。この関連は、前項のMain_1クラス#snippet_1と同様に、Employeeインスタンスを生成するときに、コンストラクタにDepartmentインスタンスを渡すことで作ります。次に逆方向、すなわちDepartmentからEmployeeへの関連の作り方です。まず3人の社員を表すEmployeeの配列を生成します【1】。そして、Department(変数sales)にはコンストラクタではなく、後からセッターによって配列をセット【2】することで、関連を作ります。このようにすれば「鶏と卵」問題は解消できます。

今度は、DepartmentからEmployeeに対してアクセスしてみましょう。以下のコードを見てください。

snippet_2 (pro.kensait.java.basic.lsn_10_2_1.Main_2)
employees = sales.getEmployees();
for (Employee e : employees) {
    System.out.println(e.getName());
}

ここではDepartmentのゲッターを経由してEmployeeにアクセスし、企画部に所属する社員の配列を取得しています。このコードを実行すると、ループ処理によって配列の個々の要素(Employeeインスタンス)が取り出され、"Alice"、"Bob"、"Carol"と順にコンソールに表示されます。

10.2.2 クラスからクラスへの依存

クラス間の依存関係

このレッスンでは、クラス間の依存関係について説明します。あるクラスが別のクラスのローカル変数になっていたり、メソッドの引数や戻り値になっている場合、両者は依存関係にあります。
例えば2つのクラス、FooとBarがあるものとします。Fooのメソッド内でBarのインスタンスを生成し、それをローカル変数として保持するようなケースや、Fooのメソッドが引数としてBarを受け取ったり、戻り値としてBarを返すようなケースは、「FooはBarに依存している」ことになります。
このようなクラスの依存関係をクラス図で表すと、以下のようになります。

【図10-2-4】クラス図(2つのクラスの依存)
image.png

クラス図では、クラスの依存関係は破線矢印で表します。

依存関係の実装

ここでは「取引モデル」を題材に、クラスの依存関係を具体的に説明します。「取引」を表すTransactionクラスがあり、そのクラスの中に注文処理をするためのorder()メソッドがあるとしたら、以下のようなコードになるでしょう。

snippet
void order(Customer customer, Product product) {
    // 注文処理
    ........
}

このorder()メソッドは、「顧客」を表すCustomerクラスと「商品」を表すProductクラスを引数として受け取っています。これらの引数を使って、指定された顧客に対して商品を注文する処理を行います。この場合Transactionクラスは、CustomerクラスとProductクラスに依存している、という関係になります。

this渡しによる相互依存

あるクラスから別クラスのメソッドを呼び出すとき、this、すなわち自身のインスタンスを渡すケースがあります。
例えば2つのクラス、FooとBarがあるものとします。そしてBarクラスのprocess()メソッドは、Fooを引数として受け取り、何らかの処理を行うものとします。このようなケースでは、FooからBarのprocess()メソッドを呼び出すとき、thisを引数として渡すことになります。
具体的には以下のコードのようになります。

pro.kensait.java.basic.lsn_10_2_2.Foo
class Foo {
    void doSomething() {
        Bar bar = new Bar();
        bar.process(this); // this渡し
    }
}
pro.kensait.java.basic.lsn_10_2_2.Bar
class Bar {
    void process(Foo foo) {
        // fooに対する処理
        ........
    }
}

このケースではFooはBarに依存し、またBarもFooに依存するという、相互依存の関係になります。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. クラスとクラスの関係性には、「関連」や「依存」といったパターンがあること。
  2. クラス間に関連がある場合、両クラスの間には「~は~を持っている」という関係(has-a関係)が成り立つこと。
  3. 関連には、方向と多重度といった考え方があること。
  4. クラスが別のクラスのローカル変数、引数、戻り値になっている場合、両者は依存の関係にあること。

Discussion