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

「OOP」という語は Object-Oriented Programming、つまりオブジェクト指向プログラミングを表し、コードを組み立てて整える方法のひとつです。OOP を使うと、プログラムを命令と関数の連なりではなく、互いにやり取りするオブジェクトの集まりとして見られます。

OOP での「オブジェクト」は、データと、そのデータを扱う関数を含むひとまとまりです。オブジェクトは「クラス」をもとに作られます。クラスはオブジェクトの設計図やひな型と考えられます。クラスができたら、その「インスタンス」を作れます。それがそのクラスから作られた具体的なオブジェクトです。

PHP で単純なクラスをどう作るか見てみましょう。クラスを定義するには「class」のキーワードを使い、そのあとにクラス名を書き、そして波かっこでクラスの関数(「メソッド」と呼びます)とクラスの変数(「プロパティ」や「属性」と呼びます)を囲みます。

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

この例では、honk というひとつの関数(つまり「メソッド」)を持つ Car というクラスを作りました。

それぞれのクラスは、主な仕事をひとつだけ受け持つべきです。クラスがあまりに多くのことをしているなら、小さく専門に分かれたクラスに分けるのがよいかもしれません。

クラスはふつう、コードを整理して見通しよく保つために別々のファイルに置きます。ファイル名はクラス名と合わせるので、Car クラスならファイル名は Car.php になります。

クラスに名前を付けるときは「PascalCase」の決まりに従うとよいでしょう。つまり名前のそれぞれの語が大文字で始まり、アンダースコアなどの区切りは使いません。メソッドとプロパティは「camelCase」の決まりに従い、小文字で始めます。

PHP のいくつかのメソッドは特別な役目を持ち、__(アンダースコア 2 つ)で始まります。もっとも大事な特別なメソッドのひとつが「コンストラクタ」で、__construct と書きます。コンストラクタは、クラスの新しいインスタンスを作るときに自動的に呼ばれるメソッドです。

コンストラクタはオブジェクトの最初の状態を決めるのによく使います。たとえば人を表すオブジェクトを作るとき、コンストラクタでその年齢や名前などの属性を設定できます。

PHP でコンストラクタをどう使うか見てみましょう。

class Person
{
	private $age;

	function __construct($age)
	{
		$this->age = $age;
	}

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

$person = new Person(25);
echo $person->howOldAreYou(); // 出力: 25

この例では、Person クラスがプロパティ(変数)$age と、それを設定するコンストラクタを持っています。そして howOldAreYou() メソッドがその人の年齢へのアクセスを与えます。

$this の疑似変数は、クラスの中でそのオブジェクトのプロパティとメソッドにアクセスするのに使います。

new のキーワードはクラスの新しいインスタンスを作るのに使います。上の例では 25 歳の新しい人を作りました。

コンストラクタのパラメータには、オブジェクトを作るときに指定されなかった場合の既定値も設定できます。たとえば次のようにです。

class Person
{
	private $age;

	function __construct($age = 20)
	{
		$this->age = $age;
	}

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

$person = new Person;  // 引数を渡さないなら、かっこは省けます
echo $person->howOldAreYou(); // 出力: 20

この例では、Person オブジェクトを作るときに年齢を指定しなければ、既定値の 20 が使われます。

うれしいことに、プロパティの定義とコンストラクタでのその初期化は、次のように短くまとめられます。

class Person
{
	function __construct(
		private $age = 20,
	) {
	}
}

念のために書いておくと、コンストラクタのほかにオブジェクトはデストラクタ(__destruct メソッド)を持てます。これはオブジェクトがメモリから解放される前に呼ばれます。

名前空間

名前空間は、関連するクラス、関数、定数を整理してまとめつつ、名前の衝突を避けられるようにします。コンピュータのフォルダのようなものと考えてください。それぞれのフォルダには、特定のプロジェクトや話題にまつわるファイルが入っています。

名前空間は、大きなプロジェクトや、クラス名の衝突が起こりうる第三者のライブラリを使うときにとりわけ役立ちます。

あなたのプロジェクトに Car というクラスがあり、それを Transport という名前空間に置きたいとしましょう。次のようにします。

namespace Transport;

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

別のファイルで Car クラスを使いたいなら、そのクラスがどの名前空間から来ているかを指定する必要があります。

$car = new Transport\Car;

簡単にするために、ファイルの先頭でどの名前空間のどのクラスを使うかを指定できます。そうすれば完全な道筋を書かずにインスタンスを作れます。

use Transport\Car;

$car = new Car;

継承

継承はオブジェクト指向プログラミングの道具で、既存のクラスをもとに新しいクラスを作り、そのプロパティとメソッドを受け継ぎ、必要に応じて広げたり定義し直したりできるようにします。継承はコードの再利用とクラスの階層を実現します。

かみ砕いて言えば、あるクラスがあって、そこから少し手を入れた別のクラスを作りたいなら、新しいクラスをもとのクラスから「継承」できます。

PHP では継承は extends のキーワードで行います。

私たちの Person クラスは年齢の情報を持ちます。もうひとつ Student というクラスを作り、Person を広げて専攻の情報を足せます。

例を見てみましょう。

class Person
{
	private $age;

	function __construct($age)
	{
		$this->age = $age;
	}

	function printInformation()
	{
		echo "Age: {$this->age} years\n";
	}
}

class Student extends Person
{
	private $fieldOfStudy;

	function __construct($age, $fieldOfStudy)
	{
		parent::__construct($age);
		$this->fieldOfStudy = $fieldOfStudy;
	}

	function printInformation()
	{
		parent::printInformation();
		echo "Field of study: {$this->fieldOfStudy} \n";
	}
}

$student = new Student(20, 'Computer Science');
$student->printInformation();

このコードはどう働くのでしょうか。

  • extends のキーワードで Person クラスを広げました。つまり Student クラスは Person のすべてのメソッドとプロパティを受け継ぎます。
  • parent:: のキーワードで親のクラスのメソッドを呼べます。ここでは、Student クラスに自分の機能を足す前に Person クラスのコンストラクタを呼びました。同じように、学生の情報を並べる前に祖先の printInformation() メソッドを呼んでいます。

継承は、クラスのあいだに「〜である」の関係があるときのためのものです。たとえば StudentPerson である。猫は動物である。おかげでコードの中であるオブジェクト(たとえば「Person」)を期待している場所で、そこから派生したオブジェクト(たとえば「Student」)を代わりに使えます。

継承の第一の目的はコードの重複を防ぐことではないと分かっておくことが大事です。むしろ継承を誤って使うと、込み入って手入れしにくいコードになります。クラスのあいだに「〜である」の関係がないなら、継承ではなく合成を考えるべきです。

PersonStudent クラスの printInformation() メソッドが、少し違う情報を出力していることに注目してください。そしてこのメソッドの別の実装を与えるクラス(たとえば Employee)を足せます。違うクラスのオブジェクトが同じメソッドに違うやり方で応えられることを、ポリモーフィズムと呼びます。

$people = [
	new Person(30),
	new Student(20, 'Computer Science'),
	new Employee(45, 'Director'),
];

foreach ($people as $person) {
	$person->printInformation();
}

合成

合成は、別のクラスからプロパティとメソッドを継承する代わりに、そのインスタンスを自分のクラスの中で使うだけの手法です。おかげで込み入った継承の構造を作らずに、複数のクラスの機能とプロパティを組み合わせられます。

たとえば Engine クラスと Car クラスがあるとします。「車はエンジンである」と言う代わりに「車はエンジンを持つ」と言います。これがまさに合成の関係です。

class Engine
{
	function start()
	{
		echo 'Engine is running.';
	}
}

class Car
{
	private $engine;

	function __construct()
	{
		$this->engine = new Engine;
	}

	function start()
	{
		$this->engine->start();
		echo 'The car is ready to drive!';
	}
}

$car = new Car;
$car->start();

ここでは CarEngine のすべてのプロパティとメソッドを持つわけではありませんが、$engine のプロパティを通してそこへ触れられます。

合成の利点は、設計がよりしなやかになり、これからの変更に合わせやすくなることです。

可視性

PHP では、クラスのプロパティ、メソッド、定数に「可視性」を定められます。可視性は、それらの要素にどこから触れられるかを決めます。

  1. Public: 要素が public と印を付けられていれば、クラスの外も含めてどこからでも触れられます。
  2. Protected: protected と印を付けられた要素には、そのクラスとそのすべての子孫(そこから継承したクラス)の中からだけ触れられます。
  3. Private: 要素が private なら、それが定義されたクラスの中からだけ触れられます。

可視性を指定しなければ、PHP は自動的に public にします。

見本のコードを見てみましょう。

class VisibilityExample
{
	public $publicProperty = 'Public';
	protected $protectedProperty = 'Protected';
	private $privateProperty = 'Private';

	public function printProperties()
	{
		echo $this->publicProperty;     // 動きます
		echo $this->protectedProperty;  // 動きます
		echo $this->privateProperty;    // 動きます
	}
}

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // 動きます
// echo $object->protectedProperty;   // エラーになります
// echo $object->privateProperty;     // エラーになります

クラスの継承の話を続けます。

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // 動きます
		echo $this->protectedProperty;  // 動きます
		// echo $this->privateProperty;   // エラーになります
	}
}

この場合、ChildClassprintProperties() メソッドは public と protected のプロパティには触れられますが、親のクラスの private のプロパティには触れられません。

データとメソッドはできる限り隠し、定められたインターフェースを通してだけ触れられるようにすべきです。そうすればクラスの内部の実装を、ほかのコードに影響を与えずに変えられます。

final のキーワード

PHP では、クラス、メソッド、定数が継承されたり上書きされたりするのを防ぎたいときに final のキーワードを使えます。クラスが final と印を付けられていれば、それを広げられません。メソッドが final と印を付けられていれば、下位のクラスで上書きできません。

あるクラスやメソッドがもう手を入れられないと分かっていれば、思わぬ衝突を心配せずに変更を加えられます。たとえば、子孫にすでに同じ名前のメソッドがあって衝突する心配なしに新しいメソッドを足せます。あるいはメソッドのパラメータを変えても、子孫の上書きされたメソッドと食い違う危険がありません。

final class FinalClass
{
}

// final のクラスからは継承できないので、次のコードはエラーになります。
class ChildOfFinalClass extends FinalClass
{
}

この例では、final のクラス FinalClass から継承しようとするとエラーになります。

静的なプロパティとメソッド

PHP でクラスの「静的な」要素と言うとき、それはクラスの特定のインスタンスではなく、クラスそのものに属するメソッドとプロパティを指します。つまりそれらに触れるのにクラスのインスタンスを作る必要はありません。代わりにクラス名を通して直接呼んだり触れたりします。

静的な要素はインスタンスではなくクラスに属するので、静的なメソッドの中では $this の疑似変数を使えないことを覚えておいてください。

静的なプロパティを使うと落とし穴だらけの分かりにくいコードになるので、決して使うべきではありません。ですからここでは例も示しません。一方、静的なメソッドは役に立ちます。例を挙げます。

class Calculator
{
	public static function add($a, $b)
	{
		return $a + $b;
	}

	public static function subtract($a, $b)
	{
		return $a - $b;
	}
}

// クラスのインスタンスを作らずに静的なメソッドを使います
echo Calculator::add(5, 3); // 出力: 8
echo Calculator::subtract(5, 3); // 出力: 2

この例では、2 つの静的なメソッドを持つ Calculator クラスを作りました。これらのメソッドは、:: の演算子を使ってクラスのインスタンスを作らずに直接呼べます。静的なメソッドは、特定のクラスのインスタンスの状態に左右されない処理にとりわけ役立ちます。

クラスの定数

クラスの中には定数も定義できます。定数はプログラムの実行中に決して変わらない値です。変数と違い、定数の値はそのままです。

class Car
{
	public const NumberOfWheels = 4;

	public function displayNumberOfWheels(): void
	{
		echo self::NumberOfWheels;
	}
}

echo Car::NumberOfWheels;  // 出力: 4

この例では、NumberOfWheels の定数を持つ Car クラスがあります。クラスの中で定数に触れるときは、クラス名の代わりに self のキーワードを使えます。

オブジェクトインターフェース

オブジェクトインターフェースはクラスの「約束事」として働きます。クラスがオブジェクトインターフェースを実装するなら、そのインターフェースが定めるすべてのメソッドを備えなければなりません。あるクラスたちが同じ「約束事」や構造を守ることを確かめる、優れたやり方です。

PHP ではインターフェースは interface のキーワードで定義します。インターフェースで定義されるメソッドはすべて公開(public)です。クラスがインターフェースを実装するときは implements のキーワードを使います。

interface Animal
{
	function makeSound();
}

class Cat implements Animal
{
	public function makeSound()
	{
		echo 'Meow';
	}
}

$cat = new Cat;
$cat->makeSound();

クラスがインターフェースを実装しているのに、期待されるメソッドがすべて定義されていなければ、PHP はエラーを投げます。

クラスは一度に複数のインターフェースを実装できます。ここが継承と違うところで、継承ではクラスはひとつのクラスからしか継承できません。

interface Guardian
{
	function guardHouse();
}

class Dog implements Animal, Guardian
{
	public function makeSound()
	{
		echo 'Bark';
	}

	public function guardHouse()
	{
		echo 'Dog diligently guards the house';
	}
}

抽象クラス

抽象クラスはほかのクラスのための土台のひな型として働きますが、そのインスタンスを直接作ることはできません。中身の定まったメソッドと、中身の定まっていない抽象メソッドが混じっています。抽象クラスから継承したクラスは、親のすべての抽象メソッドに定義を与えなければなりません。

抽象クラスを定義するには abstract のキーワードを使います。

abstract class AbstractClass
{
	public function regularMethod()
	{
		echo 'This is a regular method';
	}

	abstract public function abstractMethod();
}

class Child extends AbstractClass
{
	public function abstractMethod()
	{
		echo 'This is the implementation of the abstract method';
	}
}

$instance = new Child;
$instance->regularMethod();
$instance->abstractMethod();

この例には、ふつうのメソッドひとつと抽象メソッドひとつを持つ抽象クラスがあります。そして AbstractClass から継承し、抽象メソッドの実装を与える Child クラスがあります。

インターフェースと抽象クラスはどう違うのでしょうか。抽象クラスは抽象メソッドも具体的なメソッドも含められますが、インターフェースはクラスが実装しなければならないメソッドを定めるだけで、実装は与えません。クラスは抽象クラスからひとつしか継承できませんが、インターフェースはいくつでも実装できます。

型の検査

プログラミングでは、扱うデータが正しい型であることを確かめるのが欠かせません。PHP にはそれを保証する道具があります。データが正しい型であることを確かめることを「型の検査」と呼びます。

PHP で出会いうる型です。

  1. 基本の型: int(整数)、float(浮動小数点数)、bool(真偽値)、string(文字列)、array(配列)、null が含まれます。
  2. クラス: 値が特定のクラスのインスタンスであってほしい場合です。
  3. インターフェース: クラスが実装しなければならないメソッドの一そろいを定めます。インターフェースを満たす値はこれらのメソッドを持たなければなりません。
  4. 混ざった型: 変数が複数の許される型を持てると指定できます。
  5. Void: この特別な型は、関数やメソッドが値を返さないことを示します。

型を入れるようにコードを直すとどうなるか見てみましょう。

class Person
{
	private int $age;

	public function __construct(int $age)
	{
		$this->age = $age;
	}

	public function printAge(): void
	{
		echo "This person is {$this->age} years old.";
	}
}

/**
 * Person オブジェクトを受け取り、その人の年齢を出力する関数。
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

こうして、私たちのコードが正しい型のデータを期待して扱うようになり、思わぬ誤りを防ぐ助けになります。

型によっては PHP に直接書けないものもあります。その場合は phpDoc のコメントに書きます。これは PHP のコードを文書化する標準の形式で、/** で始まり */ で終わります。クラスやメソッドなどの説明を足せます。そしていわゆるアノテーション @var@param@return で込み入った型も書けます。これらの型は静的なコードの解析の道具が使いますが、PHP 自身は確かめません。

class Registry
{
	/** @var array<Person>  Person オブジェクトの配列であることを示します */
	private array $persons = [];

	public function addPerson(Person $person): void
	{
		$this->persons[] = $person;
	}
}

比較と同一性

PHP ではオブジェクトを 2 とおりに比べられます。

  1. 値の比較 ==: オブジェクトが同じクラスで、プロパティに同じ値を持つかを調べます。
  2. 同一性 ===: 同じオブジェクトのインスタンスかを調べます。
class Car
{
	public string $brand;

	public function __construct(string $brand)
	{
		$this->brand = $brand;
	}
}

$car1 = new Car('Skoda');
$car2 = new Car('Skoda');
$car3 = $car1;

var_dump($car1 == $car2);   // true。同じ値を持つからです
var_dump($car1 === $car2);  // false。同じインスタンスではないからです
var_dump($car1 === $car3);  // true。$car3 は $car1 と同じインスタンスだからです

instanceof 演算子

instanceof 演算子を使うと、あるオブジェクトが特定のクラスのインスタンスか、そのクラスの子孫か、あるいはあるインターフェースを実装しているかを判断できます。

Person クラスと、Person の子孫であるもうひとつのクラス Student があるとしましょう。

class Person
{
	private int $age;

	public function __construct(int $age)
	{
		$this->age = $age;
	}
}

class Student extends Person
{
	private string $major;

	public function __construct(int $age, string $major)
	{
		parent::__construct($age);
		$this->major = $major;
	}
}

$student = new Student(20, 'Computer Science');

// $student が Student クラスのインスタンスかを調べます
var_dump($student instanceof Student);  // 出力: bool(true)

// $student が Person クラスのインスタンスかを調べます(Student は Person の子孫だからです)
var_dump($student instanceof Person);   // 出力: bool(true)

出力から、$student のオブジェクトが StudentPerson の両方のクラスのインスタンスと見なされていることが分かります。

fluent インターフェース

「fluent インターフェース」は、メソッドをつなげてひとつの呼び出しにできる OOP の手法です。これはしばしばコードを単純にし、分かりやすくします。

fluent インターフェースの鍵になるのは、つながりの中のそれぞれのメソッドが今のオブジェクトへの参照を返すことです。これはメソッドの終わりで return $this; を使って実現します。この書き方は、オブジェクトのプロパティの値を設定する「セッター」と呼ばれるメソッドと結び付けられることが多いものです。

メールを送るための fluent インターフェースがどんなものになるか見てみましょう。

public function sendMessage()
{
	$email = new Email;
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Hello, this is a message.')
		  ->send();
}

この例では、setFrom()setRecipient()setMessage() のメソッドがそれぞれの値(送信者、受信者、メッセージの中身)を設定するのに使われています。これらの値を設定したあと、メソッドは今のオブジェクト($email)を返すので、そのうしろに別のメソッドをつなげられます。最後に send() メソッドを呼び、これが実際にメールを送ります。

fluent インターフェースのおかげで、直感的で読みやすいコードを書けます。

clone による複製

PHP では clone の演算子を使ってオブジェクトの複製を作れます。こうして中身の同じ新しいインスタンスが得られます。

オブジェクトを複製するときにいくつかのプロパティを変える必要があるなら、クラスに特別な __clone() メソッドを定義できます。このメソッドはオブジェクトが複製されるときに自動的に呼ばれます。

class Sheep
{
	public string $name;

	public function __construct(string $name)
	{
		$this->name = $name;
	}

	public function __clone()
	{
		$this->name = 'Clone of ' . $this->name;
	}
}

$original = new Sheep('Dolly');
echo $original->name . "\n";  // 出力: Dolly

$clone = clone $original;
echo $clone->name . "\n";     // 出力: Clone of Dolly

この例には、$name というひとつのプロパティを持つ Sheep クラプがあります。このクラスのインスタンスを複製すると、__clone() メソッドが複製された羊の名前に「Clone of」の接頭辞を付けます。

トレイト

PHP のトレイトは、クラスのあいだでメソッド、プロパティ、定数を共有し、コードの重複を防ぐ道具です。「コピーして貼り付ける」しくみ(Ctrl-C と Ctrl-V)と考えられます。トレイトの中身がクラスに「貼り付けられる」のです。おかげで、込み入ったクラスの階層を作らずにコードを使い回せます。

PHP でトレイトをどう使うか、単純な例を見てみましょう。

trait Honking
{
	public function honk()
	{
		echo 'Beep beep!';
	}
}

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

$car = new Car;
$car->honk(); // 'Beep beep!' を出力します

$truck = new Truck;
$truck->honk(); // やはり 'Beep beep!' を出力します

この例には、honk() というひとつのメソッドを含む Honking というトレイトがあります。そして CarTruck の 2 つのクラスがあり、どちらも Honking のトレイトを使っています。その結果、どちらのクラスも honk() メソッドを「持ち」、両方のクラスのオブジェクトでそれを呼べます。

トレイトを使えば、クラスのあいだで簡単に効率よくコードを共有できます。トレイトは継承の階層には入りません。つまり $car instanceof Honkingfalse を返します。

例外

OOP の例外は、コードの中のエラーや思いがけない場面を優雅に扱えるようにします。例外は、エラーや珍しい場面についての情報を運ぶオブジェクトです。

PHP には組み込みの Exception クラスがあり、すべての例外の土台になります。これにはいくつかのメソッドがあり、エラーのメッセージ、エラーが起きたファイルと行など、例外についてより詳しい情報を得られます。

コードでエラーが起きたら、throw のキーワードで例外を「投げ」られます。

function division(float $a, float $b): float
{
	if ($b == 0) {
		throw new Exception('Division by zero!');
	}
	return $a / $b;
}

division() 関数が第 2 引数としてゼロを受け取ると、'Division by zero!' というエラーのメッセージの例外を投げます。例外が投げられたときにプログラムが落ちないように、try/catch のブロックでそれを捕まえます。

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
}

例外を投げうるコードは try のブロックで包みます。例外が投げられると、コードの実行は catch のブロックへ移り、そこで例外を扱えます(たとえばエラーのメッセージを書き出します)。

trycatch のブロックのあとには、省略できる finally のブロックを足せます。これは例外が投げられたかどうかに関わらずいつも実行されます(trycatch のブロックで returnbreakcontinue を使った場合も同じです)。

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
} finally {
	// 例外が投げられたかどうかに関わらずいつも実行されるコード
}

Exception クラスから継承する自分の例外のクラス(の階層)も作れます。例として、預け入れと引き出しができる単純な銀行のアプリケーションを考えてみましょう。

class BankingException extends Exception {}
class InsufficientFundsException extends BankingException {}
class ExceededLimitException extends BankingException {}

class BankAccount
{
	private int $balance = 0;
	private int $dailyLimit = 1000;

	public function deposit(int $amount): int
	{
		$this->balance += $amount;
		return $this->balance;
	}

	public function withdraw(int $amount): int
	{
		if ($amount > $this->balance) {
			throw new InsufficientFundsException('Not enough funds in the account.');
		}

		if ($amount > $this->dailyLimit) {
			throw new ExceededLimitException('Daily withdrawal limit exceeded.');
		}

		$this->balance -= $amount;
		return $this->balance;
	}
}

違う種類の例外を見込んでいるなら、ひとつの try のブロックに複数の catch のブロックを書けます。

$account = new BankAccount;
$account->deposit(500);

try {
	$account->withdraw(1500);
} catch (ExceededLimitException $e) {
	echo $e->getMessage();
} catch (InsufficientFundsException $e) {
	echo $e->getMessage();
} catch (BankingException $e) {
	echo 'An error occurred during the operation.';
}

この例では catch のブロックの順序が大事です。すべての例外は BankingException から継承しているので、このブロックを最初に置いていたら、すべての例外がそこで捕まえられ、そのあとの catch のブロックまでコードは届きません。ですから、より具体的な例外(つまりほかから継承しているもの)を、その親の例外より catch の順序で上に置くことが大事です。

反復

PHP では、配列を回すのとよく似たやり方で、foreach のループを使ってオブジェクトを回せます。そのためには、そのオブジェクトが特別なインターフェースを実装していなければなりません。

ひとつめの選択肢は Iterator のインターフェースを実装することです。これには今の値を返す current()、キーを返す key()、次の値へ進む next()、先頭へ戻る rewind()、そしてもう終わりかを調べる valid() のメソッドがあります。

もうひとつの選択肢は IteratorAggregate のインターフェースを実装することです。これには getIterator() というメソッドがひとつだけあります。これは巡回を受け持つ代わりのオブジェクトを返すか、あるいはジェネレータでもかまいません。ジェネレータは yield を使ってキーと値を順に返す特別な関数です。

class Person
{
	public function __construct(
		public int $age,
	) {
	}
}

class Registry implements IteratorAggregate
{
	private array $people = [];

	public function addPerson(Person $person): void
	{
		$this->people[] = $person;
	}

	public function getIterator(): Generator
	{
		foreach ($this->people as $person) {
			yield $person;
		}
	}
}

$list = new Registry;
$list->addPerson(new Person(30));
$list->addPerson(new Person(25));

foreach ($list as $person) {
	echo "Age: {$person->age} years\n";
}

ベストプラクティス

オブジェクト指向プログラミングの基本の考え方を身につけたら、OOP のベストプラクティスに目を向けることが欠かせません。それは、ただ動くだけでなく、読みやすく、分かりやすく、手入れしやすいコードを書く助けになります。

  1. 関心の分離: それぞれのクラスは、はっきり定められた責任を持ち、主な仕事をひとつだけ受け持つべきです。クラスがあまりに多くのことをしているなら、小さく専門に分かれたクラスに分けるのがよいかもしれません。
  2. カプセル化: データとメソッドはできる限り隠し、定められたインターフェースを通してだけ触れられるようにすべきです。そうすればクラスの内部の実装を、ほかのコードに影響を与えずに変えられます。
  3. Dependency Injection: 依存関係をクラスの中で直接作る代わりに、外から「注入」すべきです。この考え方をより深く理解するには、Dependency Injection の章をおすすめします。
バージョン: 4.x