Полиморфизм через интерфейсы
Часть раздела Объектно-ориентированное программирование путешествия по PHP на Coddy — урок 34 из 91.
В предыдущем уроке вы познакомились с полиморфизмом через наследование, где дочерние классы переопределяют методы родительских классов. Интерфейсы предоставляют еще один мощный способ достижения полиморфизма, особенно при работе с несвязанными классами, которые имеют общее поведение.
С интерфейсами полиморфизм проистекает из самого контракта. Любой класс, реализующий интерфейс, гарантирует наличие определенных методов, независимо от его иерархии наследования:
<?php
interface Notifiable {
public function send(string $message);
}
class EmailNotifier implements Notifiable {
public function send(string $message) {
return "Email: " . $message;
}
}
class SMSNotifier implements Notifiable {
public function send(string $message) {
return "SMS: " . $message;
}
}
class PushNotifier implements Notifiable {
public function send(string $message) {
return "Push: " . $message;
}
}
function notify(Notifiable $notifier, string $message) {
return $notifier->send($message);
}
echo notify(new EmailNotifier(), "Hello") . "\n";
echo notify(new SMSNotifier(), "Hello") . "\n";
echo notify(new PushNotifier(), "Hello");
Вывод:
Email: Hello
SMS: Hello
Push: HelloФункция notify() работает с любым объектом Notifiable. Эти классы не имеют общего родительского класса — они совершенно не связаны друг с другом, за исключением реализации одного и того же интерфейса. В этом заключается ключевое преимущество перед полиморфизмом на основе наследования: вы можете группировать несвязанные классы по их возможностям, а не по их происхождению.
Ключевой момент: Полиморфизм на основе интерфейсов позволяет взаимозаменяемо использовать несвязанные классы на основе общего поведения, делая ваш код более гибким и менее зависимым от конкретных реализаций.
Задание
ЛегкоДавайте создадим систему хранения, которая демонстрирует полиморфизм на основе интерфейсов. Вы создадите различные бэкенды хранения — файловую систему и базу данных — у которых нет ничего общего, кроме того, что они оба реализуют один и тот же интерфейс хранения. Одна функция будет работать с любым типом хранилища, демонстрируя, как интерфейсы обеспечивают полиморфизм между совершенно не связанными классами.
Вы организуете свой код в четырех файлах:
Storable.php— Определите интерфейсStorableс двумя сигнатурами методов:save($key, $data)иretrieve($key). Этот контракт гарантирует, что любая система хранения может сохранять и извлекать данные, независимо от того, как это реализовано внутри.FileStorage.php— Создайте классFileStorage, который реализуетStorable. Подключите файл интерфейса. Класс должен иметь приватное свойство$directory, устанавливаемое через конструктор. Реализуйтеsave($key, $data)так, чтобы он возвращал"Saving '[data]' to file [directory]/[key].txt". Реализуйтеretrieve($key)так, чтобы он возвращал"Reading from file [directory]/[key].txt".DatabaseStorage.php— Создайте классDatabaseStorage, который также реализуетStorable. Подключите файл интерфейса. Класс должен иметь приватное свойство$tableName, устанавливаемое через конструктор. Реализуйтеsave($key, $data)так, чтобы он возвращал"Inserting '[data]' into table [tableName] with key [key]". Реализуйтеretrieve($key)так, чтобы он возвращал"Selecting from table [tableName] where key = [key]".main.php— Подключите оба файла хранилищ. Создайте функцию с именемstoreData, которая принимает параметр типаStorable, ключ и данные. Функция должна возвращать результат вызоваsave()с ключом и данными. Создайте еще одну функцию с именемfetchData, которая принимает параметр типаStorableи ключ, возвращая результат вызоваretrieve().
Вы получите четыре входных значения: путь к директории, имя таблицы, ключ и данные для хранения. Создайте объект FileStorage с директорией и DatabaseStorage с именем таблицы. Используйте функцию storeData(), чтобы сохранить данные сначала с помощью файлового хранилища, а затем с помощью хранилища базы данных. Выведите каждый результат на новой строке. Затем используйте fetchData() для извлечения данных из файлового хранилища и выведите этот результат.
Обратите внимание, как storeData() и fetchData() работают одинаково с обоими типами хранилищ — им важно только то, что объект реализует Storable. Файловая система и база данных имеют совершенно разные внутренние реализации, но они взаимозаменяемы благодаря контракту интерфейса.
Шпаргалка
Интерфейсы обеспечивают полиморфизм, позволяя использовать несвязанные классы взаимозаменяемо на основе общего поведения, а не иерархии наследования.
Любой класс, реализующий интерфейс, гарантирует наличие определенных методов, создавая контракт, который обеспечивает полиморфное поведение:
<?php
interface Notifiable {
public function send(string $message);
}
class EmailNotifier implements Notifiable {
public function send(string $message) {
return "Email: " . $message;
}
}
class SMSNotifier implements Notifiable {
public function send(string $message) {
return "SMS: " . $message;
}
}
function notify(Notifiable $notifier, string $message) {
return $notifier->send($message);
}
echo notify(new EmailNotifier(), "Hello");
echo notify(new SMSNotifier(), "Hello");Функция notify() принимает любой объект, реализующий Notifiable, независимо от иерархии классов. Это делает код более гибким и независимым от конкретных реализаций.
Ключевое преимущество: Полиморфизм на основе интерфейсов группирует несвязанные классы по их возможностям, а не по их происхождению, позволяя использовать совершенно разные реализации взаимозаменяемо через общий контракт.
Попробуйте сами
<?php
require_once 'FileStorage.php';
require_once 'DatabaseStorage.php';
// Чтение входных данных
$directory = trim(fgets(STDIN));
$tableName = trim(fgets(STDIN));
$key = trim(fgets(STDIN));
$data = trim(fgets(STDIN));
// TODO: Создайте функцию storeData, которая принимает объект Storable, ключ и данные
// Она должна возвращать результат вызова save() с ключом и данными
// TODO: Создайте функцию fetchData, которая принимает объект Storable и ключ
// Она должна возвращать результат вызова retrieve() с ключом
// TODO: Создайте экземпляр FileStorage с указанной директорией
// TODO: Создайте экземпляр DatabaseStorage с указанным именем таблицы
// TODO: Используйте storeData() для сохранения данных в файловом хранилище, затем выведите результат
// TODO: Используйте storeData() для сохранения данных в хранилище базы данных, затем выведите результат
// TODO: Используйте fetchData() для получения данных из файлового хранилища, затем выведите результат
?>В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыВведение в ООПКлассы и объектыКлючевое слово $thisМетодыСвойстваКонструктор (__construct)Деструктор (__destruct)Итоги — Простой калькулятор4Наследование
Основы наследованияКлючевое слово parent::Переопределение методовКлючевое слово finalАбстрактные классыИтоги — Иерархия сотрудников7Инкапсуляция
Public, Protected, PrivateПодробный разбор модификаторов доступаГеттеры и сеттерыСокрытие данныхConstructor Promotion (8.0)Итоги — Система учета студентов10Продвинутые концепции ООП
Композиция против наследованияВнедрение зависимостейАнонимные классыПеречисления (PHP 8.1)Файберы (PHP 8.1)Глубокое погружение в клонирование объектовГенераторы и итераторы13Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикПаттерн Репозиторий2Пространства имен и автозагрузка
Введение в пространства именКлючевое слово useСтандарт автозагрузки PSR-4Автозагрузчик ComposerИтоги: структурированный проект5Интерфейсы и контракты
Введение в интерфейсыРеализация интерфейсовРеализация нескольких интерфейсовИнтерфейс против абстрактного классаType Hinting с интерфейсамиИтоги — Калькулятор фигур8Магические методы
Введение в магические методы__toString и __debugInfo__get, __set, __isset, __unset__call и __callStatic__clone и клонирование объектов__serialize и __unserializeИтоги — Кастомная коллекция11Система типов и обработка ошибок
Объявление типовNullable-типыТипы объединения и пересеченияКлассы исключенийИерархия пользовательских исключенийTry, Catch, FinallyИтоги — Валидатор форм14Проект: Управление библиотекой
Обзор проектаКлассы Book и User3Свойства классов
Свойства экземпляра и статические свойстваКонстанты в классахСтатические методы и свойстваПриватные и защищенные свойстваСвойства Readonly (PHP 8.1)Итоги — Менеджер банковского счета6Полиморфизм
Снова о переопределении методовПолиморфизм через интерфейсыType Hinting и Union TypesПозднее статическое связываниеИтоги — Payment Processor9Трейты
Введение в трейтыИспользование нескольких трейтовРазрешение конфликтов трейтовАбстрактные методы в трейтахТрейты и наследование12Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн ObserverПаттерн Strategy