Objektumkészlet programtervezési minta

Ez a közzétett változat, ellenőrizve: 2024. április 21.

Az objektumkészlet programtervezési minta egy létrehozási minta, amely inicializált objektum példányok egy csoportját tartja használatra készen (egy készletben, angolul pool) az igények kiszolgálásához, ahelyett, hogy a keresletnek megfelelően folyamatosan újonnan létrehozná illetve megsemmisítené azokat. A készlet egy kliense egy objektum példányt igényel a készletből és bizonyos műveleteket hajt végre a kapott objektum segítségével. Amikor a kliens ezekkel végzett (nincs már szüksége az objektumra), „visszateszi“ az objektumot a készletbe, ahelyett, hogy megsemmisítené azt, ami történhet manuálisan vagy automatikusan.

Az objektumkészlet minta elsősorban a teljesítménnyel kapcsolatos okok miatt használatos: bizonyos körülmények között, a minta alkalmazása jelentősen növeli a teljesítményt. Az objektumkészlet használata „megbonyolítja“ az objektum életciklusát, mivel az objektumok csak „ki vannak véve“ a készletből illetve „vissza vannak téve“ oda, nem pedig ténylegesen létrehozva és megsemmisítve (amellyel megegyező vagy hasonló viselkedést vár el az objektumot használó kliens), ez pedig gondosságot igényel az implementációban.

Amikor nagyszámú objektummal szükséges dolgozni, amelyeket viszonylag költséges létrehozni és inicializálni, és az egyes objektumok jellemzően csak rövid ideig szükségesek, az hátrányosan érinti az egész alkalmazás teljesítményét. Az ilyen esetekben lehet kívánatos az objektumkészlet minta használata.

Az objektumkészlet minta létrehozza objektum példányok egy csoportját, amik többször használhatóak. Amikor egy új objektumra van igény, azt a készletből kérik. Ha már van előzetesen létrehozott, tárolt példány a kért objektumból, akkor az kerül azonnali visszaadásra, megspórolva a létrehozás költségeit. Ha nincs a készletben elérhető objektum, akkor egy új objektum kerül létrehozásra, és visszaadásra a kliensnek. A kiadott objektum használata után, amikor már a kliensnek nincs szüksége rá, az objektum visszakerül a készletbe, lehetővé téve a jövőbeli (többszöri) felhasználást anélkül, hogy a magas számítási költségű objektum példány létrehozást meg kellene ismételni. Fontos megjegyezni, hogy amikor egy objektum használat után visszakerül a készletbe, az esetlegesen rá hivatkozó külső referenciák érvénytelenné válnak (mintha az objektum megsemmisítésre került volna).

Bizonyos objektumkészleteknél az erőforrások korlátozottak, így az egyidejűleg használható objektumok maximális száma adott (korlátozott). Ha ezt a határt a készlet eléri, és új objektumot kérnek tőle, egy kivétel (exception) dobódhat, vagy – megfelelő program architektúra esetén – az objektumot igénylő végrehajtási szál felfüggesztődhet mindaddig, amíg egy szabad objektum vissza nem kerül a készletbe.

Az objektumkészlet minta használatos például a .NET keretrendszer különféle részeiben is. Egy példa erre a „.NET Framework Data Provider for SQL Server“ (az MS SQL szerver elérést biztosító osztálykönyvtár). Mivel az SQL szerverhez elérést biztosító kapcsolati-objektumok létrehozása lassú lehet (hálózati kapcsolat felépítése, stb.), az említett osztálykönyvtár egy objektumkészletet használ a kapcsolatot biztosító objektumokból. Amikor egy ilyen kapcsolatot bezárnak a kapcsolat objektum megfelelő metódusával, az nem szünteti meg ténylegesen a hálózati kapcsolatot a szerverrel, hanem ehelyett a kapcsolat megmarad, a kapcsolat objektum pedig egy objektumkészletbe kerül, ahonnan újból fel lesz majd használva, ha SQL szerver kapcsolatra lesz igény. Ez a módszer lényegesen felgyorsítja az igényelt SQL szerver kapcsolatok létrehozását.

Az objektumkészlet használata jelentős teljesítmény növekedést nyújthat azokban a szituációkban, ahol egy objektum példány létrehozása költséges és az adott objektum osztályából nagy gyakorisággal hozunk létre illetve semmisítünk meg példányokat. Ebben az esetben az objektum példányok sokszor újrahasznosíthatóak, és minden újrahasznosítással jelentős mennyiségű időt és teljesítményt spórolunk meg. Az objektumkészlet használata erőforrásokat igényel – memóriát vagy akár egyéb erőforrásokat is, mint például hálózati socketeket, emiatt célszerű a készletben egyidejűleg létező objektumok számát alacsonyan tartani, habár ez nem feltétlenül szükséges (a rendelkezésre álló erőforrások függvénye).

A készletben lévő objektumok kiosztása belátható (ismert) idő alatt történik, míg az új objektumok létrehozása (különösen, ha az hálózati kapcsolat létrehozásával érintett) előre nem látható időmennyiséget vehet igénybe. Ez az előny különösen akkor válik fontossá, ha az objektum létrehozása időigényes, mint például az adatbázis kapcsolatok, a socket kapcsolatok, végrehajtási szálak és nagy grafikus objektumok (mint a betűkészletek vagy a bitmap képek) esetén.

Egyéb körülmények között, az egyszerű objektumkészletezés (ahol az objektumok nem tartalmaznak egyéb erőforrást, kizárólag csak memóriát foglalnak) nem biztosan hatékony, és akár teljesítmény csökkenéshez is vezethet.[1] Csak memóriát érintő „egyszerű“ készletezés esetén, egy a memória töredezetté válását (a gyakori objektum létrehozás / megsemmisítés miatt) kiküszöbölő memória-foglalási algoritmus/technika (angolul pld. a slab allocation) használata alkalmasabb lehet, mivel ebben az esetben az egyetlen cél a memória-foglalás/felszabadítás számítási költségeinek csökkentése, a memória töredezetté válásának (fregmentálódás) csökkentésével.

Implementáció

szerkesztés

Az objektumkészletek „automatizált“ módon is implementálhatóak az olyan nyelvekben, mint a C++ smart pointerek használatával. A smart pointer konstruktorában (létrehozásakor) egy objektum példány kérhető a készletből, és a smart pointer destruktorában (megsemmisítésekor) a példány szabaddá tehető és visszarakható a készletbe. A memóriakezelésben szemétgyűjtést használó nyelvekben, ahol nincsenek destruktorok (amik garantáltan meghívódnának az objektum „elengedésekor“) - az objektumkészleteket „manuális“ módon kell implementálni, vagyis explicit módon igényelni egy objektum példányt egy gyártól (lásd még Absztrakt gyár programtervezési minta, Gyártó metódus programtervezési minta), és használat után visszaadni az objektumot a dispose metódus explicit meghívásával (mint a dispose tervezési mintában). Automatikusan hívott befejező eljárás (finalizer, finalize method) használata az objektum visszaadására nem jó ötlet, mivel általában nem garantált hogy mikor fut majd le (vagy egyáltalán lefut-e) a befejező eljárás. Célszerű try ... finally blokkokat használni a kódban, hogy az objektumok kiadása és elengedése kivétel (exception) semleges legyen.

A „manuális“ objektumkészleteket viszonylag egyszerű implementálni, de körülményesebb használni, mivel a készletbe kerülő objektumokkal kapcsolatosan manuális memóriamenedzsmentet (explicit, leprogramozott memória foglalás és felszabadítás) igényelnek.

Az üres készletek kezelése

szerkesztés

Az objektumkészletek az alábbi három stratégia egyikét alkalmazzák azon objektum-igénylések kezelésére, mikor már nincs kiadható objektum a készletben.

  1. Nem adnak vissza objektumot (hibajelzést adnak vissza a kliensnek).
  2. Új objektumot hoznak létre, ezzel megnövelve a készlet méretét. Az ezt a megoldást használó objektumkészletek általában lehetővé teszik, hogy beállítható legyen az egyidejűleg maximálisan használható objektumok száma (a maximum elérésénél pedig vagy az előző, vagy a következő megoldást alkalmazzák).
  3. Egy többszálas programozási környezetben, a készlet felfüggesztheti a kliens szálának végrehajtását mindaddig, amíg egy másik végrehajtási szálon egy másik kliens vissza nem tesz egy objektumot a készletbe.

Az objektumkészlet minta implementálásakor, a programozónak gondosan ügyelnie kell arra, hogy a készletbe „visszatett“ objektumok olyan állapotba legyenek visszaállítva, ami alkalmassá teszi őket a problémamentes „következő“ használatra. Ha ezzel nem foglalkoznak megfelelően, akkor a készletből „kiadott“ objektum gyakran – a kliens által nem várt – olyan állapotban lehet, amely a kliens hibás működését eredményezi. Az objektumkészlet felelős az objektumok használható állapotba való visszaállításáért, nem pedig a kliens (a kliens számára a működés szempontjából nem lehet érzékelhető különbség a „frissen létrehozott“ és az „újrahasznosított“ objektumok között). Az olyan objektumkészletet, ami tele vannak veszélyesen „elévült“ (nem megfelelően működőképes) objektumokkal, néha „emésztőgödörként“ is emlegetik, és „anti-mintaként“ (hogyan ne csináljuk) tekintenek rá.

Az elévült objektumok jelenléte a készletben nem mindig okoz problémát; csak akkor válnak veszélyessé, amikor az elévült állapot miatt az érintett objektum viselkedése megváltozik. Például egy objektum, amely egy autentikáció (azonosítás/jogosultságok megállapítása) részleteit reprezentálja, súlyos hibát okozhat, ha a "sikeres azonosítás megtörtént" jelző nincs visszaállítva azelőtt, mielőtt az objektum újrafelhasználás céljából kiadnák a készletből. Ebben az esetben az objektum azt jelzi a kliensnek, hogy a felhasználó sikeresen bejelentkezett (igazából vélhetőleg valaki más) már akkor, amikor a jelenlegi felhasználó még meg sem próbálta azonosítani magát. Viszont akár problémamentes is lehet egy ilyen objektum, ha például valami olyan jellemzőt felejtünk el visszaállítani, amit csak hibakereséshez (debug) használunk, mint mondjuk az azonosításhoz legutoljára használt szerver címe (amitől nem változik meg a viselkedés).

Az objektumok nem megfelelő visszaállítása információ-szivárgást is okozhat. Ha egy objektum bizalmas adatokat tartalmaz (pl.: a felhasználó hitelkártya számát) ami nincs törölve az objektum újrahasznosítása (másik kliensnek való újbóli kiadása) előtt, egy rosszindulatú vagy hibás kliens felfedheti az adatokat egy illetéktelen személy / jogosulatlan fél előtt.

Ha a készletet több végrehajtási szál is használja, a készletnek meg kell előznie, hogy párhuzamosan futó szálak megkapják és párhuzamosan (egyszerre) próbálják használni ugyanazt az objektumot. Ez nem szükséges akkor, ha a készlet által kezelt objektumok megváltoztathatatlanok (Immutable objects) vagy egyéb módon szálbiztosak (Thread safe).

Néhány publikáció nem ajánlja az objektumkészlet minta használatát bizonyos programnyelveken, mint például a Java, különösen olyan objektumok esetében, amik kizárólag memóriát használnak és nem foglalnak külső erőforrásokat.[2] Az ellenzők általában azt mondják, hogy az objektum allokáció viszonylag gyors a szemétgyűjtést használó modern programozási nyelveken; amíg ezeken a new művelet csak néhányszor tíz elemi utasítást igényel, addig az objektumkészlet minta megvalósításaiban klasszikusan szereplő new() (igénylés a készletből) és delete() (visszarakás a készletbe) függvénypár több száz utasításból áll, és sokkal komplexebb feladatot végez.

Egy másik indok, hogy a legtöbb szemétgyűjtő az „élő“ objektum hivatkozásokat nézi végig, és nem azt a memóriát, amit az objektumok maguk lefoglalnak. Ezáltal a „halott“ (hivatkozások nélküli) objektumok viszonylag kis számítási költséggel eldobhatóak. Ezzel ellentétes nagy mennyiségű „élő“ (referált), de nem használt objektum megtartása (az objektumkészlet), mivel megnöveli a szemétgyűjtés időszükségletét.[1]

Példakódok

szerkesztés

A .NET „alapvető osztálykönyvtáraiban“ (Base Class Library) van néhány osztály, ami az objektumkészlet mintát implementálja. Például a System.Threading.ThreadPool olyan módon konfigurált, hogy előre megadott számú végrehajtási szál legyen foglalható. Amikor a készletből használt végrehajtási szálak befejeződnek, elérhetőek maradnak újabb számításokhoz (újbóli használatra). Ezáltal a végrehajtási szálakat a létrehozás és megsemmisítés végrehajtási költségének „megfizetése“ nélkül lehet használni.

Az alábbiakban látható az objektumkészlet minta alapvető implementációjának kódja C# programnyelven. A rövidség miatt az osztályok tulajdonságai a C# 3.0 „automatikusan implementált tulajdonság“ szintaxisával vannak deklarálva. Ezeket teljes tulajdonság-deklarációval kell helyettesíteni a nyelv korábbi változataiban. A készlet egy statikus osztályként van bemutatva, mivel nem szokásos, hogy egy adott objektum típushoz több készletet is használjunk. Mindazonáltal ugyanennyire elfogadható az is, ha példányosítható osztályt használunk az objektumkészlet implementációjára.

//IVSR: Objektum Készlet programtervezési minta
namespace IVSR.DesignPatern.Objectpool 
{
// A PooledObject osztály az a típus (osztály), amit számításigényes vagy lassú 
// példányosítani, vagy korlátozott az elérhetősége, emiatt a készletben tároljuk.
public class PooledObject
{
    DateTime _createdAt = DateTime.Now;
 
    public DateTime CreatedAt
    {
        get { return _createdAt; }
    }
 
    public string TempData { get; set; }
}

// A Pool (készlet) osztály a legfontosabb az objektum készlet mintában. Ez irányítja a hozzáférést
// a készletben lévő objektumokhoz, kezel egy listát az elérhető objektumokról és egyet azokról is,
// amik már kiadásra kerültek a készletből és még használatban vannak. Ez az osztály gondoskodik 
// arról is, hogy a neki visszaadott objektumok megfelelő állapotba kerüljenek, és alkalmasak 
// legyenek igény esetén a következő használatra.
public static class Pool
{
    private static List<PooledObject> _available = new List<PooledObject>();
    private static List<PooledObject> _inUse = new List<PooledObject>();
 
    // Objektum kikérése a készletből
    public static PooledObject GetObject()
    {        
        lock(_available)
        {
            // Ha van a szabad készletben elérhető objektum, adjuk vissza azt
            // és helyezzük át a foglalt listára
            if (_available.Count != 0)
            {
                PooledObject po = _available[0];
                _inUse.Add(po);
                _available.RemoveAt(0);
                return po;
            }
            // Ha nincs szabad készlet, hozzunk létre új objektumot, 
            // rakjuk a foglalt listára, és adjuk vissza a kliensnek
            else
            {
                PooledObject po = new PooledObject();
                _inUse.Add(po);
                return po;
            }
        }
    }
 
    // Objektum "visszarakása" a készletbe (kliens használat befejeztével)
    public static void ReleaseObject(PooledObject po)
    {
        // FONTOS: Objektum állapot visszaállítása, 
        // hogy az újbóli használatra alkalmas legyen
        CleanUp(po);
 
        lock (_available)
        {
            // Eltávolítás a foglalt listáról, visszahelyezés az elérhető készletbe
            _available.Add(po);
            _inUse.Remove(po);
        }
    }
 
    // Az objektum visszaállítását (újra használhatóvá tételét) végző függvény
    private static void CleanUp(PooledObject po)
    {
        po.TempData = null;
    }
}
}

A fenti példában a PooledObject osztálynak két tulajdonsága van. Az egyik a CreatedAt amely az objektum létrehozásának idejét tárolja, a másik a TempData ami egy string, amelyet a kliens tetszőlegesen módosíthat (az objektum „használata“), de visszaállításra kerül, amikor az objektum visszakerül a készletbe. A példában ez mutatja be az objektum visszaállítását/„kipucolását“ a készletbe való visszarakás előtt, ami biztosítja, hogy az objektum érvényes (újrahasznosítható) állapotba kerül, mielőtt újra kiadásra kerülne egy kliensnek a készletből.

A Java2EE objektumkészletezési technikákat alkalmaz, például az enterprise Java beanek esetén. A Java támogatja a végrehajtási szálak készletezését a java.util.concurrent.ExecutorService és egyéb kapcsolódó osztályok segítségével. A végrehajtási szolgáltatónak (executor service) van egy adott mennyiségű „alap“ végrehajtási szála, amik sohasem kerülnek megsemmisítésre. Ha minden szál foglalt, a szolgáltató – megengedett mennyiségű – extra (új) szálat hoz létre igény esetén, de ezeket meg is semmisíti, ha nincsenek felhasználva egy bizonyos lejárati idő eléréséig. Ha nem lehet már több szálat létrehozni, akkor az igények egy sorba kerülnek. Legvégül ha a sor túl hosszúvá válna, beállítható, hogy az igénylő kliens végrehajtási szála felfüggesztődjön (amíg szabad objektumok nem lesznek elérhetőek a készletben).

Java nyelven általában nincs szükség arra, hogy a fejlesztők saját maguk objektumkészletet használjanak, mivel használatának okai (költséges objektumlétrehozás és szemétszedés) nem állnak fen.

public class PooledObject {
	public String temp1;
	public String temp2;
	public String temp3;
	
	public String getTemp1() {
		return temp1;
	}
	public void setTemp1(String temp1) {
		this.temp1 = temp1;
	}
	public String getTemp2() {
		return temp2;
	}
	public void setTemp2(String temp2) {
		this.temp2 = temp2;
	}
	public String getTemp3() {
		return temp3;
	}
	public void setTemp3(String temp3) {
		this.temp3 = temp3;
	}
	
	
	
}
public class PooledObjectPool {
	private static long expTime = 60000;//6 seconds
	public static HashMap<PooledObject, Long> available = new HashMap<PooledObject, Long>();
	public static HashMap<PooledObject, Long> inUse = new HashMap<PooledObject, Long>();
	
	
	public synchronized static PooledObject getObject() {
		long now = System.currentTimeMillis();
		if (!available.isEmpty()) {
			for (Map.Entry<PooledObject, Long> entry : available.entrySet()) {
				if (now - entry.getValue() > expTime) { //object has expired
					popElement(available);
				} else {
					PooledObject po = popElement(available, entry.getKey());
					push(inUse, po, now); 
					return po;
				}
			}
		}

		// either no PooledObject is available or each has expired, so return a new one
		return createPooledObject(now);
	}	
	
	private synchronized static PooledObject createPooledObject(long now) {
		PooledObject po = new PooledObject();
		push(inUse, po, now);
		return po;
        }

	private synchronized static void push(HashMap<PooledObject, Long> map,
			PooledObject po, long now) {
		map.put(po, now);
	}

	public static void releaseObject(PooledObject po) {
		cleanUp(po);
		available.put(po, System.currentTimeMillis());
		inUse.remove(po);
	}
	
	private static PooledObject popElement(HashMap<PooledObject, Long> map) {
		 Map.Entry<PooledObject, Long> entry = map.entrySet().iterator().next();
		 PooledObject key= entry.getKey();
		 //Long value=entry.getValue();
		 map.remove(entry.getKey());
		 return key;
	}
	
	private static PooledObject popElement(HashMap<PooledObject, Long> map, PooledObject key) {
		map.remove(key);
		return key;
	}
	
	public static void cleanUp(PooledObject po) {
		po.setTemp1(null);
		po.setTemp2(null);
		po.setTemp3(null);
	}
}
  1. a b Java theory and practice: Urban performance legends, revisited. IBM developerWorks, 2005. szeptember 27. [2013. október 24-i dátummal az eredetiből archiválva]. (Hozzáférés: 2012. augusztus 28.)
  2. Java theory and practice: Garbage collection in the HotSpot JVM. IBM developerWorks, 2005. szeptember 27. [2013. október 24-i dátummal az eredetiből archiválva]. (Hozzáférés: 2012. augusztus 28.)

További információk (angolul)

szerkesztés

Fordítás

szerkesztés

Ez a szócikk részben vagy egészben az Object pool pattern című angol Wikipédia-szócikk fordításán alapul. Az eredeti cikk szerkesztőit annak laptörténete sorolja fel. Ez a jelzés csupán a megfogalmazás eredetét és a szerzői jogokat jelzi, nem szolgál a cikkben szereplő információk forrásmegjelöléseként.