オブジェクト指向の基礎

📢 この記事は gemini-3-flash-preview によって翻訳されました

オブジェクト指向 (Object-Oriented, OO) は、すごく実用的なシステム化されたソフトウェア開発手法なんだ。

手続き型とオブジェクト指向

一つの問題から考えてみよう。「ゾウを冷蔵庫に入れるには、何ステップ必要かな?」

普通は、まず冷蔵庫を開けて、次にゾウを中に入れて、最後に冷蔵庫を閉めるよね。

手続き型

「どうやってやるか」を気にするスタイル。機能を一歩ずつ実現していくんだ。

さっきの問題なら:

  1. 自分が冷蔵庫を開ける
  2. 自分がゾウを冷蔵庫の中に押し込む
  3. 自分が冷蔵庫のドアを閉める

オブジェクト指向

「誰にやらせるか」を気にするスタイル。オブジェクトの操作を呼び出すことで機能を実現するんだ。

さっきの問題なら:

オブジェクトを作る:ゾウ、冷蔵庫

  1. 冷蔵庫がドアを開ける
  2. ゾウが冷蔵庫の中に入る
  3. 冷蔵庫がドアを閉める

オブジェクト指向の基礎

オブジェクト指向 = オブジェクト + 分類 + 継承 + メッセージによる通信

この 4 つの概念を使って開発されたソフトウェアシステムが、オブジェクト指向だと言えるよ。

オブジェクト

現実世界は、たくさんの具体的なモノ、出来事、概念、ルールで成り立っていて、これらはすべてオブジェクトと見なすことができる。

オブジェクト指向システムにおいて、オブジェクトは基本的な実行時のエンティティなんだ。データ(属性)と、そのデータに作用する操作(振る舞い)の両方を含んでいる。一つのオブジェクトは、通常「オブジェクト名」「属性」「メソッド」の 3 つの部分で構成されるよ。

メッセージ

オブジェクト間で通信するための仕組みをメッセージと呼ぶ。あるメッセージを特定のオブジェクトに送るとき、それには受信側のオブジェクトに特定の活動を実行させるための情報が含まれている。メッセージを受け取ったオブジェクトは、それを解釈して応答するんだ。

メソッド呼び出しの引数渡しみたいなものだね。

クラス

クラスは、大体似たようなオブジェクトの集まりを定義するものだ。一つのクラスに含まれるメソッドとデータは、オブジェクトのグループに共通する振る舞い属性を記述している。オブジェクトの共通の特徴を抽象化してクラスに保存することは、オブジェクト指向技術の最も重要なポイントなんだ。充実したクラスライブラリがあるかどうかは、そのオブジェクト指向プログラミング言語が成熟しているかどうかの重要な指標になる。

クラスはオブジェクトを抽象化したものであり、**オブジェクトはクラスを具体化したもの(インスタンス)**なんだ。分析や設計のときは、具体的なオブジェクトではなく、通常はクラスに注目する。オブジェクトを一つずつ定義する必要はなくて、クラスを定義して、その属性に異なる値を割り当てるだけで、そのクラスのオブジェクトインスタンスが手に入るんだ。

クラスは 3 つに分けられる:エンティティクラス、インターフェースクラス(境界クラス)、コントロールクラス。コントロールクラスのオブジェクトは、処理の流れを制御し、コーディネーターの役割を果たすよ。

クラスの中には「一般と特殊」の関係を持つものがある。あるクラスが別のクラスの特殊なケースだったり、逆に一般的なケースだったりする。特殊なクラスは一般なクラスの「サブクラス」で、一般的なクラスは特殊なクラスの「スーパークラス(親クラス)」になるんだ。

通常、一つのクラスとそのクラスのすべてのオブジェクトをまとめて「クラスとオブジェクト」またはオブジェクトクラスと呼ぶこともある。

メソッドのオーバーロード

メソッドをオーバーロードする方法:

  1. メソッド名は同じで、引数の数が異なる
  2. メソッド名は同じで、引数の型が異なる
  3. メソッド名は同じで、引数の型の順番が異なる

Java を例にすると、関数の書式はこんな感じ:

1
2
3
4
5
6
/*
アクセス修飾子 戻り値の型 メソッド名(引数の型1 引数名1, 引数の型2 引数名2, ···)
{
    メソッドの本体
}
*/

例を挙げるね:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test
{
    // 元のメソッド
    public void sum(int a, double b)
    {
        System.out.println(a + b);
    }
    // 1. メソッド名は同じで、引数の数が異なる
    public void sum(int a, double b, int c)
    {
        System.out.println(a + b + c);
    }
    // 2. メソッド名は同じで、引数の型が異なる
    public void sum(int a, int b)
    {
        System.out.println(a + b);
    }
    // 3. メソッド名は同じで、引数の型の順番が異なる
    public void sum(double a, int b)
    {
        System.out.println(a + b);
    }
}

オブジェクト指向の三大特徴

オブジェクト指向の 3 つの基本特徴は、カプセル化、継承、多態性(ポリモーフィズム)だよ。

カプセル化

カプセル化は情報を隠蔽する技術で、オブジェクトの利用者と作成者を切り離し、定義と実装を分けることが目的なんだ。設計者から見ればオブジェクトはプログラムモジュールだけど、ユーザーから見れば期待通りの振る舞いを提供してくれるものに見える。

つまり、現実のモノを抽象的なクラスにまとめ、自分自身のデータやメソッドを信頼できるクラスやオブジェクトだけに操作させ、信頼できないものからは情報を隠すことなんだ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Person{
    private String name;    // private を使ってアクセスを制限する
    private int age;
    
    public void setName(String name){
        // public メソッドを通じて属性を変更する手段を提供する
        this.name = name;
    }
    public String getName(){
        // public メソッドを通じて属性を取得する手段を提供する
        return name;
    }
    
    public void setAge(int age){
        if (age >= 0 && age <= 150)
            this.age = age;
    }
    public int getAge(){
        return age;
    }
    
    public void run(){
        System.out.println("走る!");
    }
}

継承

継承は、親クラスと子クラスの間でデータとメソッドを共有する仕組みだ。これはクラス間の関係の一つで、新しいクラスを定義・実装するときに、すでにある親クラスをベースにできるんだ。既存のクラスの内容を自分のものとして取り込み、そこに新しい内容を追加する感じだね。

一つの親クラスは複数の子クラスを持つことができて、これらの中身はすべて親クラスの特殊なケースになる。親クラスは子クラスの共通の属性やメソッドを記述しているんだ。子クラスは親クラス(または祖先クラス)の属性やメソッドを引き継げるから、子クラスでそれらを再定義する必要はない。もちろん、子クラス独自の属性やメソッドを追加することもできるよ。

もし子クラスが一つの親クラスからしか継承しないなら「単一継承」、二つ以上の親クラスから継承するなら「多重継承」と呼ぶ。

注:Java では一つの子クラスは一つの親クラスしか持てない(単一継承)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 前のコードの続き
public class Student extends Person{
    // キーワード extends を使って継承する親クラスを指定する
    private int id;
    
    public void setId(int id){
        this.id = id;
    }
    public int getId(){
        return id;
    }
    
    public void study(){
        System.out.println(getName() + "は勉強中");
    }
    
    // 親クラスのメソッドをオーバーライド(書き換え)
    public void run(){
        System.out.println(getName() + "は逃げたい");
    }
}

多態性 (ポリモーフィズム)

メッセージを受け取ったとき、オブジェクトはそれに応答する。異なるオブジェクトが同じメッセージを受け取っても、全く異なる結果を生じることがある。この現象を多態性と呼ぶんだ。多態性を使うと、ユーザーは一般的なメッセージを送るだけで、具体的な実装の詳細は受信側のオブジェクトに任せることができる。こうして、同じメッセージで異なるメソッドを呼び出せるようになるんだ。

多態性の実現は継承に支えられている。クラスの継承階層を利用して、共通の機能を持つメッセージを高い階層に置き、その機能を実現する異なる振る舞いを低い階層に置く。そうすることで、低い階層で生成されたオブジェクトが共通のメッセージに対して異なる応答を返せるようになるんだ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 親クラス
public class Person{
    public void work(){
        System.out.println("仕事をする");
    }
}

// 子クラス 1
public class Student extends Person{
    // メソッドのオーバーライド
    public void work(){
        System.out.println("学校に行く");
    }
    
    public void run(){
        System.out.println("Only the young CAN RUN!");
    }
}

// 子クラス 2
public class Worker extends Person{
    // メソッドのオーバーライド
    public void work(){
        System.out.println("出社する");
    }
    
    public void sleep(){
        System.out.println("寝る");
    }
}

// main
public class Main{
    public static void main(String[] args){
        // コンパイル時は左(型)を見て、実行時は右(中身)を見る
        Person stu = new Student();
        stu.work(); // "学校に行く" と出力される
        // stu.run(); は呼び出せない(Person 型には run がないから)
        
        Person wok = new Worker();
        wok.work(); // "出社する" と出力される
        // wok.sleep(); は呼び出せない
    }
}

// 実行結果:
// 学校に行く
// 出社する

多態性の形式

多態性にはいくつかの形式があって、Cardelli と Wegner は 4 つに分類しているよ。

image

  1. パラメータ多態 (Parametric polymorphism):広く使われている多態性で、最も純粋な多態性と言われている。
  2. 包含多態 (Inclusion polymorphism):多くの言語に存在し、最も一般的な例はサブタイプ化だ。つまり、ある型が別の型のサブタイプであること。
  3. 過負荷多態 (Overloading polymorphism):同じ名前が異なるコンテキスト(文脈)で異なる意味を持つこと。
  4. 強制多態 (Coercion polymorphism):型変換などによって実現される多態性。

動的バインディングと静的バインディング

バインディング(結合)とは、プロシージャの呼び出しと、その呼び出しに応答して実行されるコードを関連付けるプロセスのこと。一般的なプログラミング言語では、バインディングはコンパイル時に行われ、これを静的バインディングと呼ぶ。一方で、動的バインディングは実行時に行われる。つまり、呼び出しが発生するまで、どのコードが実行されるか決まらないんだ。

動的バインディングはクラスの継承や多態性と深く関わっている。継承関係において、子クラスは親クラスの特殊なケースだから、親クラスが使える場所には子クラスのオブジェクトも置ける。そのため実行中に、あるオブジェクトがメッセージを送ってサービスを要求したとき、受信側のオブジェクトの具体的な状況に応じて、要求された操作と実装メソッドを接続する、つまり動的接続が行われるんだ。

オブジェクト指向分析

オブジェクト指向分析 (Object-Oriented Analysis, OOA) の目的は、アプリケーションの問題を理解することだ。理解の目的は、システムの機能や性能要件を確定させることにある。

OOA には 5 つの活動がある:オブジェクトの認定、オブジェクトの組織化、オブジェクト間の相互作用の記述、オブジェクトの操作の確定、オブジェクトの内部情報の定義。

オブジェクト指向設計

オブジェクト指向設計 (Object-Oriented Design, OOD) は、OOA で作成された分析モデルを設計モデルに変換すること。その目標は、システムの構築図(ブループリント)を定義することなんだ。通常、概念モデルから生成された分析モデルを実行環境に組み込む際、実装上の問題を考慮して調整や補充が必要になる。例えば、使用するプログラミング言語が多重継承をサポートしているかどうかに合わせてクラス構造を調整したりする。OOA と OOD の間には大きな隔たりはなく、一貫した概念と表現方法が使われる。OOD も同様に、抽象化、情報隠蔽、機能の独立、モジュール化などの設計原則に従うべきだね。

オブジェクト指向設計の活動

OOD は OOA のモデルを再利用した上で、OOA に対応する以下の 5 つの活動を含んでいるよ。

  1. クラスとオブジェクトの識別
  2. 属性の定義
  3. サービスの定義
  4. 関係の識別
  5. パッケージの識別

オブジェクト指向設計の原則

  1. 単一責任の原則 (Single Responsibility Principle) 一つのクラスにとって、それを変更する理由はたった一つであるべきだ。つまり、あるクラスを修正する必要があるときはその理由が一つだけで、一つのクラスには一種類だけの責任を持たせるようにする。

  2. 開放・閉鎖の原則 (Open-Closed Principle) ソフトウェアのエンティティ(クラス、モジュール、関数など)は拡張に対して開いているべきだけど、修正に対しては閉じているべきだ。

  3. リスコフの置換原則 (Liskov Substitution Principle) サブタイプは、その基本(親)タイプと置換可能でなければならない。つまり、親クラスが登場するどんな場所でも、子クラスのインスタンスを親クラスの参照に代入できなければならない。子タイプのインスタンスがそのスーパークラスのどのインスタンスとも置き換えられるとき、それらは “is-a” 関係にあると言える。

  4. 依存関係逆転の原則 (Dependency Inversion Principle) 抽象は詳細に依存すべきではなく、詳細は抽象に依存すべきだ。つまり、高層モジュールは低層モジュールに依存すべきではなく、両者は抽象に依存すべきなんだ。

  5. インターフェース分離の原則 (Interface Segregation Principle) クライアントが利用しないメソッドに依存することを強制してはいけない。インターフェースはクライアントに属するもので、それが置かれているクラス階層に属するものではない。つまり、具体的なものではなく抽象に依存し、かつ抽象レベルにおいて詳細への依存があってはいけない。こうすることで、変化に最大限対応できるようになる。

以上がオブジェクト指向における五大原則(SOLIDの一部)だ。これらに加えて、Robert C. Martin は以下の原則も提唱しているよ。

  1. 再利用・リリース等価の原則 (Release-Reuse Equivalency Principle) 再利用の単位はリリースの単位と等価である。

  2. 共通閉鎖の原則 (Common Closure Principle) パッケージ内のすべてのクラスは、同じ種類の変更に対して共に閉じているべきだ。一つの変更があるパッケージに影響を与えるなら、そのパッケージ内のすべてのクラスに影響し、他のパッケージには影響を与えないようにする。

  3. 共通再利用の原則 (Common Reuse Principle) パッケージ内のすべてのクラスは共に再利用されるべきだ。パッケージ内の一つのクラスを再利用するなら、そのパッケージ内のすべてのクラスを再利用することになる。

  4. 非循環依存関係の原則 (Acyclic Dependencies Principle) パッケージの依存関係グラフに循環があってはいけない。つまり、パッケージ間の構造は直接的な非循環グラフでなければならない。

  5. 安定依存の原則 (Stable Dependencies Principle) より安定した方向へ依存するようにする。

  6. 安定抽象の原則 (Stable Abstractions Principle) パッケージの抽象度は、その安定度と一致すべきだ。

オブジェクト指向テスト

テストに関して言えば、オブジェクト指向で開発されたシステムのテストも、他の手法で開発されたものと大きな違いはないよ。

一般的に、オブジェクト指向ソフトウェアのテストは以下の 4 つのレベルで行われる。

  1. アルゴリズム層
  2. クラス層
  3. テンプレート層
  4. システム層

オブジェクト指向プログラミング

プログラミングパラダイム (Programming Paradigm) は、プログラミングをするときの基本的な考え方のモデルだ。考え方や使うツールを決定し、一定の適用範囲を持っている。歴史的には、手続き型、モジュール化、関数型、論理型と進化してきて、今のオブジェクト指向プログラミングパラダイムに至っているんだ。

オブジェクト指向プログラミング (Object-Oriented Programming, OOP) の本質は、オブジェクト指向プログラミング言語 (OOPL) を選び、オブジェクト、クラス、および関連する概念を用いてプログラミングを行うこと。カギとなるのは、クラスと継承を導入したことで抽象度がさらに高まった点だね。特定の OOP の概念は、通常 OOPL の特定の言語メカニズムを通じて体現されるんだ。

今では OOP はシステム分析やソフトウェア設計の範囲まで拡張されて、オブジェクト指向分析 (OOA) やオブジェクト指向設計 (OOD) という概念が登場している。これについては、さっき説明した通りだよ。

Visits Since 2025-02-28

Hugo で構築されています。 | テーマ StackJimmy によって設計されています。