NOVINKA - Online rekvalifikační kurz Python programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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:

Diagram - Ostatní návrhové vzory

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.


 

Jak se ti líbí článek?
Před uložením hodnocení, popiš prosím autorovi, co je špatněZnaků 0 z 50-500
Předchozí článek
Servant (Služebník)
Všechny články v sekci
Ostatní návrhové vzory
Přeskočit článek
(nedoporučujeme)
Immutable objects (neměnné objekty)
Článek pro vás napsal Patrik Valkovič
Avatar
Uživatelské hodnocení:
25 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity