Programtervezési minták - bevezetés és absztrakt gyár

A programtervezési minták alapgondolata 1987 óta van jelen a szoftverfejlesztésben. Egyes programozók egész sokat tudnak a mintákról, mások kevesebbet - a hasznosságukat azonban általában nem kérdőjelezik meg. A ponte.hu-nál elhatároztuk, hogy elmélyülünk a témában. 

factory_building

Érdekes módon minták alapötletét Christopher Alexander építész adta, akinek a "The Timeless Way of Building" című könyve a 70-es évek végén jelent meg. Ebben a könyvben olyan szakmai fogásokat próbált leírni, amik segítségével egy kezdő építész is gyorsan jó épületeket tervezhet. Persze nem ez az egyetlen kapcsolat az építészet és a programozás között - elég csak arra gondolnunk, hogy az informatikában a szoftverek alapszerkezetét megtervező munkatársat "architekt"-nek nevezzük, holott a szó eredetileg "építészmérnök"-öt jelent.

Az 1900-as évek végéig a programozásra sokan úgy tekintettek, mint a házépítés informatikai megfelelőjére. Az elkészült műtől elvárták, hogy néhány évig nagyobb tatarozás nélkül is működjön. Odafigyeltek a tervezésre, az alapozásra, és lehetőségek szerint a kódsorok összehabarcsolására is. Az építészeti metafóra remekül működött. A 2000-es évek elején megjelent agilis mozgalom felismerései aztán valamelyest árnyalták a képet. Kiderült, hogy a szoftverek sokkal gyakrabban változnak, mint az épületek. Nem igazán életszerű az, hogy egy bérház lakója egy reggel kilép a bejárati ajtaján, és csodálkozva szembesül a ténnyel: az este folyamán a lépcsőházat újrafestették, az ablakokra rács került, és a liftet is 5 méterrel odébbrakták. A szoftverfejlesztésben folyton ilyesmit csinálunk. Egyértelmű, hogy az építészet termelési módszereit teljes mértékben nem másolhatjuk le.

A programtervezési mintákat a "négyek bandája" (Erich Gamma, Richard Helm, Ralph Johnson és John Vlissides) gondozásában 1994-ben megjelent alapmű óta rengeteg kritika érte, de ugyanannyi, ha nem több méltató vélemény is napvilágot látott. A programozói szakmának is megvannak a maga mesterfogásai, amiket egy modern fejlesztőnek alkalmaznia kell, ha sikeres szeretne lenni. Ezek közé tartoznak a minták is. Mindegyik egy-egy gyakran előforduló problémára próbál általánosított, ugyanakkor optimalizált választ adni. A használatukkal a szoftverünket a módosításokkal szemben ellenállóbbá tehetjük. Lerövidítik a tapasztalatszerzés idejét, segítenek a tervezésben, közös szótárat adnak a fejlesztők kezébe, és "magasabb" szintű programozást is lehetővé tesznek. Egyértelmű, hogy a ponte.hu-nál előbb-utóbb foglalkoznunk kellett a témával.

Rövid áttekintéssel kezdtünk. A programtervezési mintákat hagyományosan három fő csoportba sorolják. A létrehozási (creational) minták útmutatást adnak ahhoz, hogy hogyan hozzunk létre új osztálypéldányokat. A java nyelvben ez gyakorlatilag arról szól, hogy hová rakjuk, és hová ne rakjuk a "new" utasítást. A szerkezeti (structural) minták az objektumaink közötti kapcsolatokra koncentrálnak. A cél az, hogy a kiépített struktúra időtálló legyen, ugyanakkor könnyen lehessen hozzá kiegészítéseket adni. Végül a viselkedési (behavioural) minták az objektumok együttműködéséről szólnak. Jobb algoritmusokkal jobb eredményt tudunk elérni.

Az első megvizsgált programtervezési minta az absztrakt gyár (abstract factory) volt. Ez annyira alapvető, hogy valószínűleg mindenki hallott már róla. A létrehozási minták családjába tartozik, és abban nyújt segítséget, hogy objektumok egy csoportját anélkül hozhassuk létre, hogy az üzleti logikát tartalmazó kódban törődnénk a konkrét implementációval.

Mielőtt közelebbről megismerkednénk az absztrakt gyárral, érdemes elgondolkozni azon, hogy miért nem előnyös, ha az üzleti logikát teleszórjuk "new" utasításokkal. A java nyelvben absztrakt osztályból objektum nem hozható létre, vagyis a "new" utasítás után mindig egy konkrét megvalósítás osztályát kell írnunk. Ez persze egyben össze is köti a hívó felet a konkrét megvalósítás kódjával. Ha az utóbbiban valami változik, akkor valószínű, hogy az előbbit is módosítani kell. A SOLID programozási alapelvek óta tudjuk, hogy az sosem szerencsés, ha egy módosítás más módosításokat is maga után von, ugyanis ezzel a hibalehetőség is növekszik. Ha tartunk attól, hogy egy osztály a jövőben sokat fog változni, vagy esetleg előre még nem tudjuk eldönteni, hogy egy absztrakt osztály milyen konkrét megvalósítását is szeretnénk majd használni, akkor érdemes az osztálypéldányok létrehozását egy különálló programegységbe kiszervezni. Erre ad lehetőséget az absztrakt gyár.

A minta lényege, hogy a gyakran változó, vagy még nem végleges típusú objektumainkat (nevezzük ezeket Termékeknek) egy önálló osztályban, a Gyárban hozzuk létre. A Gyárnak van egy olyan metódusa, ami visszaad egy új Terméket. Az a logika, hogy pontosan hogyan, milyen erőforrások és egyéb osztályok használatával történik egy Termék létrehozása, a Gyár magánügye. A kliens kód csak kap valahogyan egy Gyárat, és elkéri tőle a Terméket.

A minta eredeti formájában két absztrakciót is tartalmaz. (Az absztrakció azt jelenti, hogy különválasztjuk a lényeges tulajdonságokat a lényegtelenektől, és csak ez előbbiekkel foglalkozunk.) A Termékre annak absztrakt interfészén keresztül hivatkozunk. Ez az interfész csak azokat a metódusokat tartalmazza, amiket a kliens ténylegesen használ - ez általában nem sok. Ezen a módon a kliens nem függ a Termék interfész konkrét megvalósításától, ha a jövőben esetleg le szeretnénk azt cserélni, akkor nagyon könnyű dolgunk lesz. Emellett a Gyár is egy interfészen keresztül érhető el, vagyis a kliens - hasonló megfontolásokból - a konkrét Gyár implementációtól sem függ. A Gyár interfész legfontosabb (és talán egyetlen) metódusa az, ami létrehoz egy Terméket - többet nem is kell róla tudnunk. Ha egy Gyár példányt valahogyan be tudunk juttatni a kliensbe (például paraméterként átadjuk), akkor az egész objektum-létrehozási problémát dupla absztrakcióval, kicsit bonyolult módon, de elegánsan megoldottuk.

Mindezt jobban megérthetjük egy egyszerű példán keresztül. Legyen a Termék egy gomb valamilyen felhasználói felületen! Ekkor a Termék interfésze valami ilyesmi lehet:

interface Button {
    void paint();
}

A konkrét megvalósítások már sok mindentől függhetnek, a legfontosabb ilyen talán az operációs rendszer. Ezért a gombjainkból is többféle lehet:

class WinButton implements Button {
    @Override
    void paint() { … }
}
class OSXButton implements Button {
    @Override
    void paint() { … }
}

A gombokat létrehozó Gyár interfész szintén nagyon egyszerű:

interface GUIFactory {
    Button createButton();
}

Ennek az implementációja dönti el, hogy milyen konkrét gombot is ad a kliensnek.

class GUIFactoryImpl implements GUIFactory {
	private final String appearance;

	GUIFactoryImpl(String appearance) {
		this.appearance = appearance;
	}

	Button createButton() {
		switch (this.appearance) {
			case "osx": return new OSXButton();
			case "win": return new WinButton();
		}
		throw new IllegalArgumentException("unknown " + appearance);
	}

	static GUIFactory factory(String appearance) {
		return new GUIFactory(appearance);
	}
}

Az egész használatához először létre kell hoznunk valahol egy GUIFactory példányt, azt át kell adnunk a kliensnek, és az már hívhatja is a createButton metódust. Ha jól csináljuk, akkor a kliensünk csak a GUIFactory és a Button interfészeket ismeri, az implementációikat már nem.

GUIFactory factory = GUIFactoryImpl.factory("win");
…
Button button = factory.createButton();
button.paint();

Programozást tanulni nem lehet anélkül, hogy konkrétan programoznánk is. Ezért nemcsak példát néztünk, hanem a tanultakat egy feladaton keresztül ki is próbáltuk. A feladatot részenként más-más jelenlévő kolléga kódolta, míg a többiek tanácsokkal segítették.

A feladat szerint egy üzenetküldő osztályt szeretnénk írni, ami egy feladótól küld üzenetet egy címzettnek, adott tárggyal és üzenettörzzsel. Viszont két környezetünk is van, és ezekben az osztálynak picit másképp kell működnie. Az éles környezetben ténylegesen el kell küldeni egy email-t, a teszt környezetben viszont elég egy logfájlba írni az üzenet paramétereit. Ezeknek megfelelően már létezett az üzenetküldő interfész (MessageSender), amiben csak egyetlen sendMessage metódus volt, és létezett két implementációja, a két környezetnek megfelelően. A feladat az absztrakt gyár (MessageSenderFactory) és két megvalósításának elkészítése volt. Az egyik MessageSenderFactory megvalósítás olyan objektumot gyártott, ami email-t küld, a másik pedig olyat, ami csak loggol. Mindezt nem volt nehéz lekódolni - főleg úgy, hogy előre megírt egységtesztek segítették az ellenőrzést.

Ha tetszett a cikk oszd meg másokkal is.