Az Abstract Factory tervezési minta

Előző cikkben a Factory mintával ismerkedtünk meg, most pedig annak egy tovább gondolt változatát az Abstract Factory-t fogjuk megvizsgálni.

A cikkben szereplő kódok megértéséért ajánlott elolvasni az előző részeket. 

Szóval Abstract Factory, a lényeg nem sokat változik, objektumokat kell gyártani, annyi kiegészítéssel, hogy az abstract osztályoknak hála már nem csak azt tudjuk változtatni, hogy melyik objektumot gyártjuk le, hanem azt is, hogy az objektumot esetleg egy másik változatban gyártjuk le. Tudom ez kissé zavaros ilyenkor, megpróbálom ismét egy példán levezetni. Maradjunk az előző sima Factory példánknál, ott ugye két html elemet (objektumot) kellett tudni legyártani, plusz lehetőség volt, hogy megmondhattuk, hogy a kívánt elem Xhtml legyen-e vagy sem. Most ezt kiegészítjük úgy, hogy különbséget teszünk HTML4 és HTML5 között (igaz itt már az Xhtml kérdést elhagyjuk), mert míg HTML4-ben egy DIV-nél használhattuk az align tulajdonságot, HTML5-ben már nem, ott ugyan is a style-ban vagy CSS-ben kell megmondani azt. Valamint a másik hasonló példa erre az IMG készítése, HTML4-ben még lehet az align tulajdonságot használni, HTML5-ben már nem.

Lássuk a példákat mivel is kellene dolgozni:

DIV elem align tulajdonság használata:

HTML4-ben: <div align="left">tartalom</div>

HTML5-ben: <div style="text-align: left;">tartalom</div>

Az IMG elem align tulajdonság használata:

HTML4-ben: <img align="right" src="http://www.dynamicart.hu/blog/templates/ptt/images/logo2.png" />

HTML5-ben: <img style="float: right;" src="http://www.dynamicart.hu/blog/templates/ptt/images/logo2.png" />

Tudom, hogy korántsem teljes ezen elemek HTML4/5 használhatóságának felsorolása, ez csak egy kiragadott példa. Viszont jól ábrázolja, hogyan lehet az Abstract Factory-val több osztályt összehangolni, úgy, hogy azok olyan objektumokat alkossanak, amiknek közös, szabott metódusai, illetve tulajdonságai vannak.

Az Absztrakt jelzőn lesz most a hangsúly, mivel 2-2 objektumot kell legyártanunk, úgy, hogy azoknak egyébként egyezniük kell működésben, pontosabban ugyan olyan elven kell működniük, és ugyan úgy megbízhatóan kell a produktumot (objektumot) előállítaniuk. Erről a PHP-ban az absztrakt osztály gondoskodik, mivel az abból származtatott osztályoknak kötelezően bírniuk kell az absztrakt ősben meghatározott metódusokkal, így kikényszeríthetjük, hogy minden osztályban meglegyen az a kötelező kód, ami a működéshez kell.

Tehát, kell két HTML4-s objektum (div és img), valamint kell 2db HTML5-s objektum is, ezek párban meg fognak egyezni (ehhez kell az abstract). Majd a Factory rész azt hívja meg, amelyiket a környezet szerint kell.

Az Abstract Factory PHP kódja:

<?php
// A htmlElem absztrakt osztálya
abstract class htmlElem{
// minden htmlElem objektumnak meglesz ez a két adattagja
protected $output = ""; // az objektum kimenete
protected $params = array(); // bejövő paraméterek
// minden osztálynak kötelező lesz ezt a 3 függvényt definiálni, így biztosítjuk, hogy egyformán működjenek
public abstract function __construct($params);
public abstract function __toString();
public abstract function build();
}
// A html4 elemek szülőosztálya
class html4Elem extends htmlElem{
// A konstruktor beállítja az alap adatokat
public function __construct($params){
$this->params = $params;
// azonnal el is készítjük a kimenetet
$this->build();
}

// string konverzió esetén a kimenetet adjuk vissza
public function __toString(){
return $this->output;
}
// a kimenet készítése
public function build(){
$this->output = "";
}
}
// html4-s DIV objektuma
class he4Div extends html4Elem {
public function build(){
$this->output = "<div ".($this->params['align']?"align='".$this->params['align']."'":"").">".$this->params['content']."</div>";
}
}
// html4-s IMG objektum
class he4Img extends html4Elem {
public function build(){
$this->output = "<img ".($this->params['align']?"align='".$this->params['align']."'":"")." src='".$this->params['src']."'>";
}
}
// A html5 elemek szülőosztálya
class html5Elem extends htmlElem{
public function __construct($params){
$this->params = $params;
$this->build();
}

public function __toString(){
return $this->output;
}
public function build(){
$this->output = "";
}
}
// html5-s DIV objektuma
class he5Div extends html5Elem {
public function build(){
$this->output = "<div ".($this->params['align']?"style='text-align: ".$this->params['align'].";'":"").">".$this->params['content']."</div>";
}
}
// html5-s IMG objektum
class he5Img extends html5Elem {
public function build(){
$this->output = "<img ".($this->params['align']?"style='float: ".$this->params['align'].";'":"")." src='".$this->params['src']."'>";
}
}

// html factory (singleton, hogy könnyen elérhető legyen, és ebből egyébként is elég egy példány)
class htmlFactory{
// a példány változó
private static $instance = null;
// a beállított Xhtml paraméter
private $htmlVer = 4;
// beállítjuk a gyárban, hogy sima vagy xhtml
private function __construct($htmlVer){
$this->htmlVer = $htmlVer;
}

// a példány elkészítése ha még nem lenne
public static function getInstance($htmlVer = 4){
if (!isset(self::$instance)){
self::$instance = new htmlFactory($htmlVer);
}
return self::$instance;
}

// factory, ő választja ki melyik objektumot gyártja le és adja nekünk vissza
public function create($tag, $params){
switch($tag){
case "div":
if ($this->htmlVer == 4)
return new he4Div($params);
else
return new he5Div($params);
break;
case "img":
if ($this->htmlVer == 4)
return new he4Img($params);
else
return new he5Img($params);
break;
default:
return false;
break;
}
}
}
// kérünk egy példányt a factory-ból
$htmler = htmlFactory::getInstance(5);
// lekérünk egy div osztályt felparaméterezve
$obj = $htmler->create('div',array("align"=>"right", "content"=>"tartalom"));
// kimenet
echo "div: ".htmlentities($obj);
echo "<br/>";
// lekérünk egy img osztályt felparaméterezve
$obj = $htmler->create('img',array("align"=>"right", "src"=>"http://www.dynamicart.hu/blog/templates/ptt/images/logo2.png"));
// kimenet
echo "img: ".htmlentities($obj);
// kimenet:
// div: <div style='text-align: right;'>tartalom</div>
// img: <img style='float: right;' src='http://www.dynamicart.hu/blog/templates/ptt/images/logo2.png'>
?>

Mint látjuk a htmlElem az absztrakt ős, belőle származik a html4Elem és a html5Elem, majd azokból a html elemek közvetlen osztályai. Ilyen egyszerű és kis osztályoknál nem biztos, hogy látszik, de megéri ez a sok öröklés, mivel az objektumok nagyon hasonlóak (szinte egyeznek), csak a kimenet előállításánál térnek el (build()). Éppen ezért két (illetve több) megoldás is létezik a jelen kódra, ugyanis a Factory részünk, mivel Singleton is egyben a példányosításkor megkapja, hogy html4 vagy 5-s verzióhoz kell majd objektumokat gyártania, illetve menetközben, hogy pontosan melyik objektumra is van szükségünk. Nos, a fenti példa kódban – picit fura módon, de a példa miatt fontosnak tartottam – van he4Div és he5Div osztály is definiálva, így a gyár kénytelen a switch-case-ben egy if-else ággal eldönteni, hogy melyiket fogja legyártani. Ez ugye az egyik megoldás.

A másik megoldás, ha a kódot több fájlra bontjuk, így kiszervezhetjük a html4Elem és html5Elem osztályokat (meg a gyermekosztályaikat is) külön fájlba. Valahogy így:

html4Elem.php:

  • class html4Elem{…} // a példakódban html4Elem
  • class heDiv extends html4Elem{…} // a példakódban he4Div
  • class heImg extends html4Elem{…}// a példakódban he4Img

és

html5Elem.php:

  • class html5Elem{…} // a példakódban html5Elem
  • class heDiv extends html5Elem{…} // a példakódban he5Div
  • class heImg extends html5Elem{…}// a példakódban he5Img

Nem csak az lesz az előnye ennek, hogy külön fájlban átláthatóbb kódot kapunk, hanem az is, hogy a gyártó részben, nem a switch-case részben kell eldönteni, hogy he4Div vagy he5Div-t gyártunk. Mivel – nem tévedésből – a két fájlban egyeznek a gyermekosztályok, így már a Factory példányosításakor csak azt a php fájlt kell include-olni, amelyiket a környezet megkívánja. A gyermekosztályok nevei megegyeznek, ezért az említett switch-case-ben, nem kell if-else ág csupán a heDiv vagy heImg objektumokat kell legyártani a kéréstől függően.

Maga a példányosítás, és a kimeneti tesztek úgy gondolom, megfelelően vannak kommentezve, illetve nem igazán térnek el az előző – Factory – cikkben tárgyalttól.

Az Abstract Factory Javascript kódja:

// html4 DIV osztály
function he4Div(params){
this.output = "";
this.params = params;
this.build = function(){
output = "<div "+((params.hasOwnProperty('align'))?"align='"+params.align+"'":"")+">"+params.content+"</div>";
return this;
};
this.toString = function(){return output;};
};
// html4 img osztály
function he4Img(params){
this.output = "";
this.params = params;
this.build = function(){
output = "<img "+((params.hasOwnProperty('align'))?"align='"+params.align+"'":"")+" src='"+params.src+"'>";
return this;
};
this.toString = function(){return output;};
};
// html5 DIV osztály
function he5Div(params){
this.output = "";
this.params = params;
this.build = function(){
output = "<div "+((params.hasOwnProperty('align'))?"style='text-align: "+params.align+"';":"")+">"+params.content+"</div>";
return this;
};
this.toString = function(){return output;};
};
// html5 img osztály
function he5Img(params){
this.output = "";
this.params = params;
this.build = function(){
output = "<img "+((params.hasOwnProperty('align'))?"style='float: "+params.align+"';":"")+" src='"+params.src+"'>";
return this;
};
this.toString = function(){return output;};
};
// a html Factory ami a html objektumokat fogja gyártani, ez egyben singleton is
var htmlFactory = (function(){
var _instance;
function init(){
// ebben tároljuk a különböző osztályokat
var _elements = {};
return {
// a Factory rész
create: function(tag, params){
// lekérjük a már regisztrált osztályt, mármint hogy van-e
var element = _elements[tag];
if (!element)
return null;
return new element(params); // egyszerűbb megoldás

// this.htmlClass = he4Div; // innentől switch-case megoldás
// switch(tag){
// case "div4":
// this.htmlClass = he4Div;
// break;
// case "div5":
// this.htmlClass = he5Div;
// break;
// case "img4":
// this.htmlClass = he4Img;
// break;
// case "img5":
// this.htmlClass = he5Img;
// break;
// default:
// break;
// };
// return new this.htmlClass(params);
},
// osztály regisztrálás (abstract function)
registerClasses: function(elem, heClass){
var obj = new heClass();
// csak azokat az elemeket fogjuk regisztrálni, amelyek megfelelnek az általunk kívánt kritériumoknak (ez az abstract szimuláció)
if (obj.hasOwnProperty('output') && obj.hasOwnProperty('params') && obj.hasOwnProperty('build') && obj.hasOwnProperty('toString')){
//_elements[elem] = true; // switch-case megoldás
_elements[elem] = heClass; // egyszerűbb megoldás
}
}
};
};
return {
getInstance: function(){
if(!_instance){
_instance = init();
}
return _instance;
}

};
})();
// készítünk egy példányt a Factoryból
var htmler = htmlFactory.getInstance();
// regisztráljuk a gyárba a kívánt osztályokat
htmler.registerClasses("div4", he4Div);
htmler.registerClasses("div5", he5Div);
htmler.registerClasses("img4", he4Img);
htmler.registerClasses("img5", he5Img);
// csinálunk egy html5-s divet
var obj = htmler.create("div5",{align:"right", content:"tartalom"});
// kiírjuk azt
// <div style='text-align: right';>tartalom</div>
console.log(obj.build().toString());
// késztünk egy html5-s img-t
obj = htmler.create("img5",{align:"right", src:"http://www.dynamicart.hu/blog/templates/ptt/images/logo2.png"});
// ezt is kiírjük
// <img style='float: right'; src='http://www.dynamicart.hu/blog/templates/ptt/images/logo2.png'>
console.log(obj.build().toString());

A Javascript példakód is hasonlít a Factory-s Javascript kódra, viszont a PHP-s megoldáshoz képest jelentős az eltérés. Mivel itt az abstract osztályokat csak szimulálni tudjuk, így picit máshogy kell megközelíteni a dolgot. Itt is létrehoztunk 4 osztályt he4Div, he5Div, he4Img és he5Img, önállóan működnek, azonban nincs olyan öröklési rendszer, mint a PHP-ban és a futtatás során sem fogja megkövetelni semmi az absztrakt függőségeket. Ezért ezt, mint már mondtam szimulálni kell a registerClasses() függvénnyel.

Mielőtt tovább mennénk, előzetesben mondom, hogy itt is két (több) megoldás van és az egyik az most ki van kommentezve. Erre a végén fogunk kitérni.

Tehát, menjünk végig a dolgon, a registerClasses() függvény annyit tesz – példányosítás után -, hogy "regisztrálja" a kívánt osztályt, amennyiben az megfelel a mi kritériumainknak. Vagyis megvizsgálja, hogy a regisztrálni kívánt osztálynak - illetve vizsgálatkor már objektumnak - meg van-e a számunkra szükséges 4 tulajdonsága (output, params, build, toString), ha egy is hiányzik, a regisztráció nem lesz sikeres, vagyis később nem tudjuk ezt az objektumot legyártani. Ugye PHP-ban eleve le sem futna a script, ha olyan osztályt írnánk, amiben egy abstract function nincs meg.

A htmlFactory, vagyis a htmler gyár create() metódusa – és akkor ez az egyszerűbb példa – lekéri a gyártani kívánt objektumhoz (tag) annak osztályát (ezt rakta el a registerClasses() a kritériumok ellenében), amennyiben az elérhető, úgy azt legyártja és visszaadja. Ha nem volt elérhető, akkor null értéket ad vissza, vagyis nem gyárt.

Készen is volnánk, de a kikommentezett megoldást is elmondom. Azért hagytam benne, mert az talán jobban mutatja – ahogy az előző Factory JS-kódban is látszott –, a Factory-ságot, csak itt igazából felesleges. Ezért úgy is kommenteztem a sorokat, hogy az //egyszerű megoldás komment (2 sor) prezentálja nyilvánvalóan az egyszerűbbik megoldást. A //switch-case megoldás kommentezés pedig a több kódból álló, de az avatatlanabb szemnek az átláthatóbb megoldást.

A kód végén a teszt hasonlóan, mint az előző cikkben, gyárt két objektumot majd kiloggolja a konzolba azokat a toString() segítségével.

Ha tetszett a cikk, véleményed van, kiegészítenéd vagy hibát találtál, írj a cikk alatt egy kommentet, vagy írd meg emailben.

A folytatásban a Builder tervezési minta jön.

Buy and Trade Bitcoin at Binance