【PHP】ざっくりオブジェクト指向について①

【PHP】ざっくりオブジェクト指向について①

はじめに

本記事は、オブジェクト指向について学習した内容をまとめています。言語は PHP を使用し、以下の内容についてまとめています。

  • クラスの使用方法
  • プロパティについて
  • コンストラクタについて
  • アクセス権とカプセル化について
  • 継承について
  • オーバーライドについて
  • トレイトについて

サンプルコードの PHP のバージョンは8.1.20で、開発環境に Docker を使用しています。

オブジェクト指向とは?

オブジェクト指向とはプログラミングをする際の考え方のことで、プログラムを設計する設計手法(アーキテクチャ)のことです。

プログラムでオブジェクトというデータと振る舞い(メソッド)を組み合わせたコードを書くことで、プログラムでやりたいことを明確に設計することが出来ます。

オブジェクトとは「物体」や「実態」という意味で使用され、データと振る舞い(メソッド)を持った「モノ」であり、クラスと言った設計図からこのオブジェクトを生成してプログラム内で使用していきます。

具体的に言うと、「人」をオブジェクトとして考えると、「名前」や「年齢」と言った属性(プロパティ)と、「歩く」、「話す」と言った動作(メソッド)を持つイメージです。

オブジェクト指向プログラミングのメリット

オブジェクト指向プログラミングを実践することで以下のメリットがあります。

  • 関連するデータと振る舞いがオブジェクト内に集まるため、コードの変更やデバッグがしやすくなり保守性が高くなる
  • 保守性が高いとは、何か仕様変更が合った際にバグを生み出しにくかったり、誰が見てもわかりやすいコードであったりすることをいう
  • ポリモーフィズム(多態性)といった概念を用いることで、同じメソッドでも異なる振る舞いや動作を実現できるので柔軟なコードを書くことができる
    • 具体的な実装を隠蔽する(抽象化)することで、複雑な問題をシンプル且つ理解し易い形で管理することができる

オブジェクト指向プログラミングのデメリット

オブジェクト指向プログラミングのデメリットのデメリットとして以下の点が挙げられます。

  • オブジェクト指向プログラミングの概念(クラス、オブジェクト、継承、カプセル化、ポリモーフィズムなど)は、初学者にとっては理解が難しく時間がかかる
  • 適切なクラス設計や継承の設計、インターフェースの設計がされていないとコードの複雑化が増しメンテナンス性を損なう可能性がある
  • オブジェクトの生成やメソッドの呼び出しなどが、特に大規模なアプリケーションやリソースが限られた環境でパフォーマンスに影響を与えることがある
    • 現代のハードウェアの進歩ににより、これらの問題はあまり問題にならない事が多いが注意が必要

オブジェクト指向プログラミングのステップ

オブジェクト指向プログラミングでは以下のステップでコードを書いて行きます。

  1. クラスを作る
  2. プロパティ(名前、年齢)を作成し、オブジェクトに保持させたいデータを指定する
  3. クラスの中にコンストラクタ(オブジェクトからインスタンスを生成する際の初期化処理)を作る(無くても良い)
  4. メソッドを作成し、オブジェクトにさせたい動作や振る舞いを記述する
  5. クラスを元にインスタンスを作る(インスタンス化)

クラス

では実際にコードでクラスを作成してきます。今回は「人」を例にクラスを作成します。

"index.php"
<?php

// クラスを作成
class Human // 先頭は大文字にするのが慣習(小文字でもエラーにはならない)
{
    // プロパティ(インスタンス変数とも呼ばれる)を定義
    public $first_name; // 初期値を設定しないとNULLが入る
    public $last_name = "太郎";

    // コンストラクタは記述しなくても可

    // メソッドを定義
    public function helloWorld()
    {
        echo "こんにちは世界 \n";
    }
}

// インスタンス化
$human = new Human();
// メソッドを呼び出す
$human->helloWorld();

通常、インスタンス化するときに特定の値を指定したい場合、クラスにはコンストラクタを記述する必要がありますが、何も値を渡す必要がないクラスに設計している場合コンストラクタは必要ありません。

上記のスクリプトファイルをphp index.phpとコマンドから実行します。

hello-world

ターミナルにメソッドの実行結果が出力されています。

次にプロパティの値も確認します。

プロパティ

プロパティはクラスに紐づいている、そのクラスの変数のようなものと考えてください。

前述しましたが、オブジェクトに保持させたいデータを格納します。

"index.php"
// ~ 省略 ~

// インスタンス化
$human = new Human();
// メソッドを呼び出す
$human->helloWorld();

var_dump($human->first_name); // null

var_dump($human->last_name); // 太郎

hello-world_02

プロパティに初期値を指定しないとNULLが代入されます。NULLは空文字などとは異なり値が存在しないものとして扱われる特殊な値です。

コンストラクタを使用した場合

先程のプロパティはインスタンス化した後に値を代入しなけば同じ値が返されます。

生成するインスタンスごとにプロパティの値を変化させたい場合にコンストラクタを使用します。PHP の場合はpublic function __constructという特殊なメソッドをクラス内で定義します。

__construct()というメソッドはマジックメソッドと呼ばれnew演算子を使用してクラスがインスタンス化された際に最初に実行されるメソッドです。

// コンストラクタを定義してインスタンス化した際の初期化処理を記述する
public function __construct($first_name, $last_name)
{
    $this->first_name = $first_name;
    $this->last_name = $last_name;
}

次に、名前を連結させて出力するメソッドを定義し、複数のインスタンスを作成してみます。

"index.php"
class Human
{
    public $first_name;

    public $last_name;

    public function __construct($first_name, $last_name)
    {
        // $thisはクラスから生成したインスタンス自身を表す
        // 例: Humanクラスで指定したプロパティやメソッドにアクセスできる
        $this->first_name = $first_name;
        $this->last_name = $last_name;
    }

    public function helloWorld()
    {
        echo "こんにちは世界 \n";
    }

    // 名前を出力するメソッドを定義
    public function echoName()
    {
        echo "私の名前は、{$this->first_name} {$this->last_name} です。", PHP_EOL;
    }
}

$human1 = new Human("名無しの", "ごん兵衛");
$human2 = new Human("浦島", "太郎");
$human3 = new Human("ジョニー", "デップ");

$human1->echoName(); // 私の名前は、名無しの ごん兵衛 です。
$human2->echoName(); // 私の名前は、浦島 太郎 です。
$human3->echoName(); // 私の名前は、ジョニー デップ です。

実際に実行してみた結果です。

hello-world_03

コンストラクタを指定してインスタンス化する際に、任意の値を指定することで同じクラスでも異なる値を出力するインスタンスが作成されました。

注意点として、コンストラクタに指定した引数と、指定した引数の数が異なる場合はエラーになります。つまり、__construct()メソッドに引数を指定した場合、インスタンス化時に何かしらの値を代入する必要があります。

これはコンストラクタに限らず通常の関数でも同様で、定義した引数の数と実際に関数を使用するときに渡す引数の数が異なるとエラーになります。

"index.php"
// ~ 省略 ~

$human1 = new Human("名無しの"); // Error: 引数の数が足りない
$human2 = new Human("浦島", "太郎");
$human3 = new Human("ジョニー", "デップ");

// $human1 = new Human("名無しの", ""); // 何かしらの値を入れる必要がある

hello-world_05

デフォルト値と名前付き引数

PHP の関数ではデフォルト値を指定することが出来ます。これは__construct()メソッドでも指定することが出来ます。

"index.php"
class Human
{
    public $first_name;

    public $last_name;

    public function __construct($first_name = "名無しの", $last_name = "ごん兵衛")
    {
        // $thisはクラスから生成したインスタンス自身を表す
        // 例: Humanクラスで指定したプロパティやメソッドにアクセスできる
        $this->first_name = $first_name;
        $this->last_name = $last_name;
    }

    // ~ 省略 ~
}

$human1 = new Human();
$human2 = new Human("浦島", "太郎");
$human3 = new Human("ジョニー", "デップ");

$human1->echoName(); // 私の名前は、名無しの ごん兵衛 です。
$human2->echoName(); // 私の名前は、浦島 太郎 です。
$human3->echoName(); // 私の名前は、ジョニー デップ です。

デフォルト値を指定することで、コンストラクタの引数に値を渡さなくてもエラーなくインスタンスを生成することが出来ます。

また、PHP8.0 から名前付き引数という新しい機能が追加され、以下のように引数名を指定して値を渡すことも可能となりました。

"index.php"
class Human
{
    public $first_name;

    public $last_name;

    // インスタンス化する際の引数の順番に注目
    public function __construct($first_name = "名無しの", $last_name = "ごん兵衛")
    {
        $this->first_name = $first_name;
        $this->last_name = $last_name;
    }

    // メソッドを定義
    public function helloWorld()
    {
        echo "こんにちは世界 \n";
    }

    // 名前を出力するメソッドを定義
    public function echoName()
    {
        echo "私の名前は、{$this->first_name} {$this->last_name} です。", PHP_EOL;
    }
}

// 名前付き引数
$human3 = new Human(last_name: "デップ", first_name: "ジョニー");

$human3->echoName(); // 私の名前は、ジョニー デップ です。

これにより、引数の順次を気にせずに任意の順序で引数を渡すことが出来ます。

特に多くの引数を持つコンストラクタやメソッドに対して便利な機能になっています。

初期化時に定義していないプロパティへの値の代入

コンストラクタで値を初期化したい場合に、指定したプロパティが定義されていなくともエラーにはなりません。

"index.php"
// クラスを作成
class Human // 先頭は大文字にするのが慣習(小文字でもエラーにはならない)
{
    public $first_name;

    public $last_name;

    // ageが定義されていない

    // コンストラクタを定義してインスタンス化した際の初期化処理を記述する
    public function __construct($first_name, $last_name)
    {
        // 定義されていないプロパティに値を代入
        $this->age = 20;
        $this->first_name = $first_name;
        $this->last_name = $last_name;
    }

    // ~ 省略 ~
}

$human1 = new Human("名無しの", "ごん兵衛");
echo $human1->age . "\n";

しかし、このようなコードはどのようなデータがクラスに保持されるのか分かりづらく。理解しづらいコードになってしまう可能性があるので注意が必要です。

Visual Studio Code の拡張機能のPHP Intelephenseを入れていた場合は、上記のコードは構文エラーとして表示されます。

hello-world_04

hello-world_06

プロパティまとめ

  • プロパティはオブジェクトに紐付く変数のこと
  • 定義しただけだと初期値にnullが入る
  • nullを入れたくない場合は、空文字などで初期化する
  • $this->nameで、自分自身のプロパティにアクセス出来る
  • $this->name = $nameのようにプロパティに値を代入することが出来る

コンストラクタまとめ

  • インスタンス生成時に初期化したい処理などを記述する
  • インスタンス生成時に、そのプロパティに初期値を入れることでクラスという設計図から各々に違うインスタンスが生成することが出来る
  • $this自分自身を意味し、クラスから生成したインスタンス自身を指す
  • インスタンスの生成はnew 演算子を使用して生成する
  • インスタンス生成時に初期値を設定したい場合は、new Human("浦島", "太郎")などのように引数を設定する
  • コンストラクタのような特別なメソッドをマジックメソッドと呼ぶ
  • コンストラクター関数に引数を設定すると、インスタンス生成時にプロパティに初期値をセットすることができる
  • ただし、インスタンス生成時に引数に対応する値を渡してやらないとエラーになる
  • コンストラクタの引数にはデフォルト値を指定することが出来る
  • PHP8.0 から名前付き引数が使用でき、コンストラクタやメソッドの引数の順序を気にせず指定することができる

アクセス権とカプセル化

クラスを作成する際に指定できるアクセス権について紹介します。

アクセス修飾子

publicprivateアクセス修飾子と呼ばれ、クラス内のメソッドやプロパティに指定します。

  • アクセス権(アクセス修飾子)と呼ばれ、クラス内のメソッドやプロパティに付与して、外からアクセスできる範囲を制限できる
  • プロパティやメソッドには必ずつけなければならない
  • プロパティにつけない構文エラーになる
  • メソッドに付けないとpublic を付けたとみなされる

アクセス権の違い

PHP で指定できるアクセス権とその違いについて以下にまとめました。

アクセス権意味
privateそのクラスからしかアクセス出来ない
protectedそのクラスと、サブクラスからしかアクセスできない
サブクラスとは継承先のクラスのこと
publicどこからでもアクセス出来る

Humanクラスのコードをアクセス権を以下のように修正しました。

"index.php"
class Human
{
    public $first_name; // どこからでもアクセス出来る

    protected $last_name; // そのクラスと、サブクラスからアクセスできる

    private $age; // そのクラスからしかアクセス出来ない

    public function __construct($first_name = "名無しの", $last_name = "ごん兵衛", $age = 20)
    {
        $this->first_name = $first_name;
        $this->last_name = $last_name;
        $this->age = $age;
    }

    public function helloWorld()
    {
        echo "こんにちは世界 \n";
    }

    public function echoName()
    {
        echo "私の名前は、{$this->first_name} {$this->last_name} です。年齢は {$this->age} 歳です。", PHP_EOL;
    }
}

実際にコードを実行すると以下のような結果になります。

"index.php"
$human1 = new Human();

$human1->echoName(); // 動作します

$human1->first_name; // 動作します
$human1->last_name; // Fatal エラー
$human1->age; // Fatal エラー

PHP Intelephenseを入れている場合は既に構文エラーとして表示されています。

hello-world_07

カプセル化

public $first_name;と指定したプロパティは、インスタンス化後も値を変更する事ができました。

意図的にこのような設計にしている場合は問題ないですが、外部からプロパティにアクセスさせたくない場合は、private $first_name;と指定して外部からアクセスできないように設定し、誤って値が変更されないようにします。

このように誤って値が代入されて処理が進んでしまうことを防ぎ、クラス内のデータを隠蔽することをカプセル化と言います。

カプセル化の一例を見てみます。

"index.php"
class Human
{
    public $first_name;

    protected $last_name;

    private $age;

    public function __construct($first_name = "名無しの", $last_name = "ごん兵衛", $age = 20)
    {
        $this->first_name = $first_name;
        $this->last_name = $last_name;
        $this->age = $age;
    }

    public function helloWorld()
    {
        echo "こんにちは世界 \n";
    }

    public function echoName()
    {
        echo "私の名前は、{$this->first_name} {$this->last_name} です。年齢は {$this->age} 歳です。", PHP_EOL;
    }

    public function getAge()
    {
        return $this->age;
    }

    public function setAge($age)
    {

        // 渡ってきた値が数値かバリデーションを行う
        if(!is_int($age)) {
            throw new Exception("数値を指定してください。");
        }

        // 年齢に不正な値が代入されないようにバリデーション行う
        if($age < 0) {
            throw new Exception("年齢に負の値は指定できません。");
        }

        $this->age = $age;
    }
}

$human1 = new Human();

echo $human1->getAge() . "\n"; // 20
// $human1->setAge(-1); // Error 年齢に負の値は指定できません。
// $human1->setAge("aaaa"); // Error 数値を指定してください。
// $human1->setAge("30"); // Error 数値を指定してください。

$human1->setAge(30);
echo $human1->getAge(). "\n"; // 30

private $ageというプライベート変数はクラスの外部から直接アクセスは出来ませんでした。その代わりにプライベート変数にアクセスするための公開メソッドを用意しました。(getAgesetAge

setAgeメソッド内ではprivate $ageにセットされる値が不正なものでないか確認するための、各種バリデーションが行われるようにしました。

getAgesetAgeメソッドのようなプライベート変数に対して外部からアクセスしたり、操作したりするメソッドをゲッター(Getter)、セッター(Setter)と呼びます。

このような実装をすることで、Humanクラス内の内部の状態は常に適切に保たれ、不適切な値が設定されることはありません。これがカプセル化の利点です。

カプセル化の利点は以下の点が挙げられます。

  • クラスの内部データへの不適切なアクセスや修正を防ぐことができる
  • 特定のルールに基づいてデータを変更するメソッドを提供することで、データの整合性を保つことがでる
  • クラスの内部実装を変更しても、そのクラスを使用する他のコードに影響を与えることなく最適化することができる
  • 使用者にとってオブジェクトの内部動作を理解する必要がなく、提供される公開メソッドを使用するだけで良いため複雑なプログラムをより理解しやすく、管理しやすい形にすることができる

継承

オブジェクト指向には継承という仕組みがあります。継承を使用することで既に存在するクラスを元にさらに拡張したクラスを作る事ができます。

class クラス名 extends 親クラス名 {

    // 継承することで親クラスのプロパティやメソッドを引き継げる

}

Humanクラスから派生したクラスのManクラスを作成してみます。

"index.php"

// Humanクラスの内容は省略

class Man extends Human // Humanクラスを継承
{
    public function echoMan()
    {
        echo "私は男性です。". "\n";
    }

    public function test()
    {
        // Humanクラスのアクセス権がpublic
        echo $this->first_name; // アクセスできる

        // Humanクラスのアクセス権がprotected
        echo $this->last_name; // Manクラス内からならアクセスできる

        // Humanクラスのアクセス権がprivate
        // echo $this->age; // privateなのでアクセス出来ない // Error
    }
}

$man1 = new Man();
$man1->echoMan(); // 私は男性です。
$man1->setAge(30); // Humanクラスのメソッドが使用できる
$man1->echoName(); // 私の名前は、名無しの ごん兵衛 です。年齢は 30 歳です。

$man1->test();
$man1->first_name; // publicのプロパティはインスタンス化後も外部からアクセスできる
// $man1->last_name; // protectedは外部からアクセスできない // Error
// $man1->age; // privateは外部からアクセスできない // Error

実行したコードを見ても分かるように、Manクラスは年齢や名前などのプロパティを持っていません。

しかし、Humanクラスを継承していることで、元々Humanクラスで実装されていたプロパティ、各種メソッドを使用できている事がわかります。注意点としてアクセス権をprivateとしたプライベート変数は継承されていません。

クラスを継承する際の関係性として、継承元の親クラスをスーパークラス、継承先の子クラスをサブクラスと言います。実際にnewして使用するのはサブクラスになります。

注意点として 継承できるクラスは一つのみで、複数のクラスを継承するような多重継承することは出来ません。

しかし、後述しますが PHP ではトレイトと呼ばれる機能が用意されており、クラスのようなメソッドなどの振る舞いをまとめたコードを使うことで、継承しなくてもクラスのメンバーに追加できるようになります。

スーパークラスのコンストラクタを使用する場合

現状のManクラスだと、初期化時に何も値を指定していないのでHumanクラスのコンストラクタで指定されているデフォルト値である名無しの ごん兵衛や年齢の値である20が常に代入されます。

そこで、サブクラスからスーパークラスのコンストラクタを呼び出し任意の値を指定できるようにします。

"index.php"
class Man extends Human
{
    private $job;

    public function __construct($first_name, $last_name, $age, $job)
    {
        parent::__construct($first_name, $last_name, $age);

        $this->job = $job;
    }

    public function echoJob()
    {
        echo "私の職業は {$this->job} です。". "\n";
    }

    // ~ 省略 ~
}

$man1 = new Man("ジョニー", "デップ", 60, "俳優");
$man1->echoMan(); // 私は男性です。
$man1->echoName(); // 私の名前は、ジョニー デップ です。年齢は 60 歳です。
$man1->echoJob(); // 私の職業は 俳優 です。

スーパークラスのコンストラクタを使用する場合にはサブクラスのコンストラクタ内でparent::__construct()を記述します。

そして、スーパークラスで指定されている引数の値をサブクラスでも指定し、サブクラスで初期化したい値がある場合は引数に加えます。

VS Code の拡張機能PHP Debugを使用し、HumanクラスとManクラスのコンストラクタ内にブレークポイントを設置しデバッグしてみます。

extends-class

extends-class_01

extends-class.gif

スーパークラスのコンストラクタが呼び出され、その引数にサブクラスをインスタンス化時に指定した値が代入されていることが分かります。

詳細は割愛しますが、PHP Debug を使用してデバッグする場合はXdbugをインストールしておく必要があります。

オーバーライド

オーバーライドとは、サブクラスでスーパークラスから継承したメソッドの処理を上書きする(新たに定義する)機能のことです。

継承されたメソッドの振る舞いをサブクラスで変更することで、コードの柔軟性と再利用性を向上させることが出来ます。

HumanクラスのechoNameメソッドをオーバーライドすることにします。

オーバーライドはスーパークラスで実装されているメソッドと同じ名前のメソッドをサブクラスで実装します。

"index.php"
class Man extends Human
{
    private $job;

    public function __construct($first_name, $last_name, $age, $job)
    {
        parent::__construct($first_name, $last_name, $age);

        $this->job = $job;
    }

    // スーパークラスのメソッドを使用したい場合は parent::[スーパークラスのメソッド名] と指定する
    public function useParentEchoNam()
    {
        parent::echoName();
    }

    // スーパークラスのメソッドをオーバーライド
    public function echoName()
    {
        echo "私の名前は、{$this->first_name} {$this->last_name} で、職業は {$this->job} です。", PHP_EOL;
    }

    public function echoJob()
    {
        echo "私の職業は {$this->job} です。". "\n";
    }

    // ~ 省略 ~
}

$man1 = new Man("ジョニー", "デップ", 60, "俳優");
$man1->echoName(); // 私の名前は、ジョニー デップ で、職業は 俳優 です。
$man1->useParentEchoNam(); // 私の名前は、ジョニー デップ です。年齢は 60 歳です。

スーパークラスでは年齢を出力していましたが、サブクラスでは職業を出力するようにメソッドが上書きされたことが分かります。

また、parent::[スーパークラスのメソッド名]と指定することでスーパークラスのメソッドを呼び出すことが出来ます。

トレイト

トレイトとは、クラスを一つしか継承できないという成約を減らすための機能で、クラスのようにいくつかの機能をまとめて使用することが出来ます。

クラスに似ていますが単に機能をまとめているもので、トレイト自身のインスタンスを作成することは出来ません。

例として俳優のスキルと歌手のスキルの追加機能を提供するトレイトを作成します。

"index.php"
// 俳優のスキル
trait ActorSkill
{
    public function greatPerformance()
    {
        echo "私の演技は素晴らしいです。". "\n";
    }
}

// 歌手のスキル
trait SingerSkill
{
    public function doSing()
    {
        echo "私は素晴らしい歌声で歌います。". "\n";
    }
}

これをクラスで使用するには以下のように記述します。

"index.php"
class Man extends Human
{
    use ActorSkill, SingerSkill;

    // ~ 省略 ~
}

// もしくは

class Man extends Human
{
    use ActorSkill;
    use SingerSkill;

    // ~ 省略 ~
}

$man1 = new Man("ジョニー", "デップ", 60, "俳優");
$man1->echoName(); // 私の名前は、ジョニー デップ で、職業は 俳優 です。
$man1->useParentEchoNam(); // 私の名前は、ジョニー デップ です。年齢は 60 歳です。

$man1->greatPerformance(); // 私の演技は素晴らしいです。
$man1->doSing(); // 私は素晴らしい歌声で歌います。

クラスを継承しつつも、トレイトを使用することで追加機能を持ったサブクラスを作成することが出来ました。

トレイトのメソッド名とスーパークラスのメソッド名が同じだった場合は、トレイトで追加したメソッドが優先されます。その他、複数のトレイトを追加した場合の振る舞いなどは公式ドキュメントを参考に実装してみて下さい。

継承まとめ

  • 継承元の親クラスのことをスーパークラス、継承先の子クラスのことをサブクラスと言う
  • クラスを継承する場合はclass クラス名 extends 親クラス名と指定する
  • 継承することができるクラスは一つで、複数のクラスを多重継承することはできない
  • 継承することでスーパークラスのプロパティやメソッドを引き継ぎ、サブクラスでも使用できる
  • スーパークラスのメソッドやコンストラクタを、サブクラスでも使用したい場合はparent::[メソッド名]を指定する
  • トレイトという機能を使うことで、複数のクラスを継承しているかのような振る舞いを実装することが出来る
  • オーバーライドとは継承クラスにおいて、スーパークラスのメソッドを上書きする仕組みのこと

最後に

次回は、名前空間・抽象クラス・ンターフェース・ポリモーフィズムなどについてまとめていきます。