Menu
Coddy logo textTech

Полиморфизм через интерфейсы

Часть раздела Объектно-ориентированное программирование путешествия по 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. Эти классы не имеют общего родительского класса — они совершенно не связаны друг с другом, за исключением реализации одного и того же интерфейса. В этом заключается ключевое преимущество перед полиморфизмом на основе наследования: вы можете группировать несвязанные классы по их возможностям, а не по их происхождению.

Ключевой момент: Полиморфизм на основе интерфейсов позволяет взаимозаменяемо использовать несвязанные классы на основе общего поведения, делая ваш код более гибким и менее зависимым от конкретных реализаций.

challenge icon

Задание

Легко

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

Вы организуете свой код в четырех файлах:

  • 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() для получения данных из файлового хранилища, затем выведите результат
?>
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование