オブジェクト指向 【プログラミング発展】

オブジェクト指向プログラミングは、それぞれの物などの概念ごとに、その物とは何かを設計、実装していく手法です。

これまでのプログラム

これまでは、手続き型プログラミングという、一連の処理を書き連ねる方式でした。しかし、これには限界があります。

再利用ができない

手続き型プログラミングは、処理単位で書き連ねるものです。そのため、例えばゲームで車を動かすコードを作ったとすると、味方の車、敵の車、破壊された車など、それぞれで大量のコードが複製され、似ているのに使い回せないといった問題が発生します。

つまり、動作の手続きを基準とするので、現実にある物1つ1つを基準に当てはめるということが困難です。

保守性が低い

変数やメソッドは全体から見えます。クロージャで無理やりできなくはないですが、コードは複雑になり、やはり再利用が困難です。

グローバルであるということは、処理を走らせたときの副作用 (ようは書き換えた変数が遠くの別の処理に影響が及ぶ) が発生する可能性が高くなるということです。

また、再利用ができないため、似たようなコードが量産され、規模が大きくなってくると収集がつかなくなります。

オブジェクト指向とは

現実にあるようなモノごとに設計を組み、モノ同士の関係を定義してシステムを作るような方法です。つまり、そのモノとは何かと、そのモノは何ができるかに焦点を当てて作っていきます。

とはいえ、これだけでは分かりづらいですね。例えば、学校について考えてみましょう。

学校は教育機関に属しています。そして、学校といっても小学校、中学校、高校等の分類があります。

その学校には、生徒、大学なら学生が通っています。そこで、学校と生徒などをオブジェクトとして整理してみます。

学校とはなにかや、生徒とはなにかを、分類で表しています。そして、もう一つは生徒は学校とどのような関係にあるかを表しています。

オブジェクト指向プログラミングでは、この関係をほぼそのままプログラム本体に落とし込むことができます。

メリット

オブジェクト指向にはメリットがあります。

  • 人間が行う分類と同じ感覚で設計を分離できる
  • オブジェクト同士は独立しているため、複数人での作業が行いやすい
  • オブジェクトという明確な基準があり、かつ持てる関係性を絞ることで、設計しやすく、設計図も見やすくできる

用語

オブジェクト指向では、特有の用語があるのでまずはそれを解説します。すべて重要な用語です!

多いので、いきなり全部理解するのはかなり難しいと思います。とりあえずざっと読んで、必要に応じて見返しながら進めていくと良いと思います。

クラス

クラスとは、プログラム上における、オブジェクトの設計図のようなものです。どのような機能があって、どのように動くのかを定義します。

インスタンス

クラスを元に作った実際の物です。

オブジェクト

オブジェクトは、クラスやインスタンスを全部ひっくるめた総称です。

let a = 10;a10もオブジェクトの1つです。

メソッド

クラスがもつ関数です。ただ、その関数内にある関数は、メソッドではなく普通の関数です。

フィールド

クラスが持つ変数のことです。ただし、クラスのメソッド内にある変数はフィールドではなく、ただの変数です。

メンバ

クラスが持つフィールドやメソッドの総称です。それぞれメンバ変数、メンバ関数とも呼びます。

class Hoge {
    constructor() {  // コンストラクタ。インスタンス作成時に必ず実行される
        this.name = "hoge"; // フィールド
        this.type = "fuga"; // フィールド
    }
    getName() {/* ... */} // メソッド
    getType() {/* ... */} // メソッド

    setName() { // メソッド
        const hige = 100; // ただの変数
        const validate = () => { // ただの関数
            /* ... */
        }
    }
}

カプセル化

別のオブジェクトから直接操作されないよう隠蔽し、別のオブジェクトに向けて様々な操作を提供することです。

例えば、人についてのオブジェクトがあったとして、腕の角度を内部にデータとして持っていたとします。 もしそれがどこでも操作できる場合、データの入力ミスでとんでもない方向に腕が曲がるかもしれません。 しかし、腕を曲げるという動作 (メソッド) を公開し、そのメソッド内で腕の角度の範囲を修正すれば、とんでもない角度になることはなくなります。

継承

is-a関係を作ることです。

例えば、大学は教育機関の1つです。つまり、大学 is 教育機関ということです。この場合、大学は教育機関を継承している状態です。

class EducatinalInstitution {/* ... */}

// 大学が教育機関を継承している (extends)
class University extends EducatinalInstitution {
    /*...*/
}

しかし、教育機関 is 大学 ではないので、入れ替わらないようにしましょう。

親クラス (スーパークラス、基本クラス)

クラスを継承した際の、継承元のクラスです。

class EducatinalInstitution {/* ... */}

// 大学が教育機関を継承している (extends)
class University extends EducatinalInstitution {
    /*...*/
}

の例だと、EducatinalInstitutionが親クラスです。

子クラス (サブクラス、派生クラス)

クラスを継承した際の、継承先のクラスです。

上の例だと、Universityが小クラスです。

ポリモーフィズム

多様性です。例えば、学校名について考えてみます。

ポリモーフィズム例

もちろんみんな違います。では、それぞれ別々の名前でメソッドを作るのでしょうか?

使う側からしたら、全部学校名なのだから、いずれも学校名を取得するという同じ機能で取得したいですよね。

つまり、同じ名前の機能だけど、それぞれ挙動が違うように多様性をもたせるということです。

(オブジェクト指向における) インターフェース

クラスには実際に動作なども書きますが、インターフェースには動作に必要な情報と出力の定義だけをするものです。

つまり、何ができるかを書き、どうやってするかはインターフェースには書きません。

interface Hoge {
    fly(): void; // 飛ぶという動作。しかし、どうやって飛ぶか、飛ぶとどうなるかはここには書かない
    getName(): string; // 名前を取得できることを示す。しかし、具体的にどうやって、どんな名前を取得するかは書かない
}

インターフェース (メソッドの入出力等としての)

メソッドの仮引数や戻り値といった、実際に使うために必要な情報をひっくるめたものです。

オブジェクト指向とは少し外れますが、用語として頻繁に登場します。

クラスを作る

例えば、学校に関するクラスを作ってみます。とりあえず用意してみます。

class School {
    constructor() {
        this.room = [
            {/* 教室のデータ */},
        ];
        this.students = [
            {/* 通学している生徒の情報、趣味、学年、年齢等 */},
        ];
        this.teacher = {
            {/* 教師の情報、家族構成、年齢など */},
        };
    }
}

さて、このままではよくありません。学校なのに生徒や教師のあらゆる個人情報を学校が保管しています。家にいる生徒に友達が会いに行くのに、なぜか学校に問い合わせないといけない状態です。

そこで、人に関する情報を分離してみます。

class Student {
    constructor(name, age) {
        this.name = name;
        this.age = age;
        this.schoolDestination = null; // 通学先の情報が入る
    }
}
class Teacher {
    constructor(name, age) {
        this.name = name;
        this.age = age;
        this.commutingDestination = [
            /* 通勤先の学校のIDが入る。通勤先は複数かるかもしれない */
        ];
    }
}
class School {
    constructor() {
        this.room = [
            {/* 教室のデータ */},
        ];
        this.students = [
            /* Studentのインスタンスが並ぶ */
        ];
        this.teacher = [
            /* Teacherのインスタンスが並ぶ */
        ];
    }
}

これで、個人情報を分離 (責任の分離) することができました。

ここで、生徒と教師は、どちらも人です。名前も年齢もみんなが持っていますね。共通している情報を切り出しましょう。

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}
class Student extends Person {
    constructor(name, age) {
        super(name, age); // スーパークラスのコンストラクタ呼び出し
        this.schoolDestination = null; // 通学先の情報が入る
    }
}
class Teacher extends Person {{
    constructor(name, age) {
        super(name, age);
        this.commutingDestination = [
            /* 通勤先の学校のIDが入る。通勤先は複数かるかもしれない */
        ];
    }
}
class School {/* ... */}

本来は、教室も種類があって、普通の教室、家庭科室、理科室など色々ありますが、きりがないので今は省略します。同じようにClassRoomクラスを切り出してみましょう!

これである程度の設計ができました。クラス設計では、

  • 親クラスは、子クラスの知識を持たなくて良いようにする
    子クラスが親クラスのメソッドやフィールドを使うことはあっても、親が子のメソッドを呼んだりはしない
  • is-a関係はできるだけ細分化して整理する
  • できるだけ1つのクラスは小さくなるようにする
    大きいクラスは、大概複数にまたがる概念を持ってしまっている。例えば、学校と人と土地のすべての情報を持ったクラスになっているなど

クラスは大量にあっても問題ありません。まずはこのあたりを意識しましょう!

慣れてきたら、次のオブジェクト指向の原則を意識してみると良いです。

クラスの書き方は言語によってかなり異なるので、それぞれの言語で仕様を確認する必要があります。

オブジェクト指向の原則

オブジェクト指向では、主に5つの原則に従って設計します。この原則をまとめてSOLID原則と呼びます。

言葉を覚える必要はあまりないので、何を意識すればよいかを把握しましょう。

単一責任の原則

これは、1つのクラスは1つのだけの責任をもつということです。責任とは、役割やできることです。

例えば、車に関するクラスがあったとします。その車がタイヤを動かしたり、ウィンカーを出したりするのは問題ありません。これらは車が行う役割 (責任) です。

しかし、車が搭乗している人の行動を制御 (ブレインジャック!?) したり、信号機の色を変えたりするような役割を持たせてはいけません。

運転手目線だと、運転手がタイヤを直接動かしたりはおかしい (ハンドルとペダルで間接的に動かす) ですよね。逆に、運転手が運転のためにハンドルやペダルを操作することはある (運転の責任は運転手にある) ため、それは問題ありません。

開放閉鎖の原則

拡張に対して開いて (open) いなければならず、修正に対して閉じて (closed) いなければならない。何を言っているかさっぱりですね。

言い換えると、

  • 機能の追加時は既存のコードはそのままに、オブジェクトの追加で対応する (そして、必要なものだけ、その新しいオブジェクトを利用するようにする)
  • 修正時は、修正対象のオブジェクトだけ修正すれば良いようにする

ということです。1つ目は主にクラスの継承によって対応します。

リスコフの置換原則

サブクラスは、その基本(スーパー)クラスと置換可能でなければならないというものです。

これは、クラスをインスタンス化する際に、基本クラスで作っていたものをサブクラスのインスタンスに入れ替えても動作するようにする、というものです。

つまり、クラスを設計する際に、サブクラスはスーパークラスより機能を狭めるのではなく、拡張したり特徴をつけるという方向で実装しようということです。

class Person {/* ... */}
class Student extends Person {/* ... */}

person = new Person(); // ここをStudentに入れ替えても問題なく動作するように設計する

インターフェース分離の原則

汎用的なインターフェースより、各クライアントに特化したインターフェースが複数のほうが良い、ということです。

あまりに大きなインターフェースをもたせてしまうと、本来無いはずの責任を負わされているように見え、勘違いなどのもとになります。

// 悪い例
interface IAnimal {
    fly(): void;
    swim(): void;
    walk(): void;
}

class Bird implements IAnimal {
    fly() {/*...*/}
    swim() {/* !? */}
    walk() {/* ... */}
}

class Human implements IAnimal {
    fly() {/* 人間が生身で飛べる!?!? */}
    swim() {/*...*/}
    walk() {/* ... */}
}
//良い例
interface IAnimal {/* すべての動物に共通するもの */}
interface IFlyable extends IAnimal {
    fly(): void;
}
interface ISwimmable extends IAnimal {
    swim(): void;
}
interface IWalkable extends IAnimal {
    walk(): void;
}

class Human implements ISwimmable, IWalkable {
    swim() {/* ... */}
    walk() {/* ... */}
}

依存性逆転の原則

これは、依存先の知識 (詳細な動作) を持たなくても良いようにしよう、というものです。

例えば、クラスAがクラスBの機能を利用していたとします。クラスBに修正を加えれば、クラスAに影響が及ぶ可能性があります。そのため、Bを修正したらAも修正を入れる必要が大きくなります。クラスBからクラスCに依存を変えるなど、別のものに切り替えることも困難です。

そこで、クラスAの依存先を、BではなくBの親クラスに依存させ、Bの内部について知らなくても問題ないようにしようということです。

これの何が良いかというと、簡単に依存先を別のものに入れ替えることができるという点です。

// 悪い例
class MySQL {/* ... */}
class HogeDAO {
    constructor(database: MySQL) {
        // コネクションなど
        // もしOracleDBに変えたとして、こちらの実装を変更しなければならない
    }
    getHoge() : string {
        // ここも変更しないといけないかもしれない
    }
}

const dao = new HogeDAO(new MySQL());
// 良い例
interface IDatabase {/* ... */}
class MySQL implements IDatabase {/* ... */}
class DbMock implements IDatabase {/* ... */}

class HogeDAO {
    // databaseはMySQLではなくIDatabaseに依存するようにする
    constructor(database: IDatabase) {
        // OracleDBに変えても、インターフェースが同じなのでここは変えなくて良い
    }

    getHoge() : string {
        // ここもインターフェースが同じなので変えなくて良い
    }
}

// 外部からオブジェクトを作成して入れる
// MySQLはIDatabaseを継承しているのでOK
const dao = new HogeDAO(new MySQL());

// インターフェースを合わせて実装するため、別のものを簡単に指定できる
const dao = new HogeDAO(new DbMock());

これらの原則はどうしたいのか

つまり、できるだけ抽象クラスに依存するようにして、詳細に依存しないようにする、ということです。

これにより、修正範囲を狭くし、容易な機能追加や仕様変更を可能にします。

特にWeb系は変化が早いため、意識していないと大変なことになりがちです。

まとめ

オブジェクト指向とはなにか、どのように設計していくのかを紹介しました。

これまでの単純に処理を並べるだけのプログラムとは違い、それぞれのモノを基準に設計を分離し、その関係性を定義していきます。

また、オブジェクト指向に正解はありません。人によってオブジェクトの分離の基準は異なります。

一筋縄では行かず、ある意味最後の難関とも言えます。とにかくクラスを作り、例を見て、慣れていくことが大事です。まずはゲームやGUIアプリケーションのように、元からクラスが用意されているものから初めて見ると良いと思います。

oop

役に立ったらシェアしよう!