От автора: В объектно-ориентированном программировании полиморфизм – это мощный и фундаментальный инструмент. Его можно использовать в приложении для создания более гармоничного потока. Это учебное пособие опишет основную концепцию полиморфизма и то, каким образом можно легко развернуть его в PHP.
Перед тем, как мы приступим к практическим шагам, я Вам рекомендую скачать исходники.
Что такое полиморфизм?
Полиморфизм – длинное название для очень простой идеи.
Полиморфизм в объектно-ориентированном программировании описывает паттерн проектирования, в котором классы, имеющие различную функциональность, совместно используют общий интерфейс.
Прелесть полиморфизма в том, что коду, работающему с различными классами, не нужно знать, какой класс он использует, так как все они используются одинаково.
Аналогия полиморфизма в «реале» — это кнопка. Все знают, как использовать кнопку: просто нажать на нее. Что «делает» кнопка, однако, зависит от того, с чем она связана и контекста, в котором она используется — но результат не влияет на то, как ее используют. Если ваш шеф говорит вам нажать кнопку, вы уже обладаете всей необходимой информацией для выполнения этого задания.
В программировании полиморфизм используется для того, чтобы сделать приложения более модульными и расширяемыми. Вместо беспорядочных условных предложений, описывающих различные направления действия, вы создаете взаимозаменяемые объекты, которые подбираете согласно своим нуждам. Это основная задача полиморфизма.
Интерфейсы
Общий интерфейс – неотъемлемая часть полиморфизма. Определить интерфейс в PHP можно двумя способами: interfaces и abstract classes. Оба имеют свою область применения, и их можно смешивать и сочетать для подгонки в свою иерархию классов так, как вы сочтете нужным.
Интерфейс
Интерфейс – то же самое, что и класс, за исключением того, что он не может содержать код. Интерфейс может определять названия методов и аргументов, но не содержание методов. Любые классы, реализующие интерфейс, должны реализовывать все методы, определяемые интерфейсом. Класс может реализовать множественные интерфейсы.
Интерфейс объявляется ключевым словом ‘interface‘:
1 2 3 |
interface MyInterface { // methods } |
и присоединяется к классу, используя ключевое слово ‘implements‘ (множественные интерфейсы могут реализовываться путем перечисления и отделения их запятыми):
1 2 3 |
class MyClass implements MyInterface { // methods } |
Методы в интерфейсе можно определять так же, как в классе, за исключением того, что они не могут иметь содержимого (части между фигурными скобками):
1 2 3 4 5 |
interface MyInterface { public function doThis(); public function doThat(); public function setName($name); } |
Все здесь определенные методы потребуется включить во все реализуемые классы точно так, как описано (прочтите внизу комментарии к коду):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//ПРАВИЛЬНО class MyClass implements MyInterface { protected $name; public function doThis() { // код, который выполняет this } public function doThat() { // код, который выполняет that } public function setName($name) { $this->name = $name; } } // НЕПРАВИЛЬНО class MyClass implements MyInterface { // не хватает doThis()! private function doThat() { // эта должна быть public! } public function setName() { // не хватает названия аргумента! } } |
Абстрактный класс
Абстрактный класс – это смесь интерфейса и класса. Он может определять как функциональность, так и интерфейс (в виде абстрактных методов). Классы, расширяющие абстрактный класс должны реализовывать все абстрактные методы, определенные в абстрактном классе.
Абстрактный класс объявляется таким же образом, как классы с добавлением ключевого слова ‘abstract‘:
1 2 3 |
abstract class MyAbstract { // methods } |
и назначается классу, используя ключевое слово ‘extends‘:
1 2 3 |
class MyClass extends MyAbstract { // class methods } |
Обычные методы могут определяться в абстрактном классе так же как в обычном классе, вместе с любыми абстрактными методами (использующими ключевое слово ‘abstract‘). Абстрактные методы ведут себя подобно методам, определяемым в интерфейсе, и должны аналогично образом реализовываться в дочерних классах.
1 2 3 4 5 6 7 8 |
abstract class MyAbstract { public $name; public function doThis() { // do this } abstract public function doThat(); abstract public function setName($name); } |
Шаг 1: Определите задачу
Давайте представим, что у вас есть класс Article, ответственный за управление статьями в вашем веб-сайте. Он содержит информацию о статье, включая название, автора, дату и категорию. Вот как это выглядит:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class poly_base_Article { public $title; public $author; public $date; public $category; public function __construct($title, $author, $date, $category = 0) { $this->title = $title; $this->author = $author; $this->date = $date; $this->category = $category; } } |
Примечание: Примеры классов в этом учебном пособии используют соглашение об именах «package_component_Class». Это обычный способ разделять классы на виртуальные пространства имен во избежание конфликтов.
Теперь вам нужно добавить способ вывода информации в различные форматы, такие как XML и JSON. Вы можете соблазниться и сделать что-то вроде этого:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class poly_base_Article { //... public function write($type) { $ret = ''; switch($type) { case 'XML': $ret = '<article>'; $ret .= '<title>' . $obj->title . '</title>'; $ret .= '<author>' . $obj->author . '</author>'; $ret .= '<date>' . $obj->date . '</date>'; $ret .= '<category>' . $obj->category . '</category>'; $ret .= '</article>'; break; case 'JSON': $array = array('article' => $obj); $ret = json_encode($array); break; } return $ret; } } |
Это довольно неуклюжее решение, но оно работает — пока. Спросите себя, однако, что произойдет в будущем, когда нам нужно будет еще добавить форматы? Можно продолжить изменять класс, добавляя все больше условий, но так вы только его усложняете.
Одно важное правило OOP (объектно-ориентированном программировании) состоит в том, что класс должен выполнять что-то одно, и делать это хорошо.
Поэтому условные предложения должны стать красными флажками, означающими, что ваш класс пытается сделать слишком много разных вещей. Вот тут и вступает в дело полиморфизм.
Из нашего примера видно, что налицо имеются две задачи: управление статьями и форматирование их данных. В этом учебнике мы сделаем рефакторинг кода форматирования в новый набор классов и обнаружим, как легко использовать полиморфизм.
Шаг 2: Определите свой интерфейс
Первое, что нужно сделать – это определить интерфейс. Важно хорошенько обдумать свой интерфейс, потому что любые изменения в нем могут потребовать изменений в вызывающем коде. В нашем примере для определения своего единственного метода мы используем простой интерфейс:
1 2 3 |
interface poly_writer_Writer { public function write(poly_base_Article $obj); } |
Это просто; мы определили общедоступный метод write(), который принимает объект Article как аргумент. Любые классы, реализующие интерфейс Writer, определенно будут иметь этот метод.
Подсказка: если хотите ограничить тип аргументов, которые могут передаваться вашим функциям и методам, можете использовать хинты типов, как мы это делали в методе write(), он принимает только объекты типа poly_base_Article. К сожалению, в текущих версиях PHP не поддерживается хинты для типов возвращаемых значений, так что вам придется самим позаботиться о возвращаемых значениях.
Шаг 3: Создайте свою реализацию
Когда определен интерфейс, пора создавать классы, непосредственно выполняющие работу. В примере у нас имеются два формата, которые нужно вывести. Так, у нас есть два класса Writer: XMLWriter и JSONWriter. Это они должны извлекать данные из переданного объекта Article и форматировать информацию.
Вот как выглядит XMLWriter:
1 2 3 4 5 6 7 8 9 10 11 |
class poly_writer_XMLWriter implements poly_writer_Writer { public function write(poly_base_Article $obj) { $ret = '<article>'; $ret .= '<title>' . $obj->title . '</title>'; $ret .= '<author>' . $obj->author . '</author>'; $ret .= '<date>' . $obj->date . '</date>'; $ret .= '<category>' . $obj->category . '</category>'; $ret .= '</article>'; return $ret; } } |
Как можно видеть из объявления класса, мы пользуемся ключевым словом implements для реализации своего интерфейса. Метод write()содержит специальную функциональность для форматирования XML.
А вот наш класс JSONWriter:
1 2 3 4 5 6 |
class poly_writer_JSONWriter implements poly_writer_Writer { public function write(poly_base_Article $obj) { $array = array('article' => $obj); return json_encode($array); } } |
Теперь весь наш специальный код для каждого формата содержится внутри индивидуальных классов. Каждый из этих классов отвечает за оперирование отдельным форматом, и ничего более. Благодаря нашему интерфейсу никакая другая часть нашего приложения не должна заботиться об их работе для того, чтобы ими пользоваться.
Шаг 4: Используйте свою реализацию
Когда классы определены, пора снова заняться классом Article. Весь код, пребывавший в исходном методе write(), был рефакторизован в новый набор классов. Все, что нужно сделать нашим методам, это использовать новые классы, как здесь:
1 2 3 4 5 6 |
class poly_base_Article { //... public function write(poly_writer_Writer $writer) { return $writer->write($this); } } |
Весь этот метод теперь принимает объект класса Writer (то есть любого класса, реализующего интерфейс Writer), вызывает его метод write(), передавая себя ($this) как аргумент, затем отправляет возвращаемое значение прямиком в код клиента. Ему незачем больше волноваться о деталях форматирования данных, и он может сосредоточиться на своей основной задаче.
Получение Writer
Вы можете поинтересоваться, где для начала взять объект Writer, который нужно передать этому методу. Выбирать вам, а методик существует множество. Например, вы могли бы использовать класс factory для получения требуемых данных и создать объект:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class poly_base_Factory { public static function getWriter() { // grab request variable $format = $_REQUEST['format']; // construct our class name and check its existence $class = 'poly_writer_' . $format . 'Writer'; if(class_exists($class)) { // return a new Writer object return new $class(); } // otherwise we fail throw new Exception('Unsupported format'); } } |
Как я уже сказал, в зависимости от ваших требований существует много других способов применения. В этом примере переменная запроса выбирает, какой формат использовать. Она конструирует название класса из переменной запроса, проверяет, существует ли он, затем возвращает новый объект Writer. Если под этим именем ничего не существует, генерируется исключение, чтобы позволить коду клиента решить, что делать.
Шаг 5: Соберите все вместе
Когда все находится на своем месте, код клиента соберет все вот так:
1 2 3 4 5 6 7 8 9 10 |
$article = new poly_base_Article('Polymorphism', 'Steve', time(), 0); try { $writer = poly_base_Factory::getWriter(); } catch (Exception $e) { $writer = new poly_writer_XMLWriter(); } echo $article->write($writer); |
Сначала мы создали пример объекта Article, с которым будем работать. Затем пытаемся получить объект Writer из Factory, прибегнув, если генерируется исключение, к XMLWriter по умолчанию. Наконец, передаем объект Writer своему методу Article — write() и выводим результат.
Заключение
В этом учебном пособии я снабдил вас вводным курсом полиморфизма и объяснением интерфейсов в PHP. Надеюсь, вы понимаете, что я показал вам всего лишь один случай потенциального использования полиморфизма. Существует еще много-много приложений. Полиморфизм – элегантный способ избежать уродливых условных предложений в коде OOP. Он следует принципу сохранения компонентов по отдельности и является неотъемлемой частью многих дизайнерских моделей. Если у вас возникли вопросы, не стесняйтесь задать их в комментах!
Автор: Steve Guidetti
Редакция: Рог Виктор и Андрей Бернацкий. Команда webformyself.
E-mail: contact@webformyself.com
Проект webformyself.com — Как создать свой сайт. Основы самостоятельного сайтостроения
"Киберсант-вебмастер" — самый полный курс по сайтостроению в рунете!
P.S. Хотите опубликовать интересный тематический материал и заработать? Если ответ «Да», то жмите сюда.
Комментарии (5)