Введение в объектно-ориентированное программирование

Термин “ООП” означает объектно-ориентированное программирование – способ организации и структурирования кода. ООП позволяет рассматривать программу не как последовательность команд и функций, а как набор объектов, взаимодействующих друг с другом.

В ООП “объект” – это единица, содержащая данные и функции, которые оперируют этими данными. Объекты создаются на основе “классов”, которые можно понимать как чертежи или шаблоны объектов. Имея класс, мы можем создать его “экземпляр”, то есть конкретный объект, созданный на основе этого класса.

Рассмотрим, как можно создать простой класс в PHP. При определении класса используется ключевое слово “class”, за которым следует имя класса, а затем фигурные скобки, в которые заключаются функции класса (называемые “методами”) и переменные класса (называемые “свойствами” или “атрибутами”):

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

В данном примере мы создали класс Car с одной функцией (или “методом”) honk.

Каждый класс должен решать только одну основную задачу. Если класс решает слишком много задач, то целесообразно разделить его на более мелкие, специализированные классы.

Классы обычно хранятся в отдельных файлах, что позволяет упорядочить код и облегчить навигацию по нему. Имя файла должно совпадать с именем класса, поэтому для класса Car имя файла будет Car.php.

При именовании классов следует придерживаться правила “PascalCase”, то есть каждое слово в имени начинается с заглавной буквы, без подчеркиваний и других разделителей. Методы и свойства следует называть в “camelCase”, то есть со строчной буквы.

Некоторые методы в PHP играют особую роль и имеют префикс __ (два знака подчеркивания). Одним из наиболее важных специальных методов является “конструктор”, обозначаемый как __construct. Конструктор – это метод, который автоматически вызывается при создании нового экземпляра класса.

Мы часто используем конструктор для установки начального состояния объекта. Например, при создании объекта, представляющего человека, с помощью конструктора можно задать его возраст, имя или другие атрибуты.

Рассмотрим, как использовать конструктор в PHP:

class Person
{
	private $age;

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

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

$person = new Person(25);
echo $person->howOldAreYou(); // Outputs: 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;  // if no argument is passed, parentheses can be omitted
echo $person->howOldAreYou(); // Outputs: 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:: позволяет нам вызывать методы из родительского класса. В данном случае мы вызвали конструктор из класса Person, прежде чем добавить собственную функциональность в класс Student. И аналогично, метод предка printInformation() перед выводом информации о студентах.

Наследование предназначено для ситуаций, когда между классами существует связь “есть”. Например, Student – это Person. Кошка – это животное. Это позволяет нам в случаях, когда в коде ожидается один объект (например, “Person”), использовать вместо него производный объект (например, “Student”).

Важно понимать, что основная цель наследования не заключается в предотвращении дублирования кода. Напротив, неправильное использование наследования может привести к появлению сложного и трудноразрешимого кода. Если между классами нет отношения “является”, то вместо наследования следует рассмотреть композицию.

Обратите внимание, что методы printInformation() в классах Person и Student выводят немного другую информацию. И мы можем добавить другие классы (например, 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();

Здесь Car не имеет всех свойств и методов Engine, но имеет доступ к ним через свойство $engine.

Преимуществом композиции является большая гибкость проектирования и лучшая адаптируемость к будущим изменениям.

Наглядность

В PHP можно определить “видимость” для свойств, методов и констант класса. Видимость определяет, где можно получить доступ к этим элементам.

  1. Public: Если элемент помечен как public, это означает, что вы можете получить к нему доступ из любого места, даже за пределами класса.
  2. Защищенный: Элемент, помеченный как protected, доступен только в пределах данного класса и всех его потомков (классов, наследующих от него).
  3. Private: Если элемент помечен как private, то доступ к нему возможен только из класса, в котором он был определен.

Если не указать значение видимости, PHP автоматически установит его равным public.

Рассмотрим пример кода:

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

	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		echo $this->privateProperty;    // Works
	}
}

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Works
// echo $object->protectedProperty;   // Throws an error
// echo $object->privateProperty;     // Throws an error

Продолжаем наследование классов:

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		// echo $this->privateProperty;   // Throws an error
	}
}

В данном случае метод printProperties() в ChildClass может получить доступ к публичным и защищенным свойствам, но не может получить доступ к приватным свойствам родительского класса.

Данные и методы должны быть максимально скрыты и доступны только через определенный интерфейс. Это позволяет изменять внутреннюю реализацию класса, не затрагивая остальной части кода.

Ключевое слово final

В PHP мы можем использовать ключевое слово final, если хотим запретить наследование или переопределение класса, метода или константы. Если класс помечен как final, он не может быть расширен. Если метод помечен как final, он не может быть переопределен в подклассе.

Знание того, что определенный класс или метод больше не будет модифицироваться, позволяет нам легче вносить изменения, не беспокоясь о возможных конфликтах. Например, мы можем добавить новый метод, не опасаясь, что у потомка уже есть метод с таким же именем, что приведет к коллизии. Или мы можем изменить параметры метода, опять же не опасаясь, что это приведет к несоответствию с переопределенным методом потомка.

final class FinalClass
{
}

// The following code will throw an error because we cannot inherit from a final class.
class ChildOfFinalClass extends FinalClass
{
}

В данном примере попытка наследоваться от конечного класса FinalClass приведет к ошибке.

Статические свойства и методы

Когда мы говорим о “статических” элементах класса в PHP, мы имеем в виду методы и свойства, которые принадлежат самому классу, а не его конкретному экземпляру. Это означает, что для доступа к ним не нужно создавать экземпляр класса. Вместо этого вы вызываете или обращаетесь к ним непосредственно через имя класса.

Следует помнить, что поскольку статические элементы принадлежат классу, а не его экземплярам, то внутри статических методов нельзя использовать псевдопеременную $this.

Использование статических свойств приводит к обфусцированному коду, полному подводных камней, поэтому их никогда не следует использовать, и мы не будем приводить здесь пример. С другой стороны, статические методы полезны. Вот пример:

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

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

// Using the static method without creating an instance of the class
echo Calculator::add(5, 3); // Output: 8
echo Calculator::subtract(5, 3); // Output: 2

В этом примере мы создали класс Calculator с двумя статическими методами. Мы можем вызывать эти методы напрямую, не создавая экземпляр класса, используя оператор ::. Статические методы особенно полезны для операций, которые не зависят от состояния конкретного экземпляра класса.

Константы класса

Внутри классов мы имеем возможность определять константы. Константы – это значения, которые никогда не изменяются в процессе выполнения программы. В отличие от переменных, значение константы остается неизменным.

class Car
{
	public const NumberOfWheels = 4;

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

echo Car::NumberOfWheels;  // Output: 4

В данном примере мы имеем класс Car с константой NumberOfWheels. При обращении к константе внутри класса мы можем использовать ключевое слово 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();

В данном примере мы имеем абстрактный класс с одним обычным и одним абстрактным методом. Затем у нас есть класс Child, который наследуется от AbstractClass и предоставляет реализацию абстрактного метода.

Чем отличаются интерфейсы и абстрактные классы? Абстрактные классы могут содержать как абстрактные, так и конкретные методы, в то время как интерфейсы только определяют, какие методы должен реализовать класс, но не предоставляют никакой реализации. Класс может наследоваться только от одного абстрактного класса, но может реализовать любое количество интерфейсов.

Проверка типа

В программировании очень важно убедиться в том, что данные, с которыми мы работаем, имеют правильный тип. В 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.";
	}
}

/**
 * A function that accepts a Person object and prints the person's age.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

Таким образом, мы гарантируем, что наш код ожидает и работает с данными правильного типа, что помогает нам предотвратить возможные ошибки.

Некоторые типы не могут быть написаны непосредственно в PHP. В этом случае они перечисляются в комментариях phpDoc – это стандартный формат документирования PHP-кода, начинающийся с /** и заканчивающийся */. Он позволяет добавлять описания классов, методов и так далее. А также перечислять сложные типы с помощью так называемых аннотаций @var, @param и @return. Эти типы затем используются инструментами статического анализа кода, но не проверяются самим PHP.

class Registry
{
	/** @var array<Person>  indicates that it's an array of Person objects */
	private array $persons = [];

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

Сравнение и идентичность

В PHP можно сравнивать объекты двумя способами:

  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, because they have the same value
var_dump($car1 === $car2);  // false, because they are not the same instance
var_dump($car1 === $car3);  // true, because $car3 is the same instance as $car1

Оператор instanceof

Оператор instanceof позволяет определить, является ли данный объект экземпляром определенного класса, потомком этого класса или реализует ли он определенный интерфейс.

Представьте, что у нас есть класс Person и другой класс Student, который является потомком Person:

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');

// Check if $student is an instance of the Student class
var_dump($student instanceof Student);  // Output: bool(true)

// Check if $student is an instance of the Person class (because Student is a descendant of Person)
var_dump($student instanceof Person);   // Output: bool(true)

Из выводов видно, что объект $student считается экземпляром обоих классов Student и Person.

Текучие интерфейсы

Fluent Interface" – это техника в ООП, позволяющая объединять методы в цепочку одним вызовом. Это часто упрощает и уточняет код.

Ключевым элементом флюентного интерфейса является то, что каждый метод в цепочке возвращает ссылку на текущий объект. Это достигается за счет использования return $this; в конце метода. Такой стиль программирования часто ассоциируется с методами, называемыми “сеттерами”, которые устанавливают значения свойств объекта.

Давайте посмотрим, как может выглядеть свободный интерфейс для отправки электронной почты:

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(), который, собственно, и отправляет письмо.

Благодаря свободным интерфейсам мы можем писать интуитивно понятный и легко читаемый код.

Копирование с помощью 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";  // Outputs: Dolly

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

В данном примере мы имеем класс Sheep с одним свойством $name. Когда мы клонируем экземпляр этого класса, метод __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(); // Outputs 'Beep beep!'

$truck = new Truck;
$truck->honk(); // Also outputs 'Beep beep!'

В этом примере у нас есть тред Honking, содержащий один метод honk(). Затем у нас есть два класса: Car и Truck, оба из которых используют признак Honking. В результате оба класса “имеют” метод honk(), и мы можем вызывать его на объектах обоих классов.

Трейты позволяют легко и эффективно обмениваться кодом между классами. Они не входят в иерархию наследования, т.е. $car instanceof Honking вернет false.

Исключения

Исключения в ООП позволяют нам изящно обрабатывать ошибки и неожиданные ситуации в нашем коде. Они представляют собой объекты, которые несут информацию об ошибке или необычной ситуации.

В PHP есть встроенный класс Exception, который служит основой для всех исключений. Он имеет несколько методов, которые позволяют получить дополнительную информацию об исключении, например, сообщение об ошибке, файл и строку, в которой произошла ошибка, и т.д.

Когда в коде возникает ошибка, мы можем “выбросить” исключение, используя ключевое слово throw.

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

Когда функция division() получает null в качестве второго аргумента, она выбрасывает исключение с сообщением об ошибке 'Division by zero!'. Чтобы предотвратить аварийное завершение программы при возникновении исключения, мы перехватываем его в блоке try/catch:

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

Код, который может выбросить исключение, оборачивается в блок try. Если исключение выброшено, выполнение кода переходит в блок catch, где мы можем обработать исключение (например, написать сообщение об ошибке).

После блоков try и catch можно добавить необязательный блок finally, который выполняется всегда, независимо от того, было ли выброшено исключение или нет (даже если мы используем return, break или continue в блоке try или catch ):

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
} finally {
	// Code that is always executed whether the exception has been thrown or not
}

Мы также можем создавать собственные классы исключений (иерархию), которые наследуются от класса 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";
}

Лучшие практики

После того как вы освоили основные принципы объектно-ориентированного программирования, необходимо обратить внимание на лучшие практики ООП. Это поможет вам писать не только функциональный, но и читабельный, понятный и легко сопровождаемый код.

  1. Разделение обязанностей: Каждый класс должен иметь четко определенную ответственность и решать только одну основную задачу. Если класс выполняет слишком много задач, то целесообразно разделить его на более мелкие специализированные классы.
  2. Инкапсуляция: Данные и методы должны быть максимально скрыты и доступны только через определенный интерфейс. Это позволяет изменять внутреннюю реализацию класса, не затрагивая остальной код.
  3. Инъекция зависимостей: Вместо того чтобы создавать зависимости непосредственно внутри класса, следует “инжектировать” их извне. Для более глубокого понимания этого принципа мы рекомендуем главы, посвященные Dependency Injection.
версия: 4.0