PHP: ООП
20 питань · ~20 хв · Версія v3.0
Увійдіть, щоб продовжити
Класи й видимість, успадкування й інтерфейси, трейти, магічні методи, readonly і хуки властивостей, пізнє статичне зв'язування - питання всіх рівнів, від junior до lead.
- За спробу
- 20
- У пулі
- 59
- Проходжень
- 0
- Середній бал
- -
- Пройшли на 70%+
- -
Питання для підготовки
17 питаньІнтерфейс - це лише контракт: перелік публічних методів, які клас зобов'язується мати. Реалізації в ньому немає (крім констант), і клас може реалізувати скільки завгодно інтерфейсів.
Абстрактний клас - це недобудований клас: у ньому можуть бути властивості, готові методи й абстрактні методи, які мають дописати нащадки. Успадкувати можна лише один клас.
interface PaymentGateway
{
public function charge(Money $amount): Receipt;
}
abstract class HttpGateway implements PaymentGateway
{
public function __construct(protected HttpClient $http) {}
protected function post(string $path, array $data): array
{
return $this->http->post($this->baseUrl() . $path, $data);
}
abstract protected function baseUrl(): string;
}
Коли що:
- Інтерфейс - коли потрібна взаємозамінність: код залежить від
PaymentGateway, а конкретний шлюз підставляється ззовні. - Абстрактний клас - коли кілька реалізацій ділять спільний код і стан.
Часто вони йдуть разом, як у прикладі: залежать від інтерфейсу, а абстрактний клас лише прибирає повтор між реалізаціями.
Це модифікатори видимості - хто може звертатися до властивості чи методу:
public- будь-хто, зокрема код поза класом.protected- сам клас і його нащадки.private- лише сам клас, де член оголошено. Нащадок його не бачить.
class Account
{
public function __construct(private int $balance = 0) {}
public function deposit(int $amount): void
{
$this->guard($amount);
$this->balance += $amount;
}
protected function guard(int $amount): void
{
if ($amount <= 0) {
throw new InvalidArgumentException('Сума має бути додатною');
}
}
}
Тут баланс не можна змінити напряму ззовні - лише через deposit(), який перевіряє суму. У цьому й сенс інкапсуляції: клас сам стежить за тим, щоб його стан лишався коректним.
Правило за замовчуванням: усе private, доки не з'явиться причина відкрити більше. Відкриту властивість потім важко закрити - на неї вже покладається чужий код.
У PHP 8.4 з'явилася асиметрична видимість: public private(set) int $balance - читати можна звідусіль, а змінювати лише всередині класу.
Трейт - це шматок коду (методи, властивості), який підставляється в клас так, ніби його написали там. Механізм повторного використання без успадкування: клас може взяти кілька трейтів.
trait HasSlug
{
public function slug(): string
{
return Str::slug($this->title);
}
}
class Post
{
use HasSlug;
}
Підводні камені:
- Неявні залежності. Трейт вище звертається до
$this->title, але ніде цього не оголошує. Клас безtitleзламається лише під час виконання. Частково рятує абстрактний метод у трейті:abstract public function title(): string;. - Конфлікти імен. Два трейти з однаковим методом - фатальна помилка, доки не розв'язати її через
insteadofіas. - Трейт - не тип. Не можна написати
function f(HasSlug $x): перевірити, що об'єкт має поведінку трейту, можна лише через інтерфейс. - Прихована складність. Клас із п'ятьма трейтами важко читати: щоб зрозуміти, що в ньому є, треба відкрити шість файлів.
Добре працюють трейти для вузької допоміжної поведінки, як SoftDeletes чи HasFactory у Laravel. Погано - як спосіб розкидати велику модель по файлах: складність лишається, вона просто гірше видна.
self:: завжди вказує на клас, у якому написано код. static:: - на клас, через який метод викликали насправді. Друге називають пізнім статичним зв'язуванням.
class Model
{
public static function create(): static
{
return new static(); // клас виклику
}
public static function make(): self
{
return new self(); // завжди Model
}
}
class User extends Model {}
User::create(); // User
User::make(); // Model
Де це важливо:
- Фабричні методи в базовому класі. Eloquent
User::create()повертає самеUser, бо всерединіnew static. - Перевизначені константи й методи.
static::TABLEвізьме константу нащадка,self::TABLE- базового класу. - Тип повернення
static(PHP 8.0) каже аналізатору й IDE, що метод повертає клас виклику - зручно для fluent-інтерфейсів.
Коли self: коли поведінка не має змінюватися в нащадках, наприклад виклик приватного методу. static:: до приватного методу нащадка не дістанеться.
Успадкування - найсильніший зв'язок між класами: нащадок залежить від усіх деталей предка, зокрема захищених. Будь-яка зміна в базовому класі може тихо зламати десяток нащадків - це називають проблемою крихкого базового класу.
Типові симптоми:
- Ієрархія росте вглиб:
Report→PdfReport→ColoredPdfReport→ ... Нова комбінація ознак - новий клас. - Нащадок перевизначає метод, щоб «вимкнути» поведінку предка, яка йому не потрібна.
- Базовий клас обростає
protected-хелперами, бо так зручніше нащадкам.
Композиція - об'єкт отримує інші об'єкти і делегує їм роботу:
final class ReportExporter
{
public function __construct(
private Formatter $formatter, // PDF, CSV, HTML
private Storage $storage, // диск, S3
) {}
public function export(Report $report): string
{
return $this->storage->put($this->formatter->format($report));
}
}
Тепер формат і сховище комбінуються вільно, кожне тестується окремо, і змінити одне можна, не чіпаючи іншого.
Успадкування доречне, коли є справжнє «є різновидом» і спільний контракт стабільний: власні винятки від RuntimeException, моделі від Model. Корисна звичка - робити класи final за замовчуванням: тоді успадкування стає свідомим рішенням, а не випадковістю.
Спершу почитати
Прочитати - ще не значить знати
20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.