Lekce 4 - Object pool (fond objektů)
V minulé lekci, Servant (Služebník), jsme si ukázali návrhový vzor Služebník, který přidává skupině tříd určitou funkčnost bez její přímé implementace do těchto tříd.
V dnešním tutoriálu si představíme vzor Object pool, který zamezí opakovanému vytváření objektů, jejichž konstrukce je příliš složitá nebo trvá dlouhou dobu.
Motivace použití vzoru
Object fond se hodí, pracujeme-li na mnoha místech s instancí, která je složitá na vytvoření a jejíž opakovaná konstrukce nám zpomaluje celý program. Návrhový vzor Object fond se sám stará o vytvoření a předávání složitých objektů zbytku programu. Vytvořené objekty nemaže, ale uchovává na další použití.
Příklad použití vzoru
Téměř každá webová aplikace využívá databázi pro uchování dat. S tím souvisí připojení k samotné databázi, kdy si aplikace musí vytvořit spojení. Takové spojení může v extrémních případech trvat i několik sekund. Pokud takových připojení vytváříme v aplikaci více, například kvůli více vláknům nebo dokonce pro každý dotaz, je odezva aplikace pomalá.
Z této situace nás zachrání Object fond. V našem případě bude udržovat připojení k databázi. Pokud se budeme chtít dotázat na databázi, požádá fond o připojení. Jakmile dotaz provede, vrátí připojení zpět fondu. Fond připojení zrecykluje a může ho poté předat další části programu, která o připojení požádá. Hlavní přínos spočívá v eliminaci opětovného vytváření připojení, které na celé operaci trvá nejdéle.
Implementace vzoru
Nejprve si vytvoříme třídu FondPripojeni
a poté třídu
Program
, která bude využívat FondPripojeni
pro
připojení k databázi.
Třída FondPripojeni
Třída FondPripojeni
má v základní implementaci pouze dvě
metody:
VratPripojeni()
- vytvoří a vrátí připojení,PridejPripojeni()
- přidá připojení zpět fondu.
Teprve když předáme připojení zpět fondu, může jej fond předat jiné části programu.
Kód třídy vypadá následovně:
-
public class FondPripojeni { private string jmenoServeru = "MujServer"; private string databaze = "Northwind"; private List<SqlConnection> seznamPripojeni = new List<SqlConnection>(); public SqlConnection VratPripojeni() { if (seznamPripojeni.Count == 0) { Console.WriteLine("Připojení bylo vytvořeno"); return new SqlConnection($"Server={jmenoServeru}; Database={databaze}; Trusted_Connection=True"); } else { SqlConnection pripojeni = seznamPripojeni[0]; seznamPripojeni.RemoveAt(0); return pripojeni; } } public void PridejPripojeni(SqlConnection pripojeni) { seznamPripojeni.Add(pripojeni); } }
-
public class FondPripojeni { private String jmenoServeru = "MujServer"; private String databaze = "Northwind"; private List<Connection> seznamPripojeni = new ArrayList<>(); public Connection vratPripojeni() { if (seznamPripojeni.isEmpty()) { String pripojovaciUrl = "jdbc:sqlserver://" + jmenoServeru + ";databaseName=" + databaze + ";integratedSecurity=true;"; System.out.println("Připojení bylo vytvořeno"); return DriverManager.getConnection(pripojovaciUrl); } else { Connection pripojeni = seznamPripojeni.get(0); seznamPripojeni.remove(0); return pripojeni; } } public void pridejPripojeni(Connection pripojeni) { seznamPripojeni.add(pripojeni); } }
-
class FondPripojeni { private $jmenoServeru = "MujServer"; private $databaze = "Northwind"; private $seznamPripojeni = []; public function vratPripojeni(): SqlConnection { if (count($this->seznamPripojeni) === 0) { $pripojovaciUrl = "sqlsrv:Server=$this->jmenoServeru;Database=$this->databaze"; echo("Připojení bylo vytvořeno"); return new PDO($pripojovaciUrl, null, null); } else { $pripojeni = $this->seznamPripojeni[0]; unset($this->seznamPripojeni[0]); return pripojeni; } } public function pridejPripojeni(SqlConnection $pripojeni): void { $this->seznamPripojeni[] = $pripojeni; } }
-
class FondPripojeni { constructor() { this.jmenoServeru = 'MujServer'; this.databaze = 'Northwind'; this.seznamPripojeni = []; } async vratPripojeni() { if (this.seznamPripojeni.length === 0) { const konfigurace = { user: '', password: '', server: this.jmenoServeru, database: this.databaze, options: { trustServerCertificate: true, }, authentication: { type: 'default', }, }; console.log("Připojení bylo vytvořeno"); return await sql.connect(konfigurace); } else { return this.seznamPripojeni.shift(); } } pridejPripojeni(pripojeni) { this.seznamPripojeni.push(pripojeni); } }
-
class FondPripojeni: def __init__(self): self.jmeno_serveru = 'MujServer' self.databaze = 'Northwind' self.seznamPripojeni = [] def vrat_pripojeni(self): if len(self.seznamPripojeni) == 0: pripojovaci_url = f'DRIVER={{ODBC Driver 17 for SQL Server}};SERVER={self.jmeno_serveru};DATABASE={self.databaze};Trusted_Connection=yes;' print("Připojení bylo vytvořeno") return pyodbc.connect(pripojovaci_url) else: return self.seznamPripojeni.pop(0) def pridej_pripojeni(self, pripojeni): self.seznamPripojeni.append(pripojeni)
Využití fondu
Abychom si situaci demonstrovali zavoláme 20krát metodu, která se uspí. Po probuzení požádá fond o připojení. Aby bylo lépe vidět, že se vytváří méně připojení, než je vláken, bude každé připojení vypisovat informaci o tom, že bylo právě vytvořeno:
-
class Program { private FondPripojeni fondPripojeni = new FondPripojeni(); public void VyzadejObjekt() { Thread.Sleep(new Random().Next(3000)); Console.WriteLine("Žádost o objekt"); SqlConnection pripojeni = fondPripojeni.VratPripojeni(); Thread.Sleep(new Random().Next(2000)); fondPripojeni.PridejPripojeni(pripojeni); } static void Main(string[] args) { Program program = new Program(); for (int a = 0; a < 20; a++) { Thread vlakno = new Thread(() => program.VyzadejObjekt()); vlakno.Start(); } } }
-
public class Program { private FondPripojeni fondPripojeni = new FondPripojeni(); public void vyzadejObjekt() { Thread.sleep((new Random()).nextInt(3000)); System.out.println("Žádost o objekt"); Connection pripojeni = fondPripojeni.vratPripojeni(); Thread.sleep(2000); fondPripojeni.pridejPripojeni(pripojeni); } public static void main(String[] args) { Program program = new Program(); for (int a = 0; a < 20; a++) { new Thread(() -> { program.vyzadejObjekt(); }).start(); } } }
-
class Program { public function __construct() { $this->fondPripojeni = new FondPripojeni(); for ($a = 0; a < 20; a++) { $this->vyzadejObjekt(); } } public function vyzadejObjekt(): void { sleep(mt_rand(0, 3)); echo("Žádost o objekt"); $pripojeni = $this->fondPripojeni->vratPripojeni(); sleep(mt_rand(0, 2)); $this->fondPripojeni->pridejPripojeni(pripojeni); } }
-
class Program { constructor() { this.fondPripojeni = new FondPripojeni(); } async vyzadejObjekt() { let randomTime = Math.floor(Math.random() * (3000 - 0)); await this.sleep(randomTime); console.log("Žádost o objekt"); const pripojeni = this.fondPripojeni.vratPripojeni(); await this.sleep(Math.floor(Math.random() * (2000 - 0))); this.fondPripojeni.pridejPripojeni(pripojeni); } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async main() { for (let a = 0; a < 20; a++) { await this.vyzadejObjekt(); } } } const program = new Program(); program.main();
-
class Program: def __init__(self): self.fondPripojeni = FondPripojeni() def vyzadej_objekt(self): time.sleep(random.randint(0, 3)) print("Žádost o objekt") pripojeni = self.fondPripojeni.vrat_pripojeni() time.sleep(random.randint(0, 2)) self.fondPripojeni.pridej_pripojeni(pripojeni) def main(self): for _ in range(20): thread = threading.Thread(target=self.vyzadej_objekt) thread.start() program = Program() program.main()
Výstup programu:
Konzolová aplikace
Žádost o objekt
Připojení bylo vytvořeno
Žádost o objekt
Žádost o objekt
Žádost o objekt
Žádost o objekt
Žádost o objekt
Připojení bylo vytvořeno
Žádost o objekt
Žádost o objekt
Žádost o objekt
Žádost o objekt
Žádost o objekt
Žádost o objekt
Žádost o objekt
Žádost o objekt
Žádost o objekt
Žádost o objekt
Žádost o objekt
Žádost o objekt
Připojení bylo vytvořeno
Žádost o objekt
Žádost o objekt
Ačkoliv chtěla aplikace 20 připojení, tak se ve skutečnosti vytvořily pouze tři připojení. Kdyby si každé vlákno vytvořilo vlastní připojení zvýší se potřebný čas, který bude potřeba pro výpočet. Navíc bude více zahlcena linka i samotná databáze, protože budeme muset udržovat několik spojení současně.
Možná vylepšení konfigurace
Některé objekty nám mohou zabírat mnoho místa. Použijeme-li je ve fondu, může nám rychle dojít paměť. V takových situacích zpravidla nechceme mít neomezený počet existujících instancí.
Omezení počtu instancí
Fondu můžeme v parametru předat maximální počet instancí, které má
uchovávat. Pokud by chtělo vlákno další instanci, může metoda
vratPripojeni()
například vyhodit výjimku, nebo vrátit
null
.
Omezení na základě urgentnosti
Druhý přístup spočívá v tom, že bychom si nadefinovali dvě metody:
vratPripojeniIhned()
a vratPripojeniCekam()
.
Metoda
vratPripojeniIhned()
Pokud by nebyly žádné volné objekty, metoda by vytvořila novou instanci objektu připojení. Při navrácení objektů zpět by se objekt, který by byl již nad hodnotou parametru maximálního počtu instancí, neuložil zpět, ale rovnou se smazal.
Metoda
vratPripojeniCekam()
Při volání metody vratPripojeniCekam()
by se vlákno
zablokovalo do doby, než bude volná instance ve fondu.
Který způsob využijeme závisí na konkrétní situaci. V našem případě optimalizujeme především mezi časem a pamětí.
Vracení instancí do fondu
Hlavní problém, který s fondem souvisí je, že musíme vracet objekty zpět. Na první pohled se jedná o relativně jednoduchý princip, ale ne všichni programátoři ho dodržují. První problém, který může nastat je, že budeme objekt z fondu používat i po jeho navrácení zpět. Tato situace lze vyřešit obalením návratového objektu, což jsme probírali v lekci Proxy (zástupce). Proxy zajistí, aby objekt nešel použít po tom, co se navrátí zpět do fondu.
Druhým případem je situace, kdy objekt vůbec vracet nebudeme. S takovou
situací se vypořádáme jen velmi těžko a zde záleží na konkrétním
jazyce. Opět by se uplatnil návrhový vzor proxy, ale například v C++ by se
vracela hodnota přímo a ne ukazatelem. Tím by bylo zajištěno, že se objekt
vrátí zpět do fondu po opuštění kontextu. Například v destruktoru proxy.
V C# bychom mohli definovat proxy odvozené od IDisposable
, který
od nás sice pořád vyžaduje nějakou akci, ale stále je to více než
nic.
Vícevláknové aplikace
Ačkoliv byla první ukázka demonstrována pomocí více vláken, v reálné aplikaci se o vhodnou implementaci nejedná. Fond se vůbec nestará o synchronizaci, jak by tomu mělo být.
Vícevláknové mechanismy jsou popsány v kurzu Paralelní programování a vícevláknové aplikace v C#.NET.
Níže se podívejme na UML diagram, jak by měl být fond implementován. Spojíme zde synchronizaci, maximální počet instancí i navrácení zpět do fondu:

Všimněme si, jak se nám kód rozrostl. To je daň za to, kdy chceme
pracovat s více vlákny současně. V metodě vratPripojeniCekam()
opakovaně uspáváme vlákno do doby, než se uvolní objekt, který
potřebujeme. V rámci jednoduchosti je metoda definována tímto způsobem, ale
lze dále optimalizovat například pomocí semaforů.
Závěr
Object fond použijeme v programech, kde chceme zamezit opětovnému vytváření instance, nebo chceme mít kontrolu nad tím, kolik objektů existuje. Můžeme kontrolovat počet existujících objektů . A to i když při psaní kódu nevíme, kolik objektů bude aplikace reálně potřebovat.
V další lekci, Immutable objects (neměnné objekty), si představíme vzor pro neměnné objekty (immutable objects), u kterých nemůžeme změnit jejich vnitřní stav.