Основы ООП. Полиморфизм

Краткое содержание лекции

Теоретические задания | Практические задания

В отличие от двух предыдущих лекций, я не буду заставлять вас самостоятельно искать определение полиморфизма. При подготовке к лекции я убедился, что поиск "полиморфизм php" занятие гибельное. Поэтому прочтите лучше статьи о "полиморфизм java". Мне понравились следующие: Полиморфизм в Java, Объектно-ориентированное программирование. Полиморфизм.

Но главная суть полиморфизма: "Один интерфейс, множество реализаций". Давайте рассмотрим примеры на php.

Начнем с самого простого. Как я говорил раньше, с php5 появился стандартный интерфейс Countable. Он состоит из 1 метода count(). Этот интерфейс могут имплементировать совершенно разные классы: от объектов коллекций до обычных моделей.

(Collection.php) download
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php
namespace Fightmaster;

use Countable;

/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
class Collection implements Countable
{
    //code
    private $items = array();

    /**
     * @param mixed $item
     */
    public function add($item)
    {
        if (!$this->has()) {
            $this->items[] = $item;
        }
    }

    /**
     * @param mixed $item
     */
    public function remove($item)
    {
        if ($this->has()) {
            //code
        }
    }

    /**
     * @param boolean $item
     */
    public function has($item)
    {
        //code
    }

    /**
     * @return integer
     */
    public function count()
    {
        return count($this->items);
    }
}
(News.php) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
namespace Fightmaster;

use Countable;

/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
class News implements Countable
{
    //code

    /**
     * @return integer
     */
    public function count()
    {
        return count($this->comments);
    }
}

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

Чаще всего, вы встретите следующее ренешение.

(index.php) download
1
2
3
4
5
6
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
$service = new Service();
$service->doSomething();
(Service.php) download
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
26
27
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
class Service
{
    public function doSomething()
    {
        //code
        $this->quickSort();
        //$this->shellSort();

        //code
    }

    private function quickSort()
    {
        //code
    }

    private function shellSort()
    {
        //code
    }

    //code
}

Но у этого решения проблема с динамическим выбором алгоритма. А также, алгоритмов сортировки много, а сервис у нас отвечает не только за сортировку. Несовсем корректно копить столько кода в классе. Привлекательнее вариант с тем, чтобы разнести алгоритмы по классам.

(QuickSort.php) download
1
2
3
4
5
6
7
8
9
10
11
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
class QuickSort
{
    public function quickSort()
    {
        //code
    }
}
(ShellSort.php) download
1
2
3
4
5
6
7
8
9
10
11
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
class ShellSort
{
    public function shellSort()
    {
        //code
    }
}
(Service.php) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
class Service
{
    public function doSomething()
    {
        //code        
        $quickSort = new QuickSort();
        $quickSort->quickSort();
        //$shellSort = new ShellSort();
        //$shellSort->shellSort();

        //code
    }

    //code
}
(index.php) download
1
2
3
4
5
6
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
$service = new Service();
$service->doSomething();

При таком исполнении не очень понятно, а что стало лучше. Бросается в глаза проблема с наименованием метода в классе сортировок. Разумно их заменить на просто count.

(QuickSort.php) download
1
2
3
4
5
6
7
8
9
10
11
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
class QuickSort
{
    public function sort()
    {
        //code
    }
}
(ShellSort.php) download
1
2
3
4
5
6
7
8
9
10
11
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
class ShellSort
{
    public function sort()
    {
        //code
    }
}
(Service.php) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
class Service
{
    public function doSomething()
    {
        //code        
        $quickSort = new QuickSort();
        $quickSort->sort();
        //$shellSort = new ShellSort();
        //$shellSort->sort();

        //code
    }

    //code
}
(index.php) download
1
2
3
4
5
6
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
$service = new Service();
$service->doSomething();

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

(SortInterface.php) download
1
2
3
4
5
<?php
interface SortInterface
{
    public function sort();
}
(QuickSort.php) download
1
2
3
4
5
6
7
8
9
10
11
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
class QuickSort implements SortInterface
{
    public function sort()
    {
        //code
    }
}
(ShellSort.php) download
1
2
3
4
5
6
7
8
9
10
11
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
class ShellSort implements SortInterface
{
    public function sort()
    {
        //code
    }
}

Теперь осталась одна проблема, рассмотрение которой не совсем входит в текущий курс. Данный код тяжело будет протестировать. Так как объект сортировке создается непосредственно в методе doSomething.

(Service.php) download
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
26
27
28
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
class Service
{
    /**
     * @var SortInterface
     */
    private $sortAlgorithm;

    /**
     * @param SortInterface $sortAlgorithm
     */
    public function __construct(SortInterface $sortAlgorithm)
    {
        $this->sortAlgorithm = $sortAlgorithm;
    }

    public function doSomething()
    {
        //code        
        $this->sortAlgorithm->sort();
        //code
    }

    //code
}
(index.php) download
1
2
3
4
5
6
7
8
<?php
/**
 * Dmitry Petrov <dmitry.petrov@opensoftdev.ru>
 */
$sortAlgorithm = new QuickSort();
//$sortAlgorithm = new ShellSort();
$service = new Service($sortAlgorithm);
$service->doSomething();

Дальше мы не будем производить рефакторинг.

Отмечу еще раз факт, во втором примере мы не создаем еще одну реализацию интерфейса. Мы в процессе рефакторинга приходим к тому, что у нас существуют как минимум две разные реализации одного и того же, которые хочется стандартизировать. Стоит обратить внимание на конструктор класса Service. Он принимает не конкретную реализацию, и сам класс работает теперь не с чем-то конкретным. Наш Service теперь лишь знает о интерфейсе SortInterface и его интересует лишь только это. Он сможет работать с любым классом, который заключил контракт и обязался выполнять его условия, а значит Service сможет воспользоваться любым количеством реализаций.

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

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