Статьи

Понимание и применение полиморфизма в PHP

В объектно-ориентированном программировании полиморфизм является мощным и фундаментальным инструментом. Его можно использовать для создания более органичного потока в вашем приложении. Этот урок опишет общую концепцию полиморфизма и как его можно легко развернуть в PHP.


Полиморфизм — это длинное слово для очень простого понятия.

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

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

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

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


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

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

Интерфейс объявляется с использованием ключевого слова 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);
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// VALID
class MyClass implements MyInterface {
    protected $name;
    public function doThis() {
        // code that does this
    }
    public function doThat() {
        // code that does that
    }
    public function setName($name) {
        $this->name = $name;
    }
}
 
// INVALID
class MyClass implements MyInterface {
    // missing doThis()!
 
    private function doThat() {
        // this should be public!
    }
    public function setName() {
        // missing the name argument!
    }
}

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

Абстрактный класс объявляется так же, как классы с добавлением ключевого слова 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);
}

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

01
02
03
04
05
06
07
08
09
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. У вас может возникнуть соблазн сделать что-то вроде этого:

01
02
03
04
05
06
07
08
09
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>’ .
                $ret .= ‘<author>’ .
                $ret .= ‘<date>’ .
                $ret .= ‘<category>’ .
                $ret .= ‘</article>’;
                break;
            case ‘JSON’:
                $array = array(‘article’ => $obj);
                $ret = json_encode($array);
                break;
        }
        return $ret;
    }
}

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

Один важный принцип ООП состоит в том, что класс должен делать одну вещь, и он должен делать это хорошо.

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

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


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

1
2
3
interface poly_writer_Writer {
    public function write(poly_base_Article $obj);
}

Это так просто; мы определили публичный метод write() который принимает объект Article в качестве аргумента. Любые классы, реализующие интерфейс Writer, обязательно будут иметь этот метод.

Совет: Если вы хотите ограничить тип аргументов, которые могут быть переданы вашим функциям и методам, вы можете использовать подсказки типа, как мы это делали в методе write() ; он принимает только объекты типа poly_base_Article . К сожалению, подсказка возвращаемого типа не поддерживается в текущих версиях PHP, поэтому вы должны позаботиться о возвращаемых значениях.


С вашим определенным интерфейсом пришло время создавать классы, которые действительно делают вещи. В нашем примере у нас есть два формата, которые мы хотим вывести. Таким образом, у нас есть два класса Writer: XMLWriter и JSONWriter. Это зависит от них, чтобы извлечь данные из переданного объекта Article и отформатировать информацию.

Вот как выглядит XMLWriter:

01
02
03
04
05
06
07
08
09
10
11
class poly_writer_XMLWriter implements poly_writer_Writer {
    public function write(poly_base_Article $obj) {
        $ret = ‘<article>’;
        $ret .= ‘<title>’ .
        $ret .= ‘<author>’ .
        $ret .= ‘<date>’ .
        $ret .= ‘<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);
    }
}

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


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

01
02
03
04
05
06
07
08
09
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_’ .
        if(class_exists($class)) {
            // return a new Writer object
            return new $class();
        }
        // otherwise we fail
        throw new Exception(‘Unsupported format’);
    }
}

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


Со всем на месте, вот как наш клиентский код соединил бы все это:

01
02
03
04
05
06
07
08
09
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 в метод write() нашей Статьи, печатая результат.


В этом уроке я представил вам введение в полиморфизм и объяснение интерфейсов в PHP. Я надеюсь, вы понимаете, что я показал вам только один потенциальный вариант использования полиморфизма. Есть много, много других приложений. Полиморфизм — это элегантный способ избежать уродливых условных операторов в вашем ООП-коде. Он следует принципу разделения компонентов и является неотъемлемой частью многих шаблонов проектирования. Если у вас есть какие-либо вопросы, не стесняйтесь спрашивать в комментариях!