Tervezési minták – Adapter pattern
Kicsit megtörjük most a sort és a létrehozási minták helyett (amik közül a jómunkásember Builder
következne) egy másik csoportból fogunk elővenni egyet, mégpedig a struktúrális minták közül, az adapter pattern-t. Ezen mintának a lényege, hogy két inkompatibilis interfész között hidat képez. Minderre egy különálló osztályt fogunk használni, aminek az a feladata, hogy a két független vagy éppen inkompatibilis osztály funkcionalitását kombinálja.
A fenti kép is jó például szolgálhat. A laptopunk akkumulátorát és a konnektort egy adapter köti össze, ami a két összeférhetetlen “interfészt” fogja működőképessé tenni.
Ellenben írjunk egy példát, az mindig segít. Tegyük fel, hogy oldalunk egy külső API-t használ és ez a szolgáltatás lassú/netán nem mindig elérhető, így a tartalmát szeretnénk gyorsítótárazni, na meg az API kulcsunkat se akarjuk mindenáron pörgetni, nehogy túllépjük a limitet. Ezt a gyorsítótárazást először balga módon a fájlrendszerbe tesszük.
1
2
3
4
5
6
7
8
9
10
11
class FileSystemCache {
public function cacheToFile($key, $contentToCache) {
// itt megy a magic: file_put_contents vagy hasonló barbárkodás :D
}
public function getFromFile($key) {} // ha már beletettük, szedjük is ki azt
public function touch($key) {} // már nagyon profinak érezzük magunkat, ugye?:)
}
$cache = new FileSystemCache();
$cache->cacheToFile('rossz','megoldás');
A dolog rendkívül szimpla, példányosítjuk az osztályunkat, meghívjuk annak cacheToFile metódusát és máris felpörög a szerverünk winchestere (amit amúgy nem szeretnénk, de erről egy másik cikkben lesz szó).
Telnek múlnak a napok, mire a kedves ügyfél megjegyzi, hogy bizony ez a folyamat lassú. Nem lehetne gyorsítani a dolgon? Utánajárunk és megdöbbenve tapasztaljuk, hogy a memória pár százszor gyorsabban dolgozik, mint a HDD (és az SSD-nek is odaver továbbra is), így inkább a memóriában kéne tároljuk a dolgot. Egyik kollégánk rögtön jön is az ötlettel, hogy biza ő nem nagyon konyít a PHP-hoz, de ha socketeket használunk, akkor neki bizony van egy key-value szervere, amit még kisovisként írt C-ben és szívesen rendelkezésünkre bocsájtja. Kapunk is az alkalmon és megírjuk rá az osztályunkat.
1
2
3
4
5
6
7
8
class CacheLayerByRandomColleague {
public function cacheToTheService($key, $value) {
// itt is történik valami socket alapú feketemágia 3:)
}
public function getFromService($key) {
// same here
}
}
A fenti funkció hasonlóképp működik, mint amit az imént leírtunk, csak a funkciók neve és azok működése más, de lényegében ugyanazokat tudja. Telnek a napok és valaki egy checkout után meglátja, hogy mit műveltünk és a fejünkhöz vágja, hogy hát erre van a MemCached és miért nem azt használjuk. Aztán akkor írunk még 1 osztályt, ami azt használja, de lehet aztán kiderül, hogy túl sok mindent tárolnánk ott és inkább több rétegben cacheljük a dolgokat, netán más megoldással csináljuk és a végén lesz 20 féle osztályunk, amit 20 féleképp lehet használni. Ha emlékszünk az előző írásomra az interface-ekről és valami bizsergést érzünk az agyalapi részen, akkor jó úton járunk.
Ha a fenti osztályok mindegyikét mi írtuk, akkor segíthetünk magunkon úgy, hogy egy közös CacheInterface-t implementálnak. Ez is egy megoldás, de ez még nem lesz adapter pattern, mivel ott különböző interface-eket hozunk össze. Készítsünk először egy-egy interfészt a két megoldáshoz, hogy lássuk a dolog lényegét.
1
2
3
4
5
6
7
interface MultiCacheInterface {
public function getFromService($key);
public function cacheToService($key, $value);
public function getFromFile($key);
public function cacheToFile($key, $value);
public function touch($key);
}
1
2
3
4
5
6
7
class FileSystemCache implements MultiCacheInterface {
// az interfész által előírt metódusok
public function cacheToService($key, $value) {} // ez nem csinál semmit
public function getFromService($key) {} // ez se
}
1
2
3
4
5
6
7
class CacheLayerByRandomColleague implements MultiCacheInterface
{
// az interfész által előírt metódusok
public function cacheToFile($key, $value) {}
public function getFromFile($key) {} // nem működnek
}
Amint látjuk az osztályaink két különböző interfészt implementálnak, ezért az adatainkat különböző metódusok elérésével érjük el. Így a két megoldás közti váltáshoz bele kell nyúlni a kódunkba, mégpedig minden egyes helyen, ahol ezekre hivatkozunk. Ennek az áthidalásához készítünk most egy adaptert:
1
2
3
4
5
interface StorageAdapter {
public function get($key);
public function set($key, $value);
public function touch($key);
}
Ugye itt csináltunk egy általános interfészt az adapterünknek.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class CacheAdapter implements StorageAdapter {
private $cacheService, $cacheType;
public function __construct($cacheType) {
$this->cacheType = $cacheType;
// ez egy nagyon egyszerű módszer lesz, meg lehet csinálni interface typehintelt constructor injectionnel is
if ($cacheType == "filesystem") {
$this->cacheService = new FileSystemCache(); // az átadott stringnek megfelelően példányosítjuk az osztályokat
}
elseif ($cacheType == "randomcolleague") {
$this->cacheService = new CacheLayerByRandomColleague
} else throw new Exception('Unsupported cache type');
}
/* Ez a megoldás egy fokkal stílusosabb
public function __construct(MultiCacheInterface $cache) { typehinteltük az interfészt, így csak olyan osztályt fogad el, ami azt implementálta
$this->cacheService = $cache; // a typehint garantálja, hogy jó osztályt kaptunk
$this->cacheType = get_class($cache); // kinyerjük az osztály nevét
} */
public function get($key) {
if($this->cacheType == 'filesystem') {
$this->cacheService->getFromFile($key);
} elseif ($this->cacheType == 'randomcolleague') {
$this->cacheService->getFromService($key);
}
}
// és a többi metódus
}
Na, kész is vagyunk, nézzük hát mit is csináltunk. Csináltunk egy interfészt a cache osztályunk számára, amit habár implementálnak, de az első megoldásnál nem feltétel ennek megléte, viszont a typehintelt konstruktor injectionnél már igen. A lényege a dolognak annyi, hogy egy réteget hoztunk létre a két osztályunk felé. A kliensnek nem kell ismernie azok interfészét, vagy épp típusát, mivel ezt a részét az adapter végzi. Ezen osztályunk tudja, hogy épp milyen cache-el van dolga és annak megfelelő metódusokat hív meg.
1
2
3
4
5
$cache = new CacheLayerByRandomColleague();
$cache->getFromService($key);
$cache2 = new FileSystemCache();
$cache2->getFromFile($key);
Amint látjuk alapesetben így érjük el a két osztályt, különböző interfészeken, de az adapterünkkel ezt megoldottuk:
1
2
3
4
$cache = new CacheAdapter("randomcolleague");
$cache2 = new CacheAdapter("filesystem");
$cache->get($key);
$cache2->get($key);
Így ha lesz egy osztály, ami a cache-ünket használja, nem kell átírnunk benne csupán egyetlen stringet és az adapterünk megteszi a többit helyettünk!