Рейтинг@Mail.ru
 

Полиморфизм

Опять что то мутно-греческое.
Академическим примером разъяснения этого явления стали всякие действия с квадратиками и кружочками. Мол сначала мы придумываем действия (подвинуть, изменить размер), а потом уже определяем что мы будем двигать и увеличивать. Вроде бы просто, но почему то мозг отказывается принимать это за чистую монету. Мол как же можно придумать что то бесформенно-абстрактное, а потом из этого получить толк.

На самом деле это не совсем так. Если подходить с этой стороны, то может получиться, что мы попытаемся заставить плавать топор.

Что бы проще было понять, что это и как с ним бороться, попробуем подойти с другой стороны. Обычно так и делается, просто при объяснениях начинают сверху вниз, а при проектировании как раз наоборот.

Обожаю художников-абстракционистов и тихо им завидую. Посмотрите, какая прелесть:

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

Но к чему я это? А к тому, что понятие "программирование" вступает в прямую конфронтацию с понятием "абстракция". А именно этим термином чаще всего описывают то, что получается в результате применения полиморфизма.

Попробуйте проанализировать эту картину. Если даже что то и выйдет, то результат у каждого будет свой. Кто-то представит, что это амёбы, кто-то, что мыльные пузыри, кто-то, что праздник в желудке.
Именно потому, что на самом деле это ничего конкретного не обозначает. Это абстракция, полное отвлечение от реальности.

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

Допустим нам нужен тот самый праздник в желудке.

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

Потом определяем меню:
а) Суп-пюре из авокадо с трюфелями и патиссоном.
б) Котлета по киевски из молодого страуса
в) Марцепан с орешками на золотом подносе
г) Топор (хотя кулеш из него в сказке сварили как-то)
д) Компот.

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

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

Еда, как общее понятие, и есть тот абстрактный класс, с которого начинается обильное слюнотечение.

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

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

Иначе мы такого насочиняем, что Босх нервно закурит в сторонке.

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

Соответственно мы видим, что еда, это то, что можно приготовить и скушать. Значит базовый класс Food() (не путать с foo) должен содержать методы prepare_food() ("готовить") и eat_food()("есть"). А так же конечный метод укладывания пищи в живот - fast_food();
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
<?php

/* 
* class Food 
* @base 
*/ 
class Food 

     
    public 
$eat 'еду'
    public 
$way
         
    public function 
__construct($way
    { 
        
$this->way $way
        
$this->prepare_food(); 
        
$this->eat_food();         
    }     
     
    public function 
prepare_food() 
    { 
        
$this->eat $this->way $this->eat
    }         

    public function 
eat_food() 
    { 
        
$this->eat 'кушать '$this->eat;     
    } 
     
    public function 
fast_food() 
    { 
       return 
$this->eat
    } 



$stomach = new Food('варёную '); 
echo 
$stomach -> fast_food();


Но так, как нас не устраивает абстрактная еда, определяем, что будет на ужин (см меню). Для этого нужен еще один метод, который и определит необходимые продукты:
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
<?php

/** 
*class Food 
* @base 
*/ 
class Food 

    public 
$way;
 
    public function 
__construct($way
    { 
        
$this->way $way
        
$this->prepare_food(); 
        
$this->eat_food(); 
    } 

    public function 
prepare_food() 
    { 
        
$this->eat $this->way $this->set_products(); 
    } 

    public function 
eat_food() 
    { 
        
$this->eat 'кушать '$this->eat; } 
/* 
* Новый метод с продуктами 
*/ 
    
protected function set_products() 
    { 
        return 
'трюфеля, патиссон'
    } 

    public function 
fast_food() 
    { 
        return 
$this->eat
    } 


    
$stomach = new Food('варёные '); 
    echo 
$stomach -> fast_food();


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

Можно, что бы не переписывать этот класс, сделать ему наследников. Там и определять что и как готовить.
Но если мы сейчас сделаем классы - потомки, в которые перенесем метод выбора продуктов, то как же тогда мы сможем вызвать его из базового класса?

А вот и он (аплодисменты) - Полиморфизм. Дело в том, что в базовом классе можно просто объявить о намерениях - мол тут мы будем решать, что на ужин. Тогда базовый класс спокойно увидит то, что творится в потомках. Это называется виртуальная функция. Делается так (просто пустой метод):
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php

/*  
* class Food  
* @base  
*/ 
class Food 

     
    public 
$way
         
    public function 
__construct($way
    { 
        
$this->way $way
        
$this->prepare_food(); 
        
$this->eat_food();         
    }     
     
    public function 
prepare_food() 
    { 
   
// Здесь вызываем метод, которого еще толком нет       
        
$this->eat $this->way $this->set_products(); 
    }         

    public function 
eat_food() 
    { 
        
$this->eat 'кушать '$this->eat;     
    } 
    
// Пустой виртуальный метод
    
protected function set_products() 
    { 
        
// Тут будем выбирать продукты   
    
}  
     


/* 
* class Soup 
* @extends Food 
*/ 
class Soup extends Food 

     
    public function 
__construct() 
    { 
       
parent::__construct('варёные ');      
    } 
 
// А это уже конкретный метод, который и переопределит базовый         
    
public function set_products() 
    { 
        return 
'трюфеля, патиссон'
    } 
     
    public  function 
fast_food()  
    {  
       return 
$this->eat;  
    }     



$soup = new Soup(); 
echo 
$soup -> fast_food();


Теперь это не просто наследование. Теперь у нас связи глубже - мы можем из потомка влиять на базовый класс. Это называется переопределение методов. То есть пустой метод в базовом классе легким движением руки превращается в метод, определенный в потомке. Главное, чтобы у них были одинаковые имена. Теперь можно устроить пир горой:
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?php

/*  
* class Food  
* @base  
*/ 
class Food 

     
    public 
$way
         
    public function 
__construct($way
    { 
        
$this->way $way
        
$this->prepare_food(); 
        
$this->eat_food();         
    }     
     
    public function 
prepare_food() 
    {
    
// Здесь вызываем метод, которого еще толком нет         
        
$this->eat $this->way $this->set_products(); 
    }         

    public function 
eat_food() 
    { 
        
$this->eat 'кушать '$this->eat;     
    } 
    
// Пустой виртуальный метод 
    
protected function set_products() 
    { 
        
// Тут будем выбирать продукты   
    
}  
     


/* 
* class Soup 
* @extends Food 
*/ 
class Soup extends Food 

     
    public function 
__construct() 
    { 
       
parent::__construct('варёные ');      
    } 
 
// А это уже конкретный метод, который и переопределит базовый         
    
public function set_products() 
    { 
        return 
'трюфеля, патиссон'
    } 
     
    public  function 
fast_food()  
    {  
       return 
$this->eat;  
    }     



$soup = new Soup(); 
echo 
$soup -> fast_food(), '<br>'

/* 
* class Beefsteak 
* @extends Food 
*/ 
class Beefsteak extends Food 

     
    public function 
__construct() 
    { 
       
parent::__construct('жареное ');      
    } 
 
// А это уже конкретный метод, который и переопределит базовый         
    
public function set_products() 
    { 
        return 
'мясо на косточке'
    } 
     
    public  function 
fast_food()  
    {  
       return 
$this->eat;  
    }     



$soup = new Beefsteak(); 
echo 
$soup -> fast_food(), '<br>'

/* 
* class Compote 
* @extends Food 
*/ 
class Compote extends Food 

     
    public function 
__construct() 
    { 
       
parent::__construct('холодные ');      
    } 
 
// А это уже конкретный метод, который и переопределит базовый         
    
public function set_products() 
    { 
        return 
'моченые яблоки'
    } 
     
    public  function 
fast_food()  
    {  
       return 
$this->eat;  
    }     



$soup = new Compote(); 
echo 
$soup -> fast_food();

Особо нужно отметить метод set_products() в базовом классе. В нем нет конкретики, по этому он называется абстрактным. Его нельзя вызвать, можно только переопределить. Было бы странным, если придя в магазин, мы попросили полкило еды.

Сам класс, который содержит хотя бы один абстрактный метод, тоже является абстрактным. Объект этого класса нельзя создать, он нужен только для того, что бы с ним работали наследники. Вообще абстрактный класс и абстрактные методы нужно объявлять ключевым словом abstract . А методы, которые не должны наследоваться - ключевым словом final

Вот наш главный класс в идеале должен выглядеть так:
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
<?php

/*   
* class Food   
* @base   
*/  
abstract class Food  
{  
      
    private 
$way;  
          
    public function 
__construct($way)  
    {  
        
$this->way $way;  
        
$this->prepare_food();  
        
$this->eat_food();          
    }      
      
    private final function 
prepare_food()  
    {         
        
$this->eat $this->way $this->set_products();  
    }          

    private final function 
eat_food()  
    {  
        
$this->eat 'кушать '$this->eat;      
    }  
  
    abstract protected function 
set_products(); 
      
}


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

Всё это здорово, но опять ложка дегтя.
Наследование, само по себе, и так не отличается прозрачностью, а полиморфизм - вообще ребус. Бывают (и часто, в основном фреймворки) такие приложения, которые начинаются с абстрактного класса Super_site, и пошло-поехало. Весь сайт - один огромный организм, базирующийся на главном, родительском классе. Если функционал серьёзный и наследований много, сложность обслуживания взрастает в разы при, казалось бы, упрощении программы. Сокращение объема кода не всегда ведет к упрощению алгоритмов.

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

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