Marian Böhmer
Návrhové vzory v PHP
Computer Press Brno 2012
K1886.indd 1
18.1.2012 8:24:33
Návrhové vzory v PHP Marian Böhmer Překlad: Lukáš Krejčí Obálka: Martin Sodomka Odpovědný redaktor: Martin Herodek Technický redaktor: Jiří Matoušek Objednávky knih: http://knihy.cpress.cz www.albatrosmedia.cz
[email protected] bezplatná linka 800 555 513 ISBN 978-80-251-3338-5 Vydalo nakladatelství Computer Press v Brně roku 2012 ve společnosti Albatros Media a. s. se sídlem Na Pankráci 30, Praha 4. Číslo publikace 15 935. © Albatros Media a. s. Všechna práva vyhrazena. Žádná část této publikace nesmí být kopírována a rozmnožována za účelem rozšiřování v jakékoli formě či jakýmkoli způsobem bez písemného souhlasu vydavatele. 1. vydání
K1886.indd 2
18.1.2012 8:25:15
Obsah
Obsah
ÚVODEM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .11 Struktura knihy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 Komu je kniha určena. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Zpětná vazba od čtenářů . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Errata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 ČÁST I ÚVOD DO NÁVRHU SOFTWARU KAPITOLA 1
ZÁKLADNÍ PRAVIDLA PŘI NÁVRHU SOFTWARU . . . . . . . . . . . . .17 Pravidla softwarového návrhu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Zapouzdření údajů . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 Aktéři . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .22 Implementace procesů půjčování. . . . . . . . . . . . . . . . . . . . . . . . . .24
Ladění aplikace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 Znovupoužitelnost kódu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .34 Rozměnit na drobné . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .36 Kompozice místo dědění . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .38 Volná vazba místo závislostí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .41
Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3
K1886.indd 3
18.1.2012 8:25:15
Obsah ČÁST II TVOŘIVÉ VZORY KAPITOLA 2
NÁVRHOVÝ VZOR SINGLETON . . . . . . . . . . . . . . . . . . . . . . . . . . . .49 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 Skryté problémy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .53
Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 Další využití . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 Variace návrhového vzoru Singleton . . . . . . . . . . . . . . . . . . . . . . .57 KAPITOLA 3
NÁVRHOVÝ VZOR FACTORY METHOD . . . . . . . . . . . . . . . . . . . . .61 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 Další využití . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Statická Factory Method a Singleton . . . . . . . . . . . . . . . . . . . . . . .68 KAPITOLA 4
NÁVRHOVÝ VZOR ABSTRACT FACTORY . . . . . . . . . . . . . . . . . . . .71 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 Implementace tabulky HTML . . . . . . . . . . . . . . . . . . . . . . . . . . . . .75 Implementace tabulky pro příkazový řádek . . . . . . . . . . . . . . . . . .82
Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 Další využití . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
4
K1886.indd 4
18.1.2012 8:25:16
Obsah KAPITOLA 5
NÁVRHOVÝ VZOR BUILDER . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .89 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 KAPITOLA 6
NÁVRHOVÝ VZOR PROTOTYPE . . . . . . . . . . . . . . . . . . . . . . . . . . .99 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 Skryté problémy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 ČÁST III STRUKTURÁLNÍ VZORY KAPITOLA 7
NÁVRHOVÝ VZOR COMPOSITE . . . . . . . . . . . . . . . . . . . . . . . . . .113 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 Odstranění listů . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .118
Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 KAPITOLA 8
NÁVRHOVÝ VZOR ADAPTER . . . . . . . . . . . . . . . . . . . . . . . . . . . . .121 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 Další využití . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 5
K1886.indd 5
18.1.2012 8:25:16
Obsah KAPITOLA 9
NÁVRHOVÝ VZOR BRIDGE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .133 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 KAPITOLA 10
NÁVRHOVÝ VZOR DECORATOR . . . . . . . . . . . . . . . . . . . . . . . . . .145 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 Další využití . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 Test existence metody . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .158 KAPITOLA 11
NÁVRHOVÝ VZOR PROXY . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .159 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167 Další využití . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167 KAPITOLA 12
NÁVRHOVÝ VZOR FACADE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .171 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178 Další využití . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
6
K1886.indd 6
18.1.2012 8:25:16
Obsah KAPITOLA 13
NÁVRHOVÝ VZOR FLYWEIGHT . . . . . . . . . . . . . . . . . . . . . . . . . . .181 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194 ČÁST IV VZORY CHOVÁNÍ KAPITOLA 14
NÁVRHOVÝ VZOR OBSERVER . . . . . . . . . . . . . . . . . . . . . . . . . . .197 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 Další využití . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 KAPITOLA 15
NÁVRHOVÝ VZOR MEDIATOR . . . . . . . . . . . . . . . . . . . . . . . . . . . .211 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218 KAPITOLA 16
NÁVRHOVÝ VZOR TEMPLATE METHOD . . . . . . . . . . . . . . . . . . .219 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227 Další využití . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227 7
K1886.indd 7
18.1.2012 8:25:16
Obsah KAPITOLA 17
NÁVRHOVÝ VZOR COMMAND . . . . . . . . . . . . . . . . . . . . . . . . . . .231 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240 Další využití . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240 KAPITOLA 18
NÁVRHOVÝ VZOR MEMENTO . . . . . . . . . . . . . . . . . . . . . . . . . . . .241 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 KAPITOLA 19
NÁVRHOVÝ VZOR VISITOR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .243 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254 KAPITOLA 20
NÁVRHOVÝ VZOR ITERATOR . . . . . . . . . . . . . . . . . . . . . . . . . . . .255 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264 KAPITOLA 21
NÁVRHOVÝ VZOR STATE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .265 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282 8
K1886.indd 8
18.1.2012 8:25:16
Obsah Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283 Další využití . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284 KAPITOLA 22
NÁVRHOVÝ VZOR STRATEGY . . . . . . . . . . . . . . . . . . . . . . . . . . . .285 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286 KAPITOLA 23
NÁVRHOVÝ VZOR CHAIN OF RESPONSIBILITY . . . . . . . . . . . . .287 Problém . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287 Účel návrhového vzoru. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288 Implementace. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296 Skryté problémy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297 KAPITOLA 24
NÁVRHOVÝ VZOR INTERPRETER . . . . . . . . . . . . . . . . . . . . . . . . .299 Definice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299 Shrnutí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299 ČÁST V PŘÍLOHY PŘÍLOHA A
JAZYK UML. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .303 PŘÍLOHA B
VKLÁDÁNÍ ZÁVISLOSTÍ A PŘEVRÁCENÉ ŘÍZENÍ . . . . . . . . . . . .307 PŘÍLOHA C
MODERNÍ APLIKAČNÍ ROZHRANÍ S PLYNULÝMI ROZHRANÍMI . . . . . . . . . . . . . . . . . . . . . . . . . . . . .309 REJSTŘÍK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .315 9
K1886.indd 9
18.1.2012 8:25:16
K1886.indd 10
18.1.2012 8:25:16
Úvodem
Úvodem
Návrhové vzory nabízejí postupy řešení často se vyskytujících problémů při návrhu softwaru. Často jsou považované za velmi komplikované, asociované s komplexními diagramy jazyka UML a obtížně pochopitelnými architekturami. Tato kniha vám ukáže, že návrhové vzory a jejich implementace v jazyce PHP nejsou až tak komplikované, jak se mohou někomu jevit po prvním přečtení jejich definice. Na praktických příkladech se seznámíte s různými návrhovými vzory, které vám pomohou při každodenní práci a kvůli nimž už nebudete muset organizovat velká pracovní střetnutí nebo vytvářet vícestranné diagramy. Pomocí návodů v této knize se pro vás návrhové vzory stanou nástrojem, který vám ulehčí a obohatí chvíle při vývoji aplikací v jazyce PHP. Jazyk PHP 5 přinesl množství novinek, například kompletně přepracovanou podporu pro XML nebo jednodušší použití webových služeb přes protokol SOAP, který je automaticky podporovaný v jazyce PHP. Přístup k různým typům databází lze nyní díky PHP 5.1 a PDO provádět přes jednotné rozhraní. Naproti tomu tato aplikační rozhraní sama o sobě zatím neumožňovala realizovat profesionální aplikace. O to se postaral až Zend Engine 2 s kompletně přepracovaným objektovým modelem, který nabízí viditelnost pro atributy a metody, rozhraní, výjimky, ale i abstraktní a finální třídy. To vám, jakožto vývojářům v jazyce PHP, umožňuje navrhovat softwarové architektury, které nebudou v ničem zaostávat za architekturami vývojářů v jazyce Java. Tato kniha vám ukáže, jak můžete při návrhu softwaru využít prvky jazyka PHP 5.3 k tomu, aby váš software splňoval moderní standardy a bylo jej možné bez problémů rozšířit v případě, že se změní požadavky na aplikaci. 11
K1886.indd 11
18.1.2012 8:25:16
Úvodem
Dále se seznámíte s pravidly, která byste měli dodržovat při návrhu aplikace. Kromě toho vám návrhové vzory nabízejí jazyk, pomocí něhož můžete řešit problémy s ostatními kolegy vývojáři, aniž abyste museli každý detail podrobně vysvětlovat.
Struktura knihy Celá kniha je rozdělená do pěti částí a lze ji číst dvěma způsoby. Buď ji budete číst postupně, od začátku do konce, a přitom se naučíte krok za krokem jednotlivé návrhové vzory, nebo ji použijete jako katalog, v němž si vyhledáte přesně ten návrhový vzor, jehož implementací se chce zabývat. Každý návrhový vzor představuje jednu kapitolu. V některých kapitolách se setkáte s odkazy na jiné návrhové vzory – můžete je však použít i nezávisle na sobě. Jiné kapitoly naopak nemusí být rozepsané dopodrobna, a to zvláště v případě, že popisovaný návrhový vzor lze nahradit jiným návrhovým vzorem, který řeší stejný problém. V úvodní části (kapitola 1 – Základní pravidla při návrhu softwaru) si na příkladu vyzkoušíte, jakým chybám byste se měli vyhnout při návrhu architektury softwaru. Vyvodí se z nich všeobecně platná pravidla, jež se stanou základem návrhových vzorů probíraných v této knize. Druhá část (kapitoly 2–6) pojednává o návrhových vzorech týkajících se tvorby objektů. V třetí části knihy (kapitoly 7–13) se představí návrhové vzory, které se zabývají kompozicí různých objektů. V této části se navíc seznámíte se strategiemi, které umožňují rozšiřitelnost daných kompozic. Čtvrtá část (kapitoly 14–24) se zabývá poslední skupinou klasických návrhových vzorů. Tyto vzory řeší problémy, které se mohou často vyskytnout při interakci různých objektů. V poslední, páté části (Příloha) najdete krátký úvod do jazyka UML, díky němuž nebudete mít problém pochopit diagramy jazyka UML v jednotlivých kapitolách. Kromě toho v této části najdete i techniky nazývané vkládání závislostí (angl. Depencency Injection), které staví na filozofii převráceného řízení (angl. Inversion of Control – IoC), a také se naučíte vytvářet aplikační rozhraní s Fluent Interfaces.
12
K1886.indd 12
18.1.2012 8:25:16
Úvodem
Ve všech výpisech v knize budou vynechané počáteční a koncové značky () a bude uvedený jen kód jazyka PHP. Pro označení názvů jednotlivých tříd, metod a atributů budeme používat anglické pojmy, což je způsob, který doporučuji i pro vaše projekty, protože tyto pojmy se lépe hodí k nativním názvům tříd a funkcí jazyka PHP. Pro lepší přehlednost nebudou uváděné komentáře. U reálných projektů byste však měli myslet alespoň na popis aplikačního rozhraní pomocí nástroje PHPDoc (http://www.phpdoc.org). Všechny příklady jsou napsané pro jazyk PHP ve verzi 5.3. Chcete-li je použít se starší verzí jazyka PHP, musíte je patřičně upravit – vzdát se používání jmenných prostorů a odstranit z výpisů klíčová slova namespace a use.
Komu je kniha určena Tato kniha je určená vývojářům, kteří už mají zkušenosti s programováním v jazyce PHP. V ideálním případě se už při realizaci projektu střetli s objektově orientovaným programováním (OOP) v jazyce PHP 4 nebo PHP 5, avšak žádné hluboké znalosti OOP v jazyce PHP 5 nejsou podmínkou. Pro vysvětlení a pochopení jednotlivých návrhových vzorů probíraných v této knize nejsou vyžadované žádné hluboké znalosti jazyka UML (Unified Modeling Language). Kromě krátkého úvodu do tohoto jazyka, který najdete v příloze, nabízí tato kniha diagramy, jejichž obsah pro vás bude srozumitelný i se základy jazyka UML. Kniha Návrhové vzory v PHP je napsaná pro programátory, kteří chtějí při vytváření profesionálních architektur využívat vlastnosti OOP. Jejím těžištěm je přitom architektura softwaru. Věci, které v knize nebudou probírané, se týkají oblasti nízkoúrovňové logiky, jako je například přístup k souborům, analyzování dokumentů XML nebo přístup k údajům v databázi pomocí jazyka SQL. Máte-li málo zkušeností s jazykem PHP, avšak pojem návrhové vzory vám je známý z jiných programovacích jazyků, můžete se pomocí této knihy seznámit s implementací různých vzorů, jejichž realizace uvedeným způsobem je možná jen v jazyce PHP.
13
K1886.indd 13
18.1.2012 8:25:16
Úvodem
Zpětná vazba od čtenářů Nakladatelství a vydavatelství Computer Press, které pro vás tuto knihu připravilo, stojí o zpětnou vazbu a bude na vaše podněty a dotazy reagovat. Můžete se obrátit na následující adresy: redakce PC literatury Computer Press Spielberk Office Centre Holandská 3 639 00 Brno nebo
[email protected] Computer Press neposkytuje rady ani jakýkoli servis pro aplikace třetích stran. Pokud budete mít dotaz k programu, obraťte se prosím na jeho tvůrce.
Errata Přestože jsme udělali maximum pro to, abychom zajistili přesnost a správnost obsahu, chybám se úplně vyhnout nedá. Pokud v některé z našich knih najdete chybu, ať už chybu v textu nebo v kódu, budeme rádi, pokud nám ji nahlásíte. Ostatní uživatele tak můžete ušetřit frustrace a pomoci nám zlepšit následující vydání této knihy. Veškerá existující errata zobrazíte na adrese http://knihy.cpress.cz/K1886 po klepnutí na odkaz Soubory ke stažení.
14
K1886.indd 14
18.1.2012 8:25:16
Úvodem
ČÁST I
Úvod do návrhu softwaru
15
K1886.indd 15
18.1.2012 8:25:16
K1886.indd 16
18.1.2012 8:25:16
KAPITOLA 1
Základní pravidla při návrhu softwaru
Máloco podléhá tak častým změnám jako právě software. Ve většině projektů se na něj nakládají stále nové požadavky a stejně rychle odpadají požadavky dosud implementované – nebo v lepším případě jsou často pozměněné. Jako softwarový vývojář musíte jednotlivé komponenty své aplikace navrhnout takovým způsobem, abyste mohli relativně rychle a flexibilně reagovat na měnící se požadavky. V této kapitole se dozvíte, jak lze vyvíjet flexibilní aplikace, a také to, že je důležité zamyslet se nad návrhem aplikace ještě před vlastní implementací. Tím se vyhnete situaci, kdy náhle stojíte před problémem, kdy musíte přepsat velké části aplikace, aby bylo možné reagovat na změněné požadavky. Jestliže tomu nezabráníte, může každý změněný požadavek na software znamenat zvýšené náklady, čímž se prodlouží čas nutný k dokončení projektu nebo se do softwaru nestihnou zapracovat všechny požadavky. Na příkladu v této kapitole si osvojíte základy návrhu softwaru a naučíte se, jak sestavit aplikaci skládající se z více tříd, jež splňují požadavky na rozšiřitelnost a flexibilitu. Na konci kapitoly se dozvíte, jak lze co nejrychleji najít vhodný návrhový vzor pro řešení budoucích problémů. Vyzbrojeni těmito vědomostmi můžete použít následující kapitoly jako encyklopedii, když narazíte v jednom ze svých projektů na problémy, které budete chtít vyřešit pomocí návrhových vzorů. 17
K1886.indd 17
18.1.2012 8:25:16
Část I Úvod do návrhu softwaru
Protože hlavním cílem této knihy je blíže vás seznámit se správnou architekturou aplikací, výklad nebude zacházet až do detailů a nebude se věnovat perzistentnímu ukládání údajů nebo grafickému uživatelskému rozhraní. Místo toho budeme klást důraz na rozhraní, která vám dané třídy nabízejí, a na to, jak jednotlivé třídy mezi sebou komunikují.
Pravidla softwarového návrhu K objasnění základních pravidel softwarového návrhu využijete následující příklad. Představte si, že byste měli implementovat aplikaci, pomocí níž bude knihovna spravovat své publikace. Na konci této kapitoly budete mít za sebou vývoj funkční aplikace, s níž bude možné tyto publikace půjčovat a každé takovéto půjčení přesně protokolovat. V reálné knihovně bude správce chtít jistě ukládat o každé publikaci více informací a půjčování nebude nefungovat přesně tak, jako to bude uvedené zde, ale jako příklad probírané problematiky to stačí. Pokud jste se už nějakým způsobem zabývali objektově orientovaným programováním, jsou vám pojmy třída (angl. Class), objekt (angl. Object) a možná i rozhraní (angl. Interface) docela dobře známé. Koncepce tříd představuje pokus přenést věci z reálného světa, s nimiž má aplikace něco společné, do světa programování. Každý typ publikace má samozřejmě jiné vlastnosti, ale určitým způsobem se všechny vzájemně podobají, z čehož lze odvodit následující rozhraní: namespace cz\k1886\example; interface Publication { public function open(); public function close(); public function setPageNumber($page); public function getPageNumber(); }
Reprezentace libovolné knihy by potom mohla vypadat následovně: namespace cz\k1886\example; class Book implements Publication { protected $category; protected $pageCount; protected $pageNumber = 0; protected $closed = true;
18
K1886.indd 18
18.1.2012 8:25:16
Kapitola 1 Základní pravidla při návrhu softwaru public function __construct($category, $pageCount) { $this->category = $category; $this->pageCount = $pageCount; } public function open() { $this->closed = false; } public function setPageNumber($page) { if ($this->closed !== false || $this->pageCount < $page) { return false; } $this->pageNumber = $page; return true; } public function getPageNumber() { return $this->pageNumber; } public function close() { $this->setPageNumber(0); $this->closed = true; } public function __destruct() { if (!$this->closed) { $this->close(); } } // další metody pro čtení atributů }
Reprezentace knihy má čtyři atributy: kategorii, do níž náleží, počet stran, konkrétní stranu, na níž je právě otevřená, a příznak udávající, zda je otevřená nebo zavřená.
Zapouzdření údajů Třídy a objekty vám umožňují omezit přístup k údajům, čehož byste měli využívat. Díky tomu totiž můžete změnit vnitřní strukturu třídy, aniž by se to dotklo jiných tříd nebo metod, které dotyčnou třídu používají. Takové změny mohou být kromě jiného způsobené i následujícími důvody:
19
K1886.indd 19
18.1.2012 8:25:16
Část I Úvod do návrhu softwaru
Změnily se požadavky na aplikaci, přičemž je nutné změnit algoritmy. Pokud se změny provedou v rámci jedné třídy, neovlivní to žádnou z tříd, jež upravenou třídu volají. Implementované algoritmy jsou velmi pomalé, protože aplikaci používá stále větší počet uživatelů. Tyto algoritmy můžete přepsat, povedou-li ke stejnému výsledku. Je zapotřebí změnit úložiště údajů. K tomu může dojít například v situaci, když zjistíte, že aktuální úložiště je vzhledem k objemu údajů nevhodné, a je tedy nutné použít místo souboru databázi.
Pro objasnění znovu využijeme příklad knihovny. Vzhledem k tomu, že půjčování publikací není zadarmo, musí být někde uložená denní sazba pro jednu publikaci. Je-li tato hodnota zpočátku stále stejná, může vás napadnout použít globální proměnnou nebo nějaký podobný způsob konfigurace: $dailyRate = 10;
Co se ale stane, přijde-li požadavek, aby se denní sazba pro dětské knihy lišila od sazby pro knihy vědecké nebo aby celková částka závisela na délce vypůjčení. Tím by velmi vzrostl počet konfiguračních možností a museli byste v příslušných místech své aplikace implementovat logiku, pomocí níž byste načítali správnou konfiguraci. Z těchto důvodů je lepší zapouzdřit přístup k denní sazbě hned od začátku. Protože se předpokládá, že výška této sumy se bude lišit v závislosti na vypůjčené publikaci, představuje rozhraní Publication dobré místo pro tuto logiku. Toto rozhraní totiž implementují všechny publikace (knihy, časopisy apod.). Rozšiřte tedy toto rozhraní o jednu metodu a tu pak implementujete v příslušných třídách. Příklad metody pro třídu Book by mohl vypadat následovně: namespace cz\k1886\example; class Book implements Publication { // ... atributy a metody public function getDailyRate() { return 10; } }
20
K1886.indd 20
18.1.2012 8:25:16
Kapitola 1 Základní pravidla při návrhu softwaru
Má-li být pro jiný typ knihy účtovaná jiná denní sazba, pak stačí jen přepsat tuto metodu a nový požadavek máte implementovaný. Tím jste se seznámili s prvním pravidlem návrhu softwaru u objektově orientované architektury: PRAVIDLO Přístup k údajům vždy v rámci třídy zapouzdřete a poskytněte metody, pomocí nichž lze dané údaje získat.
Stejně jednoduchá je implementace požadavku, aby denní sazba byla nižší v případě vypůjčení knihy na déle než dva týdny. V tomto případě stačí také změnit jen jednu metodu: namespace cz\k1886\example; class Book implements Publication { // ... atributy a metody public function getDailyRate($days = 1) { if ($days >= 14) { return 7.50; } return 10; } }
S tím také samozřejmě souvisí úprava tříd, jež volají metodu getDailyRate(), protože tato metoda musí od nynějška předávat počet dní. Této dodatečné změně se můžete vyhnout, pokud jste se při návrhu rozhraní pokusili stanovit, jaké údaje by byly eventuálně potřebné při výpočtu denní sazby. Nikdy však nebude možné zohlednit všechny budoucí požadavky hned při první implementaci rozhraní. V takovém případě byste museli metodě předávat všechny dostupné údaje, čímž byste rozbili zapouzdření údajů. Pokuste se najít zlatou střední cestu a metodu navrhnout tak, aby byla v budoucnosti jednoduše rozšiřitelná – jak jsme si to ukázali v tomto příkladu. Tím jste se naučili už druhé pravidlo objektově orientovaného návrhu softwaru: PRAVIDLO Svá rozhraní navrhujte tak, aby je bylo možné později rozšířit.
21
K1886.indd 21
18.1.2012 8:25:17
Část I Úvod do návrhu softwaru
Na začátku návrhu softwaru provedete vždy stejné kroky: musíte se zamyslet nad spravovanými údaji a nad tím, jak lze tyto údaje zahrnout do entit. Z toho pak vyplynou jednotlivé třídy a metody.
Aktéři V příkladu knihovny se nacházejí následující aktéři, kteří musí být částí vaší aplikace:
jednotlivé knihy, které jsou částí knihovny a lze je půjčovat, členové knihovny, kteří si knihy půjčují, samotná knihovna, která spravuje jednotlivé publikace a stará se o půjčování.
Z těchto představitelů můžete hned odvodit potřebné třídy a rozhraní. Pro knihy jste to už provedli, zůstávají už jen členové a knihovna. Implementujte proto třídu Member, která bude reprezentovat jednoho člena knihovny: namespace cz\k1886\example; class Member { protected $id; protected $name; public function __construct($id, $name) { $this->id = $id; $this->name = $name; } public function getId() { return $this->id; } public function getName() { return $this->name; } }
Daný člen se skládá z jedinečného identifikátoru, jímž se v systému identifikuje, a ze svého jména, což pro jednoduchou demonstraci úplně stačí. Informace, které jsou přiřazené členovi, se předají prostřednictvím konstruktoru a uloží se do atributů objektu. Přístup k těmto údajům je možný pomocí metod getId() a getName(). 22
K1886.indd 22
18.1.2012 8:25:17
Kapitola 1 Základní pravidla při návrhu softwaru
Po vytvoření tříd pro členy a knihy nastal čas věnovat se samotné knihovně, kterou reprezentuje třída Library: namespace cz\k1886\example; class Library { protected $library = array(); public function addToLibrary($id, Publication $p) { $this->library[$id] = $p; } public function rentPublication(Publication $p, Member $m) { } public function returnPublication(Publication $p) { } }
Knihovna má jeden atribut, do něhož se ukládají všechny publikace určené k vypůjčení, a také tři metody:
Metoda addToLibrary() se používá k přiřazení nové publikace do knihovny. Tato publikace se jednoduše uloží do pole $library. Pomocí metody rentPublication() si může člen knihovny vypůjčit konkrétní publikaci. Tuto metodu jste zatím neimplementovali, neboť nejdříve je nutné definovat, v jaké formě se budou údaje ukládat. Pro vrácení publikace se používá metoda returnPublication(). V tomto případě není nutné předat objekt člena, který publikaci vrací. Koneckonců knihovna přece musí vědět, kdo měl danou publikaci vypůjčenou.
V následujících krocích budete deklarovat metody naplněné skutečnou logikou. Metody rentPublication() a returnPublication() mají vždy něco společné s vypůjčením publikace. Buď proces vypůjčení začal, nebo byl vrácením publikace zpět do knihovny ukončený. Tohoto procesu se vždy účastní dva aktéři: publikace, která má být vypůjčena, a osoba, která si ji vypůjčila. Dále jsou důležité časové údaje, kdy byla publikace vypůjčená a také kdy byla vrácená, aby tak bylo možné vypočítat cenu za vypůjčení. Ve skutečné knihovně to většinou funguje jinak, ale jako příklad postačí i tato varianta. Podle pravidel zapouzdřování, která jste se právě naučili, se pokuste dostat tyto informace do jedné třídy. 23
K1886.indd 23
18.1.2012 8:25:17
Část I Úvod do návrhu softwaru
Implementace procesů půjčování Nová třída RentalAction musí obsahovat následující informace:
osoba, která si publikaci půjčuje, publikace, která se půjčuje, datum, kdy byla publikace vypůjčena, datum, kdy byla publikace vrácena a proces ukončen.
Implementace této třídy může vypadat následovně: namespace cz\k1886\example; class RentalAction { protected $publication; protected $member; protected $rentDate; protected $returnDate = null; function __construct(Publication $p, Member $m, $date = null) { $this->publication = $p; $this->member = $m; // v případě neuvedení data použít aktuální if (null === $date) { $date = date('Y-m-d H:i:s'); } $this->rentDate = $date; } public function getPublication() { return $this->publication; } public function getMember() { return $this->member; } public function getRentDate() { return $this->rentDate; } public function getReturnDate() { return $this->returnDate;
24
K1886.indd 24
18.1.2012 8:25:17
Kapitola 1 Základní pravidla při návrhu softwaru } }
Konstruktoru třídy musíte při vytvoření nového procesu předat jako parametry publikaci, která má být vypůjčena, a dále osobu, která si ji půjčuje. Volitelně můžete uvést datum a čas, kdy byla daná publikace vyzvednuta. V případě neuvedení data se použije aktuální datum. Tyto hodnoty se uloží v příslušných atributech třídy. Dále se ve třídě nacházejí čtyři metody pro čtení atributů, které vám umožňují přistupovat k atributům třídy. Pokud si nyní přijde nějaký člen vaší knihovny vypůjčit určitou publikaci, můžete vytvořit odpovídající proces vypůjčení takto: use cz\k1886\example\Book; use cz\k1886\example\Member; use cz\k1886\example\RentalAction; $book = new Book('PC', 100); $mabo = new Member(1, 'Marian Böhmer'); $rentalAction = new RentalAction( $book, $mabo, '2011-08-22 16:00:00' );
V této chvíli je sice možné knihu vypůjčit, avšak zatím není možné zaznamenat, že už byla i vrácena. Pro tento účel jste si v této třídě už předem rezervovali atribut $returnDate. K jeho nastavení je nutné vložit do třídy RentalAction následující metodu: namespace cz\k1886\example; class RentalAction { // ... atributy a metody třídy public function markPublicationReturned($date = null) { // v případě neuvedení data použít aktuální if (null === $date) { $date = date('Y-m-d H:i:s'); } $this->returnDate = $date; } }
Bude-li publikace přinesena zpět do knihovny, pak pro uložení této informace do objektu stačí zavolat příslušnou metodu: 25
K1886.indd 25
18.1.2012 8:25:17
Část I Úvod do návrhu softwaru $rentalAction->markPublicationReturned('2011-08-30 13:00:00');
Výsledek této operace je zobrazený na obrázku 1.1.
Obrázek 1.1: Proces vypůjčení knihy z knihovny
Pokud si chcete ověřit, zda byla kniha vrácena a tím daný proces ukončen, můžete k tomu využít hodnotu, kterou vrací metoda getReturnDate(): if (null !== $rentalAction->getReturnDate()) { print 'Publikace byla vrácená'; }
Tento kód se však neimplementuje jednoduše, a proto je vhodné zapouzdřit jej do další metody: namespace cz\k1886\example; class RentalAction { // ... atributy a metody třídy public function isReturned() { return null !== $this->returnDate; } }
26
K1886.indd 26
18.1.2012 8:25:17
Kapitola 1 Základní pravidla při návrhu softwaru
Tuto metodu můžete nyní využít ke zjištění, zda už byla publikace vrácena: if
($rentalAction->isReturned()) { print 'Publikace byla vrácená';
}
Tím jste se naučili další pravidlo vývoje softwaru: PRAVIDLO V metodách tříd nezapouzdřujte jen údaje, ale také algoritmy, díky čemuž budou komplexní operace implementované centrálně na jednom místě.
Zavedení metody isReturned() vám v budoucnosti umožní na základě podmínky určit, zda už byl proces vypůjčení ukončen a kniha vrácena zpět do knihovny. Pomocí nové třídy RentalAction je možné implementovat chybějící metody ve třídě Library. Nejdříve je nutné vložit do uvedené třídy nový atribut $rentalActions, v němž budou v podobě objektů uložené jednotlivé procesy vypůjčení. Nejjednodušším datovým typem použitelným k tomuto účelu je pole. Následující implementace metody pro vypůjčení publikace musí splňovat následující kritéria:
otestovat, zda je požadovaná publikace součástí knihovny, otestovat, zda je požadovaná publikace aktuálně vypůjčená (v takovém případě ji nelze znovu vypůjčit), pomocí nové instance třídy RentalAction vytvořit nový proces vypůjčení.
Implementace metody rentPublication() může vypadat následovně: namespace cz\k1886\example; class Library { protected $rentalActions = array(); // ... atributy a metody třídy public function rentPublication(Publication $p, Member $m) { $publicationId = array_search($p, $this->library); if (false === $publicationId) { throw new UnknownPublicationException(); } if (!$this->isPublicationAvailable($p)) { throw new PublicationNotAvailableException();
27
K1886.indd 27
18.1.2012 8:25:18
Část I Úvod do návrhu softwaru } $rentalAction = new RentalAction($p, $m); $this->rentalActions[] = $rentalAction; return $rentalAction; } }
Pomocí funkce array_search() jazyka PHP můžete zjistit, zda se publikace nachází v nabídce knihovny. Vrátí-li hodnotu false, pak signalizuje chybu vyvoláním výjimky. Test, zda je daná publikace právě vypůjčená, je trochu komplikovanější, a proto je jeho logika přesunuta do další metody. Díky tomu mohou tuto logiku použít i jiné metody třídy. Tímto způsobem opět zapouzdřujete určitý algoritmus do jedné metody. Konkrétní implementace této metody bude probrána později. V případě, že publikace není vypůjčená, vytvoří se nová instance třídy RentalAction, které předáte půjčovanou publikaci a člena, který si ji půjčuje. Tuto instanci potom uložíte do k tomu určeného atributu, kde jsou uložené i všechny ostatní procesy vypůjčení. Metoda rentPublication() vrací objekt RentalAction. Vrácení tohoto objektu sice není v současnosti pro knihovnu důležité, protože neobsahuje žádné další informace než ty, které už obdržela, avšak v budoucnosti může objekt RentalAction obsahovat i další informace, například číslo objednávky, které budete chtít poskytnout aplikaci. Ještě před tím, než začnete ve vypůjčené publikaci listovat, je nutné podívat se na implementaci pomocné metody isPublicationAvailable(). Pro zjištění, zda je daná publikace právě vypůjčená, stačí jen otestovat, zda atribut $rentalActions obsahuje proces vypůjčení pro hledanou publikaci, který ještě nebyl ukončen. K tomuto účelu vám dobře poslouží metody pro čtení atributů třídy RentalAction: public function isPublicationAvailable(Publication $p) { foreach ($this->rentalActions as $rentalAction) { if
($rentalAction->getPublication() !== $p) { continue;
} if
($rentalAction->isReturned()) { continue;
} return false; } return true; }
28
K1886.indd 28
18.1.2012 8:25:18
Kapitola 1 Základní pravidla při návrhu softwaru
Ve funkci isPublicationAvailable() probíhá iterace přes všechny prvky v poli $rentalActions (všechny procesy vypůjčení). Pokud aktuální proces nepatří hledané publikaci nebo už je ukončený, pokračuje se s další iterací. Pokud daný proces náleží k hledané publikaci a ještě není ukončen, nemůže být daná publikace znovu vypůjčena, což se signalizuje vrácením hodnoty false. Neexistuje-li pro hledanou publikaci žádný proces, který ještě nebyl ukončen, vrátí se hodnota true. V tomto okamžiku lze třídu Library použít k půjčování publikací a navíc je zaručené, že jedna publikace může být vypůjčená jen jednou, jak to ukazuje následující příklad: use cz\k1886\example\Library; use cz\k1886\example\Book; use cz\k1886\example\Member; $library = new Library(); $book = new Book('PC', 100); $mabo = new Member(1, 'Marian Böhmer'); $luigi = new Member(2, 'Luigi Valentino'); $library->addToLibrary('pc1', $book); $library->rentPublication($book, $mabo); $library->rentPublication($book, $luigi);
Po jeho provedení reaguje knihovna následující výjimkou: Fatal error:
Uncaught exception 'cz\k1886\example\
PublicationNotAvailableException' in ch01\Library.php:24
Tuto výjimku můžete ve frontendu své aplikace změnit na chybové hlášení. Aby bylo možné i vrácení vypůjčených publikací, musíte dále implementovat metodu returnPublication(). Tato metoda musí najít aktuální proces vypůjčení, který náleží k dané publikaci, a označit jej jako ukončený. Kód této metody se podobá tomu v metodě isPublicationAvailable(). I v tomto případě procházíme přes všechny procesy vypůjčení, dokud nenajdeme proces pro hledanou publikaci, který ještě nebyl ukončen. Tento proces potom označíme pomocí metody markPublicationReturned() jako ukončený. Kompletní implementace vypadá takto: namespace cz\k1886\example; class Library {
29
K1886.indd 29
18.1.2012 8:25:18
Část I Úvod do návrhu softwaru // ... atributy a metody třídy public function returnPublication(Publication $p) { foreach ($this->rentalActions as $rentalAction) { if ($rentalAction->getPublication() !== $p) { continue; } if ($rentalAction->isReturned()) { continue; } $rentalAction->markPublicationReturned(); return true; } return false; } }
V tomto okamžiku mohou být vypůjčené publikace znovu vráceny a připraveny na své další vypůjčení: use cz\k1886\example\Library; use cz\k1886\example\Book; use cz\k1886\example\Member; $library = new Library(); $book = new Book('PC', 100); $mabo = new Member(1, 'Marian Böhmer'); $luigi = new Member(2, 'Luigi Valentino'); $library->addToLibrary('pc1', $book); $library->rentPublication($book, $mabo); $library->returnPublication($book); $library->rentPublication($book, $luigi);
Při provedení tohoto kódu nedošlo k vyvolání žádné výjimky. Místo toho se vytvořily dva procesy vypůjčení, z nichž jeden byl i ukončený. U této implementace vás možná napadne, proč není proces po jeho ukončení z pole vymazán? Jednoduše proto, že byste v takovém případě ztratili historii procesů vypůjčení. Představte si, že by chtěl správce knihovny zjistit, které publikace se nejvíce půjčují nebo kdo je nejaktivnějším členem knihovny. Pomocí procesů uložených v poli $rentalActions lze tyto údaje snadno poskytnout. Je nutné si uvědomit, že v tomto příkladu nešlo o uchovávání údajů ani o jeho výkonnost, ale o vytvoření architektury aplikace. Místo pole s objekty typu Ren30
K1886.indd 30
18.1.2012 8:25:18
Kapitola 1 Základní pravidla při návrhu softwaru talAction byste v reálné aplikaci použili pro jejich uložení databázovou tabulku.
Databázové systémy už nabízejí nástroje, jak lokalizovat záznam k aktuálnímu procesu vypůjčení konkrétní publikace.
Ladění aplikace V této chvíli máte za sebou základy aplikace a jste schopni půjčovat publikace a přijímat je zpět. V této podkapitole si na příkladu části aplikace ukážeme, jaká další pravidla návrhu softwaru byste měli ve svých aplikacích zohlednit. Třebaže funkce pro ladění (angl. debugging) aplikací v jazyce PHP lze zlepšit pomocí externích nástrojů, skoro každý vývojář v jazyce PHP využívá k výpisu nezbytných informací při tomto kroku příkaz print nebo echo. Tyto příkazy se během vývoje jednoduše vloží do zdrojového kódu a před tím, než se aplikace nasadí do provozu, se zase odstraní. V rámci této části budete takovýto ladicí kód postupně, kroku za krokem vylepšovat, při čemž se seznámíte s různými pravidly, která byste měli zohlednit i v dalších softwarových projektech. Při dalším vývoji softwaru knihovny můžete do zdrojového kódu následujícím způsobem vložit hlášení používané při ladění: namespace cz\k1886\example; use cz\k1886\example\RentalAction; class Library { protected $library = array(); protected $rentalActions = array(); public function addToLibrary($id, Publication $p) { $this->library[$id] = $p; print 'Nová publikace v knihovně: ' . $p->getCategory() . "\n"; } public function rentPublication(Publication $p, Member $m) { $publicationId = array_search($p, $this->library); if (false === $publicationId) { throw new UnknownPublicationException(); } if (!$this->isPublicationAvailable($p)) { throw new PublicationNotAvailableException(); }
31
K1886.indd 31
18.1.2012 8:25:18
Část I Úvod do návrhu softwaru $rentalAction = new RentalAction($p, $m); $this->rentalActions[] = $rentalAction; print $m->getName() . ' si vypůjčil publikaci: ' . $p->getCategory() . "\n"; return $rentalAction; } public function returnPublication(Publication $p) { foreach ($this->rentalActions as $rentalAction) { if ($rentalAction->getPublication() !== $p) { continue; } if ($rentalAction->isReturned()) { continue; } $rentalAction->markPublicationReturned(); print $rentalAction->getMember()->getName() . ' vrátil publikaci: ' . $p->getCategory() . "\n"; return true; } return false; } // ... ostatní metody }
Pokud nyní znovu zavoláte příkazy z předchozího testovacího skriptu s pozměněnou třídou Library, můžete přesně sledovat, kdy se která metoda volá: Nová publikace v knihovně: PC Marian Böhmer si vypůjčil publikaci: PC Marian Böhmer vrátil publikaci: PC Luigi Valentino si vypůjčil publikaci: PC
V závislosti na tom, jakou část aplikace chcete ladit, se bude měnit i množství ladicích informací. Před přesunem aplikace na produkční server jednoduše tyto řádky kódu vymažete. Co se ale stane, pokud objevíte problém, který se vyskytuje jen v produkčním prostředí, protože souvisí například s množstvím paralelních dotazů na aplikaci? Tato hlášení nemůžete jednoduše zobrazit koncovému uživateli. V takovém případě používá většina vývojářů protokolovací soubor, do něhož přesměrují veškerá hlášení. Často je tedy ladicí kód rozšířený o příkazy if/else nebo switch, 32
K1886.indd 32
18.1.2012 8:25:18
Kapitola 1 Základní pravidla při návrhu softwaru
pomocí nichž lze přepínat mezi laděním v produkčním nebo vývojovém prostředí. V případě knihovny by to vypadalo následovně: namespace cz\k1886\example; class Library { protected $library = array(); protected $rentalActions = array(); public function addToLibrary($id, Publication $p) { $this->library[$id] = $p; switch(DEBUG_MODE) { case 'echo': print 'Nová publikace v knihovně: ' . $p->getCategory() . "\n"; break; case 'log': error_log('Nová publikace v knihovně: ' . $p->getCategory() . "\n", 3, './library.log'); break; } } public function rentPublication(Publication $p, Member $m) { $publicationId = array_search($p, $this->library); if (false === $publicationId) { throw new UnknownPublicationException(); } if (!$this->isPublicationAvailable($p)) { throw new PublicationNotAvailableException(); } $rentalAction = new RentalAction($p, $m); $this->rentalActions[] = $rentalAction; switch(DEBUG_MODE) { case 'echo': print $m->getName() . ' si vypůjčil publikaci: ' . $p->getCategory() . "\n"; break; case 'log': error_log($m->getName() . ' si vypůjčil publikaci: ' . $p->getCategory() . "\n", 3, './library.log');
33
K1886.indd 33
18.1.2012 8:25:19
Část I Úvod do návrhu softwaru break; } return $rentalAction; } // ... ostatní metody třídy }
Pomocí konstanty DEBUG_MODE můžete přepínat mezi jednoduchými hlášeními vypisovanými na obrazovku a zapisovanými do protokolovacího souboru funkcí error_log(). K tomu je nutné přidat do skriptu následující řádek, umožňující centrální řízení způsobu ladění: define('DEBUG_MODE', 'log');
Co však na prvý pohled vypadá jako komfortní řešení, je v konečném důsledku jen velká slabina aplikace. Na každém místě, kde chcete vložit ladicí kód, musíte vložit devět řádků kódu. Tím bude zdrojový kód vaší aplikace o něco delší, a aplikace se tak stane pomalejší a obtížně udržovatelná. Na tomto místě přichází na řadu základní pravidlo znovupoužitelnosti kódu.
Znovupoužitelnost kódu Určitě vás napadlo, že byste se měli vyhýbat duplicitnímu kódu, který často vzniká při postupu ve stylu „Copy & Paste“. Při každé změně tohoto kódu (například při změně názvu souboru nebo při přidání třetího ladicího režimu) je nutné provést změnu na mnoha místech kódu. Zabránit tomu můžete tím, že duplicitní kód přesunete do samostatné metody, kterou na potřebných místech jen zavoláte. Nová metoda třídy Library bude vypadat následovně: protected function debug($message) { switch(DEBUG_MODE) { case 'echo': print $message . "\n"; break; case 'log': error_log($message . "\n", 3, './library.log'); break; } }
34
K1886.indd 34
18.1.2012 8:25:19
Kapitola 1 Základní pravidla při návrhu softwaru
Nové metodě debug() je nutné předat řetězec se zprávou, která se má nějak zpracovat. Tato metoda na základě konstanty DEBUG_MODE rozhodne, zda se zadaná informace vypíše na obrazovku nebo se zapíše do protokolovacího souboru. Následně je zapotřebí upravit i metody, které mají novou metodu debug()využívat. namespace cz\k1886\example; class Library { protected $library = array(); protected $rentalActions = array(); public function addToLibrary($id, Publication $p) { $this->library[$id] = $p; $this->debug( 'Nová publikace v knihovně: ' . $p->getCategory() ); } public function rentPublication(Publication $p, Member $m) { // ... vlastní logika metody $this->debug( $m->getName() . ' si vypůjčil publikaci: ' . $p->getCategory() ); return $rentalAction; } public function returnPublication(Publication $p) { // ... vlastní logika metody $this->debug( $rentalAction->getMember()->getName() . ' vrátil publikaci: ' . $p->getCategory() ); // ... vlastní logika metody } }
Díky této úpravě jste zamezili duplikování kódu, třída Library je opět skoro tak velká, jako byla na začátku, a změny stačí provést jen jednou nezávisle na tom, na
35
K1886.indd 35
18.1.2012 8:25:19
Část I Úvod do návrhu softwaru
kolika místech se metoda debug()volá. Tím jste se naučili další pravidlo objektově orientovaného návrhu aplikací: PRAVIDLO Znovupoužitelnost kódu je lepší než duplicitní kód.
Co se ale stane, pokud bude nutné zapracovat do aplikace nové způsoby ladění? Kromě zápisu do protokolovacího souboru byste mohli chtít odeslání hlášení pomocí e-mailů, SMS nebo zápis do syslog. Pokud byste každou z těchto možností zakomponovali do metody debug(), stávala by se stále větší a větší, a tím i náchylnější na chyby. Jako se tomu dá zabránit?
Rozměnit na drobné Metody, které obsahují vlastní aplikační logiku a volají metodu debug(), nemusí vědět, jakým způsobem se předávaná informace zpracovává. Co tedy bráni tomu, pokusit se pomocí dědění udělat kód sloužící pro ladění flexibilnější? K dosažení tohoto cíle potřebujete místo jedné třídy Library implementovat následující tři třídy:
AbstractLibrary – abstraktní třída, která obsahuje aplikační logiku, ale také
abstraktní metodu debug(), kterou používají ostatní metody. EchoingLibrary – třída, která je odvozená od třídy AbstractLibrary a implementuje abstraktní metodu debug(). Ladicí informace se vypisují pomocí příkazu print. LoggingLibrary – třída, která je odvozená od třídy AbstractLibrary a taktéž implementuje abstraktní metodu debug(). V této implementaci se ladicí informace místo vypisování na obrazovku zapisují do protokolovacího souboru.
Začněte s implementací abstraktní třídy: namespace cz\k1886\example; abstract class AbstractLibrary { // ... atributy a metody třídy abstract protected function debug($message); }
V této třídě dojde jen k malým změnám. Do její definice je nutné zahrnout klíčové slovo abstract a toto slovo je též nutné uvést při definici metody debug(). 36
K1886.indd 36
18.1.2012 8:25:19
Kapitola 1 Základní pravidla při návrhu softwaru
Metoda debug() navíc nebude mít žádné tělo, které se bude nacházet až v konkrétní implementaci. Jako další je nutné implementovat obě konkrétní třídy, v nichž přepíšeme metodu debug(): namespace cz\k1886\example; class EchoingLibrary extends AbstractLibrary { protected function debug($message) { print $message . "\n"; } } class LoggingLibrary extends AbstractLibrary { protected function debug($message) { error_log($message . "\n", 3, './library.log'); } }
Obě třídy jsou zaměřené jen na konkrétní úlohy. Třída EchoingLibrary se stará výlučně jen o vypsání zprávy na obrazovku a třída LoggingLibrary se stará jen o zápis do protokolovacího souboru. Tím jsou oba procesy ladění od sebe oddělené. Nyní musíte samozřejmě trochu upravit i skript s příkladem, protože již není možné vytvořit instanci třídy Library. Tu jsme totiž přejmenovali na třídu AbstractLibrary a definovali jako abstraktní. Místo toho vytvořte instanci konkrétní třídy v závislosti na konstantě DEBUG_MODE: use cz\k1886\example\LoggingLibrary; use cz\k1886\example\EchoingLibrary; use cz\k1886\example\Book; use cz\k1886\example\Member; switch(DEBUG_MODE) { case 'echo': $library = new EchoingLibrary(); break; case 'log': $library = new LoggingLibrary(); break; } $book = new Book('PC', 100); $mabo = new Member(1, 'Marian Böhmer'); $luigi = new Member(2, 'Luigi Valentino');
37
K1886.indd 37
18.1.2012 8:25:19
Část I Úvod do návrhu softwaru $library->addToLibrary('pc1', $book); $library->rentPublication($book, $mabo); $library->returnPublication($book); $library->rentPublication($book, $luigi);
Pokud jste tento příklad vyzkoušeli, měli byste vidět stejný výsledek jako v předchozím příkladu, tedy za předpokladu, že je konstanta DEBUG_MODE nastavená na echo. V případě, že chcete ladicí informace posílat přes SMS nebo e-mail, musíte implementovat novou třídu, která v metodě debug() poskytuje tuto možnost. Tím jste se naučili další pravidlo: PRAVIDLO Vyvarujte se monolitickým strukturám a rozložte je na co nejmenší části, které mohou být implementované nezávisle na sobě. Pokud používáte rozsáhlé příkazy if f/elseif f/else nebo switch, popřemýšlejte, zda by se nedaly nahradit zaměnitelnými třídami.
Po tomto elegantním vyřešení ladění ve třídě Library se můžete pustit do dalších tříd, které je také nutné odladit. Řekněme, že chcete například vypsat informace v metodě markPublicationReturned() třídy RentalAction. Nejdříve vás ale napadne, že jste daný problém přece jen nevyřešili tak elegantně, jak jste si to právě mysleli. Abstraktní metodu debug() musíte totiž přidat i do třídy RentalAction a potom i do všech konkrétních implementací, které budou jednotlivé způsoby ladění zpracovávat. Rozměnit na drobné tedy znamená, že budete muset znovu vytvořit třídy AbstractRentalAction, EchoingRentalAction a LoggingAbstractAction. Tím ale porušíte jedno z prvních pravidel, které jste definovali, a vlastní kód na ladění budete implementovat vícekrát, jednou ve třídě Library a jednou ve třídě RentalAction. Pokud k tomu budete ještě chtít ladění pomocí SMS, musíte tuto funkci implementovat znovu dvakrát. Z toho vyplývá, že zatím vyvinutá architektura pro ladění aplikace není úplně perfektní a daný kód je nutné optimalizovat.
Kompozice místo dědění Jak jste právě viděli, je změna metody ladění velmi omezená, protože každá třída může dědit metody jen od jedné třídy. Na vyřešení tohoto problému byste měli třídu, která implementuje logiku pro ladění, oddělit od třídy, která představuje aplikaci jako takovou (aplikuje business logiku). Oddělte tedy kód ladění od aplikační logiky tím, že kód ladění přesunete do úplně nové třídy. Zachovejte 38
K1886.indd 38
18.1.2012 8:25:19
Kapitola 1 Základní pravidla při návrhu softwaru
přitom obě třídy oddělené – tj. jedna třída vypíše údaje přímo, druhá je zapíše do protokolovacího souboru. namespace cz\k1886\example; class DebuggerEcho { public function debug($message) { print $message . "\n"; } } class DebuggerLog { public function debug($message) { error_log($message . "\n", 3, './library.log'); } }
Obě třídy obsahují metodu debug(), postupujete proto podle pravidla, že třídy řeší elementární problémy a mají být co do velikosti co nejmenší. Aby bylo v budoucnosti možné tyto třídy co nejjednodušeji vyměnit, můžete je zavedením rozhraní začlenit do jedné skupiny. Toto nové rozhraní Debugger vyžaduje od každé třídy, která chce zpracovávat hlášení o ladění, aby implementovala metodu debug(): namespace cz\k1886\example; interface Debugger { public function debug($message); }
Protože tyto dvě třídy už danou metodu obsahují, splňují podmínky dané rozhraním Debugger. namespace cz\k1886\example; class DebuggerEcho implements Debugger { public function debug($message) { print $message . "\n"; } } class DebuggerLog implements Debugger { public function debug($message) { error_log($message . "\n", 3, './library.log'); } }
39
K1886.indd 39
18.1.2012 8:25:19
Část I Úvod do návrhu softwaru
Místo abstraktní třídy tu máte sice rozhraní, ale nic víc se v kódu nezměnilo. Ve skutečnosti jste jen přesunuli metodu debug() do jiné třídy, jejíž instanci vytvoříte velice jednoduše: use cz\k1886\example\DebuggerEcho; $debugger = new DebuggerEcho();
Následně můžete předat metodě debug() zprávu: $debugger->debug('Lorem ipsum dolor sit amet');
Po provedení tohoto kódu uvidíte text Lorem ipsum dolor sit amet vypsaný na obrazovce. Pokud zaměníte řádek, který vytváří instanci třídy, a vytvoříte instanci třídy DebuggerLog, zapíše se tato zpráva do protokolovacího souboru. Kód používaný pro ladění aplikací nyní funguje soběstačně, aniž by k tomu potřeboval třídu Library. Třídu Library je ještě nutné upravit tak, aby používala tento nový způsob ladění. namespace cz\k1886\example; use cz\k1886\example\RentalAction; class Library { protected $library = array(); protected $rentalActions = array(); protected $debugger; public function __construct() { switch (DEBUG_MODE) { case 'echo': $this->debugger = new DebuggerEcho(); break; case 'log': $this->debugger = new DebuggerLog(); break; } } protected function debug($message) { $this->debugger->debug($message); } // ... ostatní metody třídy }
40
K1886.indd 40
18.1.2012 8:25:19
Kapitola 1 Základní pravidla při návrhu softwaru
V úvodu jste do třídy Library vložili nový atribut $debugger. Tomu se pak v konstruktoru podle hodnoty konstanty DEBUG_MODE přiřadí instance třídy DebuggerEcho nebo DebuggerLog. Jako poslední věc musíte upravit metodu debug() takovým způsobem, aby delegovala úlohu na objekt v atributu $debugger. Žádné další změny v kódu nejsou nutné. Pokud nyní provedete kód z testovacího skriptu, objeví se na obrazovce znovu stejný výpis nebo se provede zápis do protokolovacího souboru, podle toho, na jakou hodnotu je nastavená konstanta DEBUG_MODE. Podobným způsobem byste postupovali i při implementaci tohoto kódu ve třídě RentalAction. V tomto okamžiku jste zvládli oddělit kód z původní třídy a zapouzdřit ho do univerzálně použitelné třídy. Tím jste splnili předchozí pravidla, jež přikazují vyvarovat se duplicitnímu kódu. Na základě tohoto řešení můžete odvodit další pravidlo objektově orientovaného návrhu: PRAVIDLO Dědění vede k neflexibilním strukturám. Na kombinaci různých funkcí používejte raději kompozice objektů.
Nyní už na aplikaci skutečně není mnoho co vyměnit. Je velmi jednoduché napsat nový ladicí kód, který například odešle informace e-mailem. Avšak jedna možnost vylepšení ještě existuje a bude vysvětlená na následujících řádcích.
Volná vazba místo závislostí Objekt, který zpracovává hlášení o ladění, se v současnosti vytváří v konstruktoru třídy Library, z čehož vyplývá, že tato třída musí znát všechny verze těchto objektů. Pokud budete chtít napsat nový způsob zpracování, například ten na posílání e-mailů, budete muset upravit třídu Library a rovněž všechny ostatní, které tyto objekty využívají. Toto na jedné straně znamená množství práce a na druhé straně to je samozřejmě náchylné k chybám. Mnohem elegantnější by bylo, kdyby knihovna nemusela o vlastních možnostech ladění vůbec nic vědět. Tento způsob programování jde ruku v ruce s úplně základním pravidlem objektově orientovaného programování, které říká: PRAVIDLO Vždy programujte vůči rozhraní, a nikdy ne vůči konkrétní implementaci.
41
K1886.indd 41
18.1.2012 8:25:19
Část I Úvod do návrhu softwaru
Když se vrátíte k deklaraci jednotlivých tříd DebuggerEcho a DebuggerLog, můžete si uvědomit, že jste již definovali jedno rozhraní, které implementovaly obě z těchto tříd. Zbývá tedy jen upravit třídu Library tak, aby znala jen Debugger rozhraní, ale nevěděla nic o konkrétních implementacích. Tento princip se nazývá princip převrácení závislostí (angl. Dependency Inversion Principle – DIP) a vyžaduje, abyste se místo konkrétní implementace vždy opírali o abstrakci. V tomto okamžiku zatím princip DIP nevyužíváte, protože v mnohých případech jste závislí na konkrétních třídách, jako jsou DebuggerEcho a DebuggerLog, ale také RentalAction nebo Member. V dalších kapitolách této knihy se naučíte, jak se lze dost často od těchto závislostí osvobodit. Při aktuálních problémech těsných závislostí mezi třídou knihovny a jednotlivými konkrétními implementacemi rozhraní Debugger můžete aplikovat princip DIP velmi jednoduše – objekt implementující rozhraní Debugger nevytvoříte v konstruktoru třídy Library, ale konstruktoru této třídy jej prostě předáte: namespace cz\k1886\example; class Library { // ... atributy třídy public function __construct(Debugger $debugger) { $this->debugger = $debugger; } // ... další metody třídy }
Pomocí určení typu proměnné v definici konstruktoru je zabezpečené, že tento bude akceptovat jen třídy, jež implementují rozhraní Debugger. Tím je zaručené, že v předávaném objektu existuje metoda debug(). Knihovně je nyní úplně jedno, jakým způsobem se budou předávaná hlášení zpracovávat. Ví totiž jen to, že předaný objekt $debugger nabízí metodu debug(), na kterou lze zpracování hlášení delegovat. Samozřejmě je nutné trochu upravit i vytvoření instance knihovny: use cz\k1886\example\DebuggerEcho; use cz\k1886\example\Library; $debugger = new DebuggerEcho(); $library = new Library($debugger);
Tím jste sestavili další pravidlo a ihned jej aplikovali na svoji aplikaci:
42
K1886.indd 42
18.1.2012 8:25:20
Kapitola 1 Základní pravidla při návrhu softwaru PRAVIDLO Vyhýbejte se těsným závislostem mezi jednotlivými třídami aplikace a vždy upřednostňujte volné vazby tříd.
V případě ladicího kódu jste toho dosáhli pomocí techniky, která se nazývá vkládání závislosti (angl. Dependency Injection – DI). Objekt implementující rozhraní Debugger jste vložili do objektu Library. Tento objekt nemusí vědět, jakého typu je vkládaný objekt, stačí jen, že implementuje požadované rozhraní. Téma vkládání závislostí je podrobněji rozepsané v příloze na konci knihy. V této chvíli vás může napadnout otázka, co se stane s aplikací, když budete chtít ladění úplně vypnout. Konstruktor třídy Library totiž vždy očekává objekt, jemuž lze předávat zprávy ke zpracování. Na první pohled není možné ladění aplikace kompletně deaktivovat. Naštěstí to jen tak vypadá, jinak by byla celá tato práce zbytečná. Protože knihovna stále očekává objekt implementující rozhraní Debugger, nevyhnete se mu ani v případě, jestliže není požadován žádný způsob ladění. Avšak nikde není definované, že objekt implementující rozhraní Debugger musí nějakým způsobem předané zprávy zpracovat. Musí je jen přijmout. Proto můžeme vytvořit takovou implementaci, která všechna předaná hlášení ignoruje: namespace cz\k1886\example; class DebuggerVoid implements Debugger { public function debug($message) { // ... všechna hlášení ignorovat } }
Tuto variantu můžete použít, pokud nepotřebujete žádný způsob ladění aplikace. Jednoduše předáte třídě Library instanci této třídy: use cz\k1886\example\DebuggerVoid; use cz\k1886\example\Library; $debugger = new DebuggerVoid(); $library = new Library($debugger);
Toto je možné díky tomu, že knihovna nemusí vědět, jakým způsobem se předaná hlášení zpracovávají.
43
K1886.indd 43
18.1.2012 8:25:20
Část I Úvod do návrhu softwaru
Shrnutí Na předchozích stranách jste dokázali krok za krokem převést neflexibilní řešení, které bylo založené především na použití „Copy & Paste“, na flexibilní a jednoduše použitelný systém. Ladicí kód nemusí používat jen třída Library, ale bez problémů jej mohou využívat i jiné třídy aplikace, přičemž stačí inicializovat jen skutečně použité třídy, čímž výrazně vzroste výkon aplikace. Použití tříd není omezené jen na příklad knihovny, ale může být přenesené na každou z vašich dalších aplikací. Docílili jste zde maximální znovupoužitelnosti kódu a seznámili jste se s nejdůležitějšími pravidly vývoje softwaru, které můžete přenést i do ostatních částí svých aplikací. Kromě toho jste zde použili i jeden návrhový vzor. Tento návrhový vzor se nazývá strategie (angl. Strategy) a v této kapitole jste jej použili pro delegování zpracování hlášení na objekt třídy implementující rozhraní Debugger. V následující tabulce najdete seznam návrhových vzorů probíraných v této knize ještě předtím, než se jimi budete blíže zabývat: Název vzoru
Cíl vzoru
Abstract Factory (Abstraktní továrna) Vytváří rodiny příbuzných objektů. Builder (Stavitel)
Odděluje tvorbu komplexních objektů od jejich reprezentace.
Factory Method (Tovární metoda)
Deleguje vytváření objektů na potomky.
Prototype (Prototyp)
Vytváří objekty kopírováním prototypového objektu.
Singleton (Jedináček)
Zabezpečuje, že existuje jen jedna instance určité třídy.
Adapter (Adaptér)
Upraví rozhraní na rozhraní očekávané klientem.
Bridge (Most)
Oddělí rozhraní třídy od její vlastní implementace, při čemž lze obě nezávisle na sobě změnit.
Composite (Strom)
Spojuje více objektů do stromové struktury, kterou lze použít jako jeden objekt.
Decorator (Dekorátor)
Rozšiřuje objekty za běhu programu o novou funkčnost.
Facade (Fasáda)
Nabízí abstraktní rozhraní, které zjednodušuje používání určitého subsystému.
Flyweight (Muší váha)
Umožňuje společné použití malých objektů.
44
K1886.indd 44
18.1.2012 8:25:20
Kapitola 1 Základní pravidla při návrhu softwaru Název vzoru
Cíl vzoru
Proxy (Zástupce)
Kontroluje přístup k objektu pomocí zástupce: - přístup k objektu na jiném serveru (Remote Proxy), - vytvoření objektu až v okamžiku potřeby (Virtual Proxy), - vykonávání administrativních úloh (Secure Proxy).
Chain of Responsibility (Zřetězení Umožňuje odeslat požadavek řetězu objektů. Zřezodpovědnosti) tězené objekty samy rozhodnou, který z nich jej zpracuje. Command (Příkaz)
Zapouzdřuje požadavek jako objekt.
Interpreter (Interpret)
Definuje gramatické pravidla a určuje způsob jejich interpretace.
Iterator (Iterátor)
Umožňuje sekvenční přístup k prvkům objektu bez znalosti jeho implementace.
Mediator (Prostředník)
Zajišťuje komunikaci mezi dvěma objekty, které nemusí být v přímé interakci a znát poskytované metody.
Memento (Memento)
Zachytává a uchovává vnitřní stav objektu bez porušení jeho zapouzdření.
Observer (Pozorovatel)
Umožňuje šíření událostí, které nastaly v jednom objektu, na všechny na něm závislé objekty.
State (Stav)
Umožňuje změnit chování objektu při změně jeho vnitřního stavu.
Strategy (Strategie)
Definuje rodinu algoritmů, které jsou navzájem zaměnitelné.
Template method (Šablonová meto- Definuje kroky určitého algoritmu a přenechává da) jejich implementaci svým potomkům. Visitor (Návštěvník)
Přidává do objektové struktury novou funkčnost a zapouzdří ji do třídy.
45
K1886.indd 45
18.1.2012 8:25:20
K1886.indd 46
18.1.2012 8:25:20
Kapitola 1 Základní pravidla při návrhu softwaru
ČÁST II
Tvořivé vzory
47
K1886.indd 47
18.1.2012 8:25:20
Část II Tvořivé vzory
Potom, co jste se seznámili s pravidly pro návrh softwaru, dozvíte se v následujících kapitolách, jak vypadá použití jednotlivých návrhových vzorů v praxi. Návrhové vzory představené v této části se používají k vytváření objektů. Možná si říkáte: K tomu se přece v jazyce PHP používá operátor new. Na co jsou potom vzory dobré? Samozřejmě, že zčásti máte pravdu: instance objektů se vytvářejí vždy pomocí operátoru new, jinou možnost v jazyce PHP nemáte. V předchozí kapitole jste se naučili pravidlo, že vždy máte programovat vůči rozhraní, a nikdy ne vůči konkrétní implementaci. Naproti tomu jste na některých místech vytvářeli konkrétní implementace rozhraní pomocí operátoru new a tím toto pravidlo porušovali. V této kapitole se dozvíte, jak se lze vyhnout používání operátoru new ve zdrojovém kódu, a tím i závislosti na konkrétní implementaci. Pomocí návrhového vzoru Factory Method (Tovární metoda) vytvoříte objekt, aniž byste znali název třídy, a pomocí návrhového vzoru Abstract Factory (Abstraktní továrna) dokonce vytvoříte instanci celé rodiny objektů bez uvedení konkrétního názvu třídy. Pomocí návrhového vzoru Builder (Stavitel) oddělíte tvorbu komplexu objektů od jejich reprezentace a prostřednictvím návrhového vzoru Prototype (Prototyp nebo Klon) použijete stávající objekty jako vzory pro vytváření nových instancí. Na začátku kapitoly se seznámíte s návrhovým vzorem Singleton (Jedináček), pomocí něhož můžete omezit počet možných instancí třídy.
48
K1886.indd 48
18.1.2012 8:25:20
KAPITOLA 2
Návrhový vzor Singleton
Jazyk PHP 5 vám pomocí klíčových slov public, protected a private umožňuje kontrolovat, kdo získá přístup k určitým atributům a metodám třídy. Dále vám jazyk PHP 5 umožňuje omezit to, co může být při odvozování tříd přepsané, a dokonce i to, co přepsané být musí. Jedno vám však jazyk PHP neumožňuje: nedokážete totiž omezit počet instancí dané třídy. A právě k tomuto účelu slouží návrhový vzor Singleton. Návrhový vzor Singleton je jeden z nejjednodušších návrhových vzorů a zároveň i jeden z nejpoužívanějších. Možná jste jej už použili, aniž byste věděli, že se jedná o návrhový vzor.
Problém Ve vzorovém příkladu z předchozí kapitoly jste vyčlenili kód pro ladění aplikací ze třídy Library a zapouzdřili jej do nové třídy. Tím jste umožnili tomuto kódu spolupracovat i s ostatními třídami. Při vytváření instance třídy Library jednoduše předáte implementaci rozhraní Debugger konstruktoru: $debugger = new DebuggerEcho(); $library = new Library($debugger);
49
K1886.indd 49
18.1.2012 8:25:20
Část II Tvořivé vzory
Chcete-li odladit i třídy Book a Member, může příslušný kód vypadat následovně: $debuggerBook = new DebuggerEcho(); $book = new Book($debuggerBook, 'PC', 100); $debuggerMember = new DebuggerEcho(); $member = new Member($debuggerMember, 1, 'Marian Böhmer');
Tímto způsobem jste vytvořili tři instance stejné třídy a tím spotřebovali skoro třikrát tolik paměti. Knihovna však obsahuje určitě více než jednu publikaci a má registrovaných více než jednoho člena. Spotřeba paměti tedy bude se zvyšujícím se počtem publikací a členů knihovny neustále narůstat. Když se ale na objekt Debugger podíváte blíže, hned vás napadne, že není potřebné pro každou publikaci a každého člena používat samostatný objekt typu Debugger; tento objekt se nakonec používá jen na vypsání předaných hlášení a neví nic o objektu, který jej používá. Ideální by bylo, kdybyste mohli pro každou publikaci a každého člena knihovny používat stejný objekt typu Debugger – tím byste ušetřili množství paměti a vyhnuli se zbytečnému vytváření dalších objektů. S růstem knihovny budou vytvářené objekty typu Debugger na stále nových místech, a nikdy tak nebudete mít jistotu, který z nich byl vytvořený jak první. V takovém případě potřebujete k objektu typu Debugger centrální přístupový bod.
Účel návrhového vzoru Tohoto centrálního přístupového bodu docílíte pomocí návrhového vzoru Singleton. Návrhový vzor Singleton zajistí, že z určité třídy může existovat nejvíce jedna instance, a poskytne k ní globální přístupový bod. Pro aplikování tohoto vzoru na výše popsaný problém je nutné vykonat následující kroky: 1. Poskytnout centrální bod pro přístup k instanci třídy Debugger. 2. Tento centrální přístupový bod musí vždy nabízet přístup ke stejnému
objektu nezávisle na počtu volaní. 3. Zabránit možnosti vytvoření další instance třídy.
50
K1886.indd 50
18.1.2012 8:25:20
Kapitola 2 Návrhový vzor Singleton
Implementace Vytvoříte-li v kódu objekt typu Debugger na místě, kde jej chcete použít, nemáte žádnou možnost zjistit, zda už daný objekt existuje. Pro vytvoření instance objektu na centrálním místě přesuňte tento kód do nové metody. Protože k vytvoření objektu nejsou nutné žádné další informace, použijte k tomu statickou metodu, tj. metodu třídy, kterou lze zavolat bez nutnosti vytvoření instance této třídy: namespace cz\k1886\debuggers; class DebuggerEcho implements Debugger { public static function getInstance() { $debugger = new self(); return $debugger; } public function debug($message) { print $message . "\n"; } }
Ve statické metodě getInstance() vytvoříte novou instanci třídy DebuggerEcho, kterou následně vrátíte. Místo vytvoření objektu typu Debugger pomocí operátoru new můžete nyní k tomuto účelu použít novou metodu: use cz\k1886\debuggers\DebuggerEcho; $debugger1 = DebuggerEcho::getInstance(); $debugger1->debug('Lorem ipsum dolor sit amet.'); $debugger2 = DebuggerEcho::getInstance(); $debugger2->debug('Proin fringilla bibendum sagittis.'); if ($debugger1 === $debugger2) { print '$debugger1 === $debugger2'; } else { print '$debugger1 !== $debugger2'; }
V tomto příkladu objekt nevytváříme přímo v místě, kde jej potřebujeme, nicméně počet vytvořených objektů se nezměnil, protože při každém volání metody getInstance() se vytvoří nový objekt typu Debugger. To potvrzuje i výpis z příkladu: 51
K1886.indd 51
18.1.2012 8:25:21
Část II Tvořivé vzory Lorem ipsum dolor sit amet. Proin fringilla bibendum sagittis. $debugger1 !== $debugger2
Z toho vyplývá, že metoda getInstance() by měla obsahovat více logiky a při každém volaní provést následující kroky: 1. Při volání ověřit, zda už existuje instance třídy. 2. Pokud ne, vytvořit instanci třídy pomocí operátoru new a uložit ji. 3. Vrátit uloženou instanci.
Aby bylo možné použít stejný objekt vícekrát, musí být uložený v rámci metody getInstance(). Vzhledem k tomu, že tato metoda je definovaná jako statická, musí být atribut, který bude uchovávat instanci dané třídy, také definovaný jako statický: namespace cz\k1886\debuggers; class DebuggerEcho implements Debugger { private static $instance = null; public static function getInstance() { if (null == self::$instance) { self::$instance = new self(); } return self::$instance; } public function debug($message) { print $message . "\n"; } }
Třídu DebuggerEcho jste v tomto kroku doplnili o statický atribut $instance. Metoda getInstance() při jejím volání ověří, zda $instance již obsahuje instanci třídy. Pokud ne, vytvoří se nová instance, která se přiřadí do atributu a kterou následně metoda vrátí. Při opětovném provedení testovacího skriptu již docílíme požadovaného efektu: Lorem ipsum dolor sit amet. Proin fringilla bibendum sagittis. $debugger1 === $debugger2
52
K1886.indd 52
18.1.2012 8:25:21
Kapitola 2 Návrhový vzor Singleton
V této chvíli je úplně jedno, jak často se bude metoda getInstance() volat, aplikace bude používat vždy stejný objekt, čímž se ušetří systémové prostředky.
Skryté problémy Návrhový vzor Singleton s sebou nese i pár skrytých problémů. Co se stane, pokud vaši týmoví kolegové nebudou vědět, že pro získání objektu typu Debugger mají použít metodu getInstance()? Vytvoří jej klasickým způsobem: use cz\k1886\debuggers\DebuggerEcho; // váš objekt debugger1 $debugger1 = DebuggerEcho::getInstance(); $debugger1->debug('Lorem ipsum dolor sit amet.'); // objekt debugger2 vašeho kolegy $debugger2 = new DebuggerEcho(); $debugger2->debug('Proin fringilla bibendum sagittis.'); if ($debugger1 === $debugger2) { print '$debugger1 === $debugger2'; } else { print '$debugger1 !== $debugger2'; }
Vytvořené objekty znovu nejsou stejné a znovu dochází k plýtvání pamětí. Musíte tedy svým týmovým kolegům zakázat možnost vytvářet instance samostatně a tím je donutit používat metodu getInstance(). Řešení tohoto problému je velmi jednoduché. Použití konstruktoru mimo třídu zakážete tak, že jej deklarujete jako protected: namespace cz\k1886\debuggers; class DebuggerEcho implements Debugger { // ... statický atribut a metoda getInstance() protected function __construct() {} public function debug($message) { print $message . "\n"; } }
53
K1886.indd 53
18.1.2012 8:25:21
Část II Tvořivé vzory
Pokud se v této chvíli pokusí některý z vašich kolegů vytvořit novou instanci objektu typu Debugger, zareaguje na to interpret jazyka PHP následující chybou: Fatal error: Call to protected cz\k1886\debuggers\DebuggerEcho:: __construct() from invalid context in test.php on line 10
Váš kolega bude muset místo toho použít k získání objektu metodu getInstance(). Vynalézaví kolegové, kteří přesto budou chtít vytvořit novou instanci objektu, by mohli přijít na myšlenku klonovat objekt, který získají pomocí metody getInstance(): use cz\k1886\debuggers\DebuggerEcho; $debugger1 = DebuggerEcho::getInstance(); $debugger1->debug('Lorem ipsum dolor sit amet.'); $debugger2 = clone $debugger1; $debugger2->debug('Proin fringilla bibendum sagittis.'); if ($debugger1 === $debugger2) { print '$debugger1 === $debugger2'; } else { print '$debugger1 !== $debugger2'; }
Po provedení tohoto testovacího skriptu se znovu ukáže, že se používají dvě instance třídy. I když vám nikdo takovéto kolegy nepřeje, nemůžete si být se současným řešením problému nikdy jistí, že skutečně existuje jen jedna instance třídy. Z toho vyplývá, že jste požadavek číslo tři zatím nesplnili na 100 %. Naštěstí vám jazyk PHP také umožňuje změnit chování třídy při klonování, k čemuž stačí implementovat metodu s názvem __clone(). Klonování objektu zakážete tím, že zakážete volání metody __clone() mimo danou třídu: namespace cz\k1886\debuggers; class DebuggerEcho implements Debugger { // ... statický atribut a metoda getInstance() protected function __construct() {} private function __clone() {}
54
K1886.indd 54
18.1.2012 8:25:21
Kapitola 2 Návrhový vzor Singleton public function debug($message) { print $message . "\n"; } }
Při opakovaném pokusu klonovat objekt typu Debugger obdrží váš kolega chybové hlášení, které mu oznamuje, že klonování objektu je zakázané: Fatal error: Call to private cz\k1886\debuggers\DebuggerEcho:: __clone() from context '' in test.php on line 10
Tím jste zabezpečili, že vždy může existovat maximálně jeden objekt třídy DebuggerEcho, a implementovali jste tudíž svého prvého jedináčka. POZNÁMKA Možná se divíte, proč byl konstruktor deklarovaný jako protected a metoda __ clone() jako private. Důvodem jsou možnosti, které poskytuje dědění. V případě konstruktoru chcete umožnit třídám, které jsou potomky třídy DebuggerEcho, aby jej mohly přepsat. Takto deklarovaný konstruktor není sice možné použít mimo třídu, avšak je možné jej použít na jeho obvyklé úlohy. V případě metody __clone() toto není nutné a má být jen zamezení její používání.
Definice Návrhový vzor Singleton zajistí, že z určité třídy může existovat nejvíce jedna instance, a poskytne k ní globální přístupový bod. Pro implementaci tohoto návrhového vzoru jsou vždy nutné následující čtyři kroky: 1. Definovat statický atribut, který obsahuje instanci objektu třídy. 2. Implementovat statickou metodu, která vrací objekt z bodu 1 a v případě,
že neexistuje, jej vytvoří. 3. Zabránit vytvoření nové instance pomocí operátoru new tak, že bude konstruktor deklarovaný jako protected. 4. Zajistit, aby objekt třídy nemohl být klonovaný, k čemuž stačí metodu __ clone() deklarovat jako private. Na obrázku 2.1 je zobrazený diagram jazyka UML pro obecný návrhový vzor Singleton a na obrázku 2.2 je jeho konkrétní implementace. 55
K1886.indd 55
18.1.2012 8:25:21
Část II Tvořivé vzory Singleton -instance : Singleton -attribute1 -attribute2 #Singleton() +getInstance() : Singleton +method1() +method2()
Obrázek 2.1: UML diagram návrhového vzoru Singleton
DebuggerEcho -instance : DebuggerEcho #DebuggerEcho() +getInstance() : DebuggerEcho -__clone()
Obrázek 2.2: UML diagram konkrétní implementace
Shrnutí Použití návrhového vzoru Singleton má pro vaši aplikaci následující dopady:
Můžete přesně kontrolovat způsob, jakým se přistupuje ke třídě implementující tento návrhový vzor. Z dané třídy existuje vždy maximálně jedna instance. Druhou instanci třídy při použití tohoto návrhového vzoru není možné vytvořit. Existují však různé modifikace tohoto návrhového vzoru, u nichž je povolena i více než jedna instance. Návrhový vzor Singleton vám umožňuje zredukovat počet globálních proměnných nebo je ze zdrojového kódu úplně odstranit. Místo ukládání globálních instancí tříd v globálním oboru názvů můžete přistupovat k jejich instancím přes statické metody, čímž udržujete globální obor názvů volný.
Návrhový vzor Singleton je jeden z nejjednodušších návrhových vzorů, což může být i důvodem, proč je využívaný i v situacích, kdy to není nutné. Před jeho nasazením byste se proto měli zamyslet, zda skutečně povolíte jen jednu instanci třídy, nebo zda je užitečné současné využití více instancí a ve skutečnosti potřebujete jen centrální přístupový bod k objektu. V posledním případě byste k tomu mohli využít i návrhový vzor Registry (Registr). 56
K1886.indd 56
18.1.2012 8:25:21
Kapitola 2 Návrhový vzor Singleton
Další využití Kromě objektu typu Debugger existují i další možnosti, kde lze využít návrhový vzor Singleton. Často se například využívá při poskytování centrálního přístupového bodu ke konfiguraci aplikace. Při existenci jen jednoho objektu, který se stará o ukládání konfigurace, získáte hned dvě výhody:
Pokud se změní nějaká konfigurační hodnota komponenty, platí tato změna okamžitě pro všechny komponenty. Konfigurační soubory stačí načíst jen jednou, což se kladně projeví na spotřebě dostupných zdrojů.
Stejně často se návrhový vzor Singleton používá, pokud aplikace přistupuje k externím zdrojům, jako jsou například databáze. Použitím jednoho centrálního objektu, který se stará o přístup k databázi, stačí otevřít jen jedno připojení, které si pak jednotlivé komponenty sdílejí.
Variace návrhového vzoru Singleton Možná jste si říkali, jak se dá použít návrhový Singleton pro objekty, které mají být z vnějšku parametrizované. Chcete-li například znovu změnit způsob ladění aplikace na protokolování do souboru, avšak nezapisovat všechna hlášení do jednoho souboru, lze použít pro různé komponenty různé soubory. Objekt typu Debugger pro protokolování do souboru by mohl vypadat následovně: namespace cz\k1886\debuggers; class DebuggerLog implements Debugger { protected $logfile = null; public function __construct($logfile) { $this->logfile = $logfile; } public function debug($message) { error_log($message . "\n", 3, $this->logfile); } }
57
K1886.indd 57
18.1.2012 8:25:21
Část II Tvořivé vzory
Při vytváření instance třídy DebuggerLog se konstruktoru předá název souboru, do něhož se mají předaná hlášení zapisovat. Díky tomu lze zapisovat hlášení do různých souborů. use cz\k1886\debuggers\DebuggerLog; $debugger1 = new DebuggerLog('./debugger1.log'); $debugger1->debug('Lorem ipsum dolor sit amet.'); $debugger2 = new DebuggerLog('./debugger2.log'); $debugger2->debug('Proin fringilla bibendum sagittis.');
Jenže v tomto případě již nemůžete využít vzor Singleton, protože chcete povolit více než jednu instanci. To ale chcete umožnit jen v případě, kdy různé instance zapisují do různých souborů. V případě zápisu do stejného protokolovacího souboru by se měla použít stejná instance. Jednoduchou změnou implementace návrhového vzoru Singleton je i toto možné. Místo jedné globální instance musíte do statického atributu uložit instanci třídy podle použitého souboru. Tento atribut tedy vyměníte za pole: namespace cz\k1886\debuggers; class DebuggerLog implements Debugger { protected $logfile = null; private static $instances = array(); public static function getInstance($logfile) { if (!isset(self::$instances[$logfile])) { self::$instances[$logfile] = new self($logfile); } return self::$instances[$logfile]; } protected function __construct($logfile) { $this->logfile = $logfile; } private function __clone() {} public function debug($message) { error_log($message . "\n", 3, $this->logfile); } }
58
K1886.indd 58
18.1.2012 8:25:21
Kapitola 2 Návrhový vzor Singleton
Nyní už stačí jen předat název protokolovacího souboru metodě getInstance(). Následující příklad vytvoří dvě instance třídy DebuggerLog, přestože se metoda getInstance() bude volat třikrát: use cz\k1886\debuggers\DebuggerLog; $debugger1 = DebuggerLog::getInstance('./debugger1.log'); $debugger1->debug('Lorem ipsum dolor sit amet.'); $debugger2 = DebuggerLog::getInstance('./debugger2.log'); $debugger2->debug('Proin fringilla bibendum sagittis.'); $debugger3 = DebuggerLog::getInstance('./debugger1.log'); $debugger3->debug('Mauris vitae augue dolor.');
První a poslední volání vrátily stejný objekt, o čemž se můžete přesvědčit i na obrázku 2.3.
Obrázek 2.3: Pole instancí upravené implementace návrhového vzoru Singleton
Sice jste zde neimplementovali vzor Singleton v klasické podobě, nicméně s touto variací se budete v praxi střetávat velmi často.
59
K1886.indd 59
18.1.2012 8:25:22
K1886.indd 60
18.1.2012 8:25:22
KAPITOLA 3
Návrhový vzor Factory Method
V dosud probíraných kapitolách jste vytvářeli nové publikace vždy pomocí operátoru new. Odhlédneme-li od toho, že to v praxi nefunguje tak jednoduše, vždy jste svůj kód spojovali s konkrétní implementací. A právě tomu byste měli zabránit, protože tím ztížíte možnost implementace nových tříd, budete-li chtít vložit nové typy publikací. Když vytváříte objekty pomocí operátoru new přímo v místě, kde je potřebujete, máte tvorbu jejich instancí roztroušenou po celém kódu aplikace. Při změně tvorby instancí, ať už kvůli přidání nových argumentů do konstruktoru nebo kvůli vložení nových tříd, je zapotřebí upravit každý soubor, který vytváří dané objekty. S rostoucím počtem míst, která je nutné takto změnit, stoupá i počet chyb, jichž se lze při tom dopustit. Lepší by bylo, kdybyste měli jeden objekt nebo třídu, které by publikace vytvářely za vás, jakousi „továrnu na publikace“. A právě k tomuto účelu slouží návrhový vzor Factory Method (Tovární metoda).
Problém K usnadnění integrace nových typů publikací chcete rozšířit aplikaci o vydavatelství. Tato vydavatelství by znala všechny informace o tvorbě nových publika-
61
K1886.indd 61
18.1.2012 8:25:22
Část II Tvořivé vzory
cí a zároveň by vykonávala úlohy, které chcete provést vždy před tím, než nová publikace opustí tiskařskou halu. Tyto úlohy by mohly být například následující:
Vytvoření nového objektu, přiřazení určité kategorie a počet stran. Otevření publikace, nalistování určité strany a následně její zavření, čímž se ověří, že je vše v pořádku.
Přirozeně vám nebude stačit jen jedno vydavatelství, protože knihovna nepůjčuje jen knihy, ale také odborné časopisy a případně i jiné publikace. Jednotlivá vydavatelství se přitom specializují jen na jeden druh publikací, a proto budete potřebovat pro každý typ samostatné vydavatelství. Tato vydavatelství se od sebe odlišují jen ve způsobu výroby jednotlivých publikací, všichni ale mají ověřit správnost vyhotovení podle jednotného vzoru.
Účel návrhového vzoru Vydavatelství implementujeme pomocí návrhového vzoru Factory Method. Účel tohoto návrhového vzoru se výborně hodí pro vyřešení výše zadané úlohy. Návrhový vzor Factory Method definuje rozhraní pro vytváření objektů, při čemž vlastní tvorbu instancí přenechává svým potomkům. Potomci rozhodují, které konkrétní implementace se mají použít. Pro tuto konkrétní aplikaci to znamená následující: 1. Implementovat třídu pro vydavatelství, která definuje nezbytné rozhraní. 2. Do této třídy vložit kód, který je stejný pro všechna vydavatelství. 3. Následně implementovat potomky této třídy, kteří se budou starat o vytvá-
ření různých typů publikací.
Implementace Začněte vytvořením vydavatelství tak, že implementujete abstraktní třídu, která bude fungovat jako základ pro konkrétní vydavatelství. Třída AbstractPublisher vám má nejdříve umožnit předávat knihovně libovolné publikace. Stejně jako ve skutečném světě budou i zde různá vydavatelství publikovat i různé typy publikací, a tak budete mít na konci vydavatelství na publikace o počítačích, historii nebo medicíně. Z toho vyplývá, že každé vydavatelství potřebuje označení, tj. potřebujete atribut, který bude toto označení obsahovat. Na přiřazení tohoto označení do atributu využijete konstruktor třídy: 62
K1886.indd 62
18.1.2012 8:25:22
Kapitola 3 Návrhový vzor Factory Method namespace cz\k1886\publishers; abstract class AbstractPublisher { protected $category; public function __construct($category) { $this->category = $category; } }
Co této třídě zatím chybí, je metoda, která umožní prodat publikaci. K tomuto účelu implementujte metodu sellPublication(). Této metodě nejdříve předáte počet stran, které má prodávaná publikace obsahovat. Metoda sellPublication() má vytvořit novou publikaci, otevřít ji, přejít na určitou stranu a zavřít. Tato metoda však nemá vědět, jak je daná publikace vytvořená, nakonec kniha je tvořená jiným způsobem než časopisy či noviny. Místo toho k tomu použije další metodu s názvem createPublication(). Tato metoda má vytvořit nový objekt, který implementuje rozhraní Publication, a následně jej vrátit. Díky implementaci tohoto rozhraní si můžete být jistí, že vrácený objekt obsahuje metody open(), setPageNumber() a close(). namespace cz\k1886\publishers; abstract class AbstractPublisher { protected $category; public function __construct($category) { $this->category = $category; } public function sellPublication($pageCount) { $publication = $this->createPublication($pageCount); $publication->open(); $page = mt_rand(1, $pageCount); $publication->setPageNumber($page); $publication->close(); return $publication; } abstract protected function createPublication($pageCount); }
63
K1886.indd 63
18.1.2012 8:25:22
Část II Tvořivé vzory
Metodu createPublication() jsme deklarovali jako abstraktní, protože základní třída nemůže vědět, jaký je postup při tvorbě určitého druhu publikace. Proto byla i třída AbstractPublisher deklarovaná jako abstraktní, takže nelze vytvářet její instance. Pro vytvoření objektu je nutné vytvořit třídu, která je odvozená od této třídy. Takový potomek musí jen implementovat abstraktní metodu createPublication() a vytvořit v ní instancí nové publikace. Pro knihu může taková třída vypa-
dat následovně: namespace cz\k1886\publishers; use cz\k1886\publications\Book; class BookPublisher extends AbstractPublisher { protected function createPublication($pageCount) { $publication = new Book($this->category, $pageCount); return $publication; } }
V metodě createPublication() vytvoříte instanci třídy Book a předáte jí název kategorie a počet stran. Jednoduchý testovací skript může vypadat takto: use cz\k1886\publishers\BookPublisher; $bookPublisher = new BookPublisher('PC'); $book = $bookPublisher->sellPublication(100); print "Zakoupena nová publikace: \n"; printf("Typ: %s \n", get_class($book)); printf("Kategorie: %s \n", $book->getCategory()); printf("Počet stran: %d \n", $book->getPageCount());
Po vytvoření instance vydavatelství se použije metoda sellPublication(), která se stará o vytvoření nové publikace. Přitom není nutné uvádět, z které třídy má být instance nové publikace vytvořená. Tento údaj je zapouzdřený ve třídě BookPublisher. Po provedení testovacího skriptu získáte následující výpis: Zakoupena nová publikace: Typ: cz\k1886\publications\Book Kategorie: PC Počet stran: 100
64
K1886.indd 64
18.1.2012 8:25:22
Kapitola 3 Návrhový vzor Factory Method
Jak asi očekáváte, je nově zakoupená publikace instancí třídy Book. Analogicky k tomu můžete implementovat další vydavatelství, které vydává odborné časopisy. Stačí jen vytvořit dalšího potomka třídy AbstractPublisher: namespace cz\k1886\publishers; use cz\k1886\publications\Journal; class JournalPublisher extends AbstractPublisher { protected function createPublication($pageCount) { $publication = new Journal($this->category, $pageCount); return $publication; } }
V této třídě se při tvorbě publikace vytvoří instance třídy Journal. V obou třídách se implementace metody stará jen o vytvoření nových objektů a neví nic o tom, jak se s nimi bude dále nakládat. Testovací skript je velmi podobný tomu předchozímu: use cz\k1886\publishers\JournalPublisher; $journalPublisher = new JournalPublisher('medicína'); $journal = $journalPublisher->sellPublication(20); print "Zakoupena nová publikace: \n"; printf("Typ: %s \n", get_class($journal)); printf("Kategorie: %s \n", $journal->getCategory()); printf("Počet stran: %d \n", $journal->getPageCount());
Další vydavatelství, jež mohou k produkci publikací využívat i komplexnější kód, můžete velmi jednoduše implementovat a přidat do aplikace. Tím jste z kódu odstranili vytváření objektů publikací a zapouzdřili jej za rozhraní. Používáte vždy jen rozhraní třídy AbstractPublisher a nemusíte vědět, z jakých tříd se přitom vytvářejí instance a jaký kód je k tomu potřebný.
Definice Návrhový vzor Factory Method definuje rozhraní pro vytváření objektů, při čemž vlastní tvorbu instancí přenechává svým potomkům. Potomci rozhodují, které konkrétní implementace se mají použít. K dosažení tohoto cíle je nutné provést následující kroky: 65
K1886.indd 65
18.1.2012 8:25:22
Část II Tvořivé vzory 1. Implementovat abstraktní třídu, v níž je deklarovaná jedna nebo více
abstraktních metod předurčených k vytváření objektů. 2. Přidat do této třídy další metody, které obsahují logiku identickou u všech konkrétních implementací. V těchto metodách už můžete přistupovat k abstraktní metodě. 3. Vytvořit libovolný počet konkrétních tříd odvozených od abstraktní třídy, v nichž se do abstraktní metody vloží různé implementace. 4. Na tvorbu instancí skutečných objektů a oddělení aplikačního kódu od konkrétních implementací využít potomky třídy AbstractPublisher. Obrázek 3.1 zobrazuje vztahy mezi jednotlivými aktéry tohoto návrhového vzoru. <
> Publication
ConcreteProduct
Creator +factoryMethod() : Publication +methodA()
ConcreteCreator +factoryMethod() : Publication
Obrázek 3.1: Diagram jazyka UML pro návrhový vzor Factory Method
Následně je na obrázku 3.2 konkretizace tohoto návrhového vzoru podle příkladu z této kapitoly. AbstractPublisher #name +AbstractPublisher(name) #createPublication(pageCount) : Publication +sellPublication(pageCount) : Publication
<> Publication
Journal
Book
BookPublisher #createPublication(pageCount) : Publication
JournalPublisher #createPublication(pageCount) : Publication
Obrázek 3.2: Diagram jazyka UML pro konkrétní implementaci
Shrnutí Díky návrhovému vzoru Factory Method lze v kódu frameworku vyvíjet stále vůči rozhraním a konkrétní implementace těchto rozhraní mohou zůstat mimo 66
K1886.indd 66
18.1.2012 8:25:22
Kapitola 3 Návrhový vzor Factory Method
tento kódu. Z tohoto důvodu je tento vzor jedním z návrhových vzorů, na které nejčastěji narazíte v různých frameworcích s otevřeným zdrojovým kódem. Návrhový vzor Factory Method vyžaduje k vytvoření konkrétní implementace odvození další třídy. Často je nutné tuto třídu vytvořit jen za jedním účelem, což vede k vyšší komplexnosti aplikace. I přes tuto zápornou vlastnost převládají pozitiva tohoto návrhového vzoru, protože není problém vložit do aplikačního kódu nové specializované třídy (jako ve výše uvedeném příkladu třídu Journal). V další části kapitoly se dozvíte, jak se dá pomocí malé úpravy tohoto návrhového vzoru odstranit i toto negativum.
Další využití Schování tvorby instancí tříd pomocí tohoto návrhového vzoru je velká pomůcka při vývoji objektově orientovaných architektur, avšak jeho používání často vede – tak jako v této kapitole – k velkému počtu tříd, které nabízejí jen jednu metodu, jež se opět skládá jen ze dvou řádků zdrojového kódu. Je tak nutné znovu vytvářet instance těchto továren, čímž při jejich tvorbě svazujete na určitých místech svůj kód s konkrétní implementací. Na odstranění tohoto negativa se v mnohých aplikacích používá statická tovární metoda. Jak už její název napovídá, jedná se o metodu vytvářející objekt, která se volá staticky. Při použití statické metody odpadá možnost tvorby potomků a při volání metody je nutné uvádět název požadované třídy. Místo toho je tato tovární metoda parametrizovaná a na základě předaného parametru je rozhodnuté, jaká konkrétní třída se má použít k vytvoření instance. Dobrou aplikací statické tovární metody je znovu vytváření instancí třídy Debugger. Nyní tedy implementujte novou třídu s názvem DebuggerFactory se statickou metodou createDebugger(), která přijímá typ instance implementující rozhraní Debugger, která se má vytvořit: namespace cz\k1886\debuggers; class DebuggerFactory { public static function createDebugger($type) { switch (strtolower($type)) { case 'log' : require_once 'DebuggerLog.php'; return new DebuggerLog(); break;
67
K1886.indd 67
18.1.2012 8:25:23
Část II Tvořivé vzory case 'echo' : require_once 'DebuggerEcho.php'; return new DebuggerEcho(); break; default : throw new UnknownDebuggerException(); } } }
V této metodě rozhodujeme pomocí příkazu switch a parametru $type, která třída se má použít k vytvoření instance. K tomu nejdříve načtete příslušný soubor s třídou a následně vytvoříme novou instanci. Při předání neznámého typu se vyvolá výjimka. Testovací skript k tomuto příkladu by mohl vypadat následovně: use cz\k1886\debuggers\DebuggerFactory; define ('DEBUG_MODE', 'log'); $debugger = DebuggerFactory::createDebugger(DEBUG_MODE); $debugger->debug('Lorem ipsum dolor sit amet.');
Tento kód nemusí vědět nic o konkrétní implementaci objektu typu Debugger – stačí jen zavolat metodu, která vrátí požadovaný objekt.
Statická Factory Method a Singleton V předchozí kapitole jste se dozvěděli, že u bezstavových objektů, jako je například objekt typu Debugger, je rozumné postarat se pomocí návrhového vzoru Singleton, aby nedošlo k vytvoření mnoha nepotřebných instancí. Zavedením statické tovární metody jste však tuto výhodu zaměnili za jinou, která spočívá v tom, že nemusíte znát názvy tříd různých implementací „ladicích“ objektů. Lepší by ale bylo, kdybyste mohli využívat oboje. I toto je možné pomocí statické tovární metody, která musí koneckonců jen vracet objekty. Nikde není definované, že objekty je nutné vytvářet pomocí operátoru new. Zkombinujte proto tovární metodu s implementací návrhového vzoru Singleton. namespace cz\k1886\debuggers; class DebuggerFactory { public static function createDebugger($type) {
68
K1886.indd 68
18.1.2012 8:25:23
Kapitola 3 Návrhový vzor Factory Method switch (strtolower($type)) { case 'log' : return DebuggerLog::getInstance(); break; case 'echo' : return DebuggerEcho::getInstance(); break; default : throw new UnknownDebuggerException(); } } }
Místo operátoru new nyní používáme metodu getInstance(), která vždy vrátí stejnou instanci. Díky této úpravě máte spolu návrhové vzory Factory Method a Singleton i jejich výhody a nemuseli jste přitom ani jednou změnit stávající zdrojový kód.
69
K1886.indd 69
18.1.2012 8:25:23