Lusta inicializáció programtervezési minta

Ez a közzétett változat, ellenőrizve: 2020. október 4.

A számítógépes programozásban, a lusta inicializáció programtervezési minta egy olyan taktika, amely szerint késleltetjük egy objektum létrehozását, vagy valamely számításigényes művelet elvégzését egészen addig, amíg az objektumra vagy a számítás eredményére először ténylegesen szükség lesz.

Ezt tipikusan egy változó elérésére szolgáló eljárás vagy egy objektum-tulajdonság (property) definíció bevezetésével valósítjuk meg, amelyben ellenőrizzük, hogy az elérni (használni) kívánt objektum példány (vagy számítási eredmény) létezik-e már. Ha nem létezik, egy új példány kerül létrehozásra és tárolásra a kapcsolódó változóban, és ennek értéke kerül a hívónak (az adatra hivatkozó kódrészletnek) visszaadásra „éppen a kellő időben” (Just In Time) módon.

Ezzel a viselkedéssel az objektum létrehozását „elhalasztják” az első használatig, amely bizonyos körülmények között (pl. az objektum ritkább használata esetén), csökkenti a rendszer válaszidejét és gyorsítja az indítást azáltal, hogy elkerüli a nagyméretű objektumok előzetes létrehozását és memóriafoglalását. (Megjegyezzük, hogy akár ellenkező hatást okozhat az általános teljesítményben, ha a késleltetett objektum létrehozás előnyeit „elhasználjuk” a rendszer indító/előkészítő fázisa során.)

Több szálon futó kód esetén, a lusta inicializációval használt objektumokhoz való hozzáférést szinkronizálni kell (kölcsönös kizárás/mutex), a versenyhelyzet (race condition) elkerülése érdekében.

A késleltetett kiértékelés jól mutatja az alapötlet általános megvalósítását. Az erősen imperatív programozási nyelvekben ez a minta rejtett veszélyeket hordoz, csakúgy mint bármely programozási szokás amely megosztott állapotokra támaszkodik.

A „lusta gyár”

szerkesztés

A programtervezési minták nézőpontjából vizsgálva, a lusta inicializáció gyakran együttesen használt a gyártó metódus programtervezési mintával. Ez a megoldás három elgondolást kombinál:

  • Egy gyártó metódust használunk, hogy példányokat kapjunk egy osztályból (gyártó metódus programtervezési minta)
  • A példányokat egy asszociatív tömbben tároljuk (létrehozási paraméterek/létrehozott példány), így amikor legközelebb „ugyanazokkal” a paraméterekkel kérnek példányt a gyártól, „ugyanazt” a példányt adja majd vissza, ami az adott paraméterekkel már létrehozásra került (többke programtervezési minta)
  • Lusta inicializációt használunk az objektumok létrehozására, azok csak akkor jönnek létre, amikor először kérik őket a gyártól

Példakódok

szerkesztés

C-ben a lusta inicializáció egyetlen függvényben vagy egyetlen forrásfájlban kerül megvalósításra, statikus változók használatával.

Egy függvényben:

#include<string.h>
#include<stdlib.h>
#include<stddef.h>
#include<stdio.h>

struct fruit {
    char *name;
    struct fruit *next;
    int number;
    /* Egyéb adattagok */
};

struct fruit *get_fruit(char *name) {
    static struct fruit *fruit_list;
    static int seq;
    struct fruit *f;
    for (f=fruit_list; f; f=f->next)
        if (0==strcmp(name, f->name))
            return f;
    if (!(f = malloc(sizeof(struct fruit))))
        return NULL;
    if (!(f->name = strdup(name))) {
        free(f);
        return NULL;
    }
    f->number = ++seq;
    f->next = fruit_list;
    fruit_list = f;
    return f;
}

/* Példa kód */

int main(int argc, char *argv[]) {
    int i;
    struct fruit *f;
    if (argc<2) {
        fprintf(stderr, "Használat: fruits gyümölcs-név [...]\n");
        exit(1);
    }
    for (i=1; i<argc; i++) {
        if ((f = get_fruit(argv[i]))) {
            printf("Gyümölcs %s: száma %d\n", argv[i], f->number);
        }
    }
    return 0;
}

Egyetlen forrásfájl használata a fentiek helyett lehetővé teszi, hogy az állapotok megosztásra kerüljenek több függvény között úgy, hogy a nem kapcsolódó függvények elől továbbra is el vannak rejtve.

fruit.h:

#ifndef _FRUIT_INCLUDED_
#define _FRUIT_INCLUDED_

struct fruit {
    char *name;
    struct fruit *next;
    int number;
    /* Egyéb adattagok */
};

struct fruit *get_fruit(char *name);
void print_fruit_list(FILE *file);

#endif /* _FRUIT_INCLUDED_ */

fruit.c:

#include<string.h>
#include<stdlib.h>
#include<stddef.h>
#include<stdio.h>
#include"fruit.h"

static struct fruit *fruit_list;
static int seq;

struct fruit *get_fruit(char *name) {
    struct fruit *f;
    for (f=fruit_list; f; f=f->next)
        if (0==strcmp(name, f->name))
            return f;
    if (!(f = malloc(sizeof(struct fruit))))
        return NULL;
    if (!(f->name = strdup(name))) {
        free(f);
        return NULL;
    }
    f->number = ++seq;
    f->next = fruit_list;
    fruit_list = f;
    return f;
}

void print_fruit_list(FILE *file) {
    struct fruit *f;
    for (f=fruit_list; f; f=f->next)
        fprintf(file, "%4d  %s\n", f->number, f->name);
}

main.c:

#include<stdlib.h>
#include<stdio.h>
#include"fruit.h"

int main(int argc, char *argv[]) {
    int i;
    struct fruit *f;
    if (argc<2) {
        fprintf(stderr, "Használat: fruits gyümölcs-név [...]\n");
        exit(1);
    }
    for (i=1; i<argc; i++) {
        if ((f = get_fruit(argv[i]))) {
            printf("Gyümölcs %s: száma %d\n", argv[i], f->number);
        }
    }
    printf("A következő gyümölcsök (fruits) generálódtak:\n");
    print_fruit_list(stdout);
    return 0;
}

A .NET 4.0-ban a Microsoft elérhetővé tett egy Lazy osztályt, amely lusta (késői) betöltéshez használható. Az alábbi néhány sor mintakód a Fruit osztály lusta betöltését mutatja:

Lazy<Fruit> lazyFruit = new Lazy<Fruit>();
Fruit fruit = lazyFruit.Value;

Az alábbi egy minta kód C# programozási nyelven.

A Fruit osztály maga nem csinál semmit a példában, a _typesDictionary osztály-változó egy szótár/asszociatív tömb amiben a Fruit példányokat tároljuk a typeName (típusnév) alapján.

using System;
using System.Collections;
using System.Collections.Generic;

public class Fruit
{
    private string _typeName;
    private static Dictionary<string, Fruit> _typesDictionary = new Dictionary<string, Fruit>();

    private Fruit(String typeName)
    {
        this._typeName = typeName;
    }

    public static Fruit GetFruitByTypeName(string type)
    {
        Fruit fruit;

        if (!_typesDictionary.TryGetValue(type, out fruit))
        {
            // Lusta inicializáció
            fruit = new Fruit(type);

            _typesDictionary.Add(type, fruit);
        }
        return fruit;
    }

    public static void ShowAll()
    {
        if (_typesDictionary.Count > 0)
        {
            Console.WriteLine("Az elkészített példányok száma = {0}", _typesDictionary.Count);
            
            foreach (KeyValuePair<string, Fruit> kvp in _typesDictionary)
            {
                Console.WriteLine(kvp.Key);
            }
            
            Console.WriteLine();
        }
    }
    
    public Fruit()
    {
        // szükséges a mintakód lefordításához
    }
}

class Program
{
    static void Main(string[] args)
    {
        Fruit.GetFruitByTypeName("Banana");
        Fruit.ShowAll();

        Fruit.GetFruitByTypeName("Apple");
        Fruit.ShowAll();

        // az előzőleg már létrehozott példányt adja vissza 
        // ami a "Banana" paraméterrel való első hívásnál létrejött
        Fruit.GetFruitByTypeName("Banana");
        Fruit.ShowAll();

        Console.ReadLine();
    }
}

Az alábbiakban egy meglehetősen lényegre törő példáját mutatjuk be a lusta inicializáció programtervezési mintának azzal, hogy a példa enumerációt (felsorolással definiált típust) használ alkalmazott típusként.

//IVSR: LazyInitialization design pattern
namespace IVSR.DesignPatterns.LazyInitialization
{
    public class LazyFactoryObject
    {
        // az elemek belsőleg használt gyűjteménye.
        // az IDictionary típus gondoskodik róla, hogy az elemek egyediek legyenek
        private IDictionary<LazyObjectType, LazyObject> _LazyObjectList = 
            new Dictionary<LazyObjectType, LazyObject>();

        // felsorolási típus a kívánt típusnevek megadásához.
        // így nem szükséges string paraméterátadást, 
        // és lehetséges a fordításkori típusellenőrzés
        public enum LazyObjectType
        { 
            None,
            Small,
            Big,
            Bigger,
            Huge
        }

        // egy általános típus a létrehozandó objektumoknak
        public struct LazyObject
        {
            public LazyObjectType Name;
            public IList<int> Result;
        }

        // egy felsorolási típus elemet vesz át, és létrehoz egy 
        // 'drága' (időigényes, számításigényes stb.) listát
        private IList<int> Result(LazyObjectType name)
        {
            IList<int> result = null;

            switch (name)
            { 
                case LazyObjectType.Small:
                    result = CreateSomeExpensiveList(1, 100);
                    break;
                case LazyObjectType.Big:
                    result = CreateSomeExpensiveList(1, 1000);
                    break;
                case LazyObjectType.Bigger:
                    result = CreateSomeExpensiveList(1, 10000);
                    break;
                case LazyObjectType.Huge:
                    result = CreateSomeExpensiveList(1, 100000);
                    break;
                case LazyObjectType.None:
                    result = null;
                    break;
                default:
                    result = null;
                    break;
            }

            return result;
        }

        // nem lenne 'drága' (számításigényes) létrehozni az elemet 
        // de hogy érthető legyen a lényeg, késleltetjük néhány 
        // számításigényes objektum létrehozását addig, 
        // amíg szükség nem lesz rájuk
        private IList<int> CreateSomeExpensiveList(int start, int end)
        {
            IList<int> result = new List<int>();

            for (int counter = 0; counter < (end - start); counter++)
            {
                result.Add(start + counter);
            }

            return result;
        }

        public LazyFactoryObject()
        { 
            // üres konstruktor
        }

        public LazyObject GetLazyFactoryObject(LazyObjectType name)
        {
            LazyObject noGoodSomeOne;

            // 'előveszi' a LazyObjectType elemet a listáról ha van már ott,
            // egyébként (ha nincs) létrehoz egyet és a listára (is) teszi
            if (!_LazyObjectList.TryGetValue(name, out noGoodSomeOne))
            {
                noGoodSomeOne = new LazyObject();
                noGoodSomeOne.Name = name;
                noGoodSomeOne.Result = this.Result(name);

                _LazyObjectList.Add(name, noGoodSomeOne);
            }

            return noGoodSomeOne;
        }
    }
}

Az alábbiakban egy C++ példa.

#include <iostream>
#include <string>
#include <map>
 
using namespace std;
 
class Fruit {
    public:
        static Fruit* getFruit(const string& type);
        static void printCurrentTypes();

    private:
        static map<string,Fruit*> types;
        string type;

        // megjegyzés: a privát konstruktor kikényszeríti, hogy a statikus 
        // getFruit() függvénnyel kelljen objektum példányt létrehozni
        Fruit(const string& t) : type( t ) {}
};

// szükséges definíció a statikus tag-változó használatához
map<string,Fruit*> Fruit::types;        
 
/*
 * Lusta gyártó metódus, azt a 'Fruit' példányt veszi elő, amire a paraméterként
 * átadott névvel hivatkoztak. Létrehoz egy új példányt, ha szükséges.
 * előfeltétel: a 'type' paraméter valamilyen gyümölcs név legyen pl. "alma"
 * utófeltétel: visszaadja a 'Fruit' példányt, amire a névvel hivatkoztunk
 */
Fruit* Fruit::getFruit(const string& type) {
    // próbálunk keresni egy létező példányt; ha nem találunk, 
    // az std::map függvény 'types.end()' értéket ad vissza
    map<string,Fruit*>::iterator it = types.find(type);  
 
    Fruit *f;
    if (it == types.end()) { 
        // ha nem találtunk példányt a kért típusból, hozzunk létre egyet
        f = new Fruit(type); // lusta inicializációs rész
        // hozzáadjuk az újonnan létrehozott 'Fruit' példányt 
        // a 'types' asszociatív tömbhöz, hogy később megtaláljuk
        types[type] = f;     
    } else { // ha már van ilyen példányunk
        // a visszaadott érték a megtalált 'Fruit' példány lesz
        f = it->second; 
    }
    return f;
}

/*
 * Példaként lássuk a minta használatát
 */
void Fruit::printCurrentTypes() {
    if (!types.empty()) {
        cout << "A létrehozott példányok száma = " << types.size() << endl;
        for (map<string,Fruit*>::iterator iter = types.begin(); iter != types.end(); ++iter) {
            cout << (*iter).first << endl;
        }
        cout << endl;
    }
}

int main(void) {
    Fruit::getFruit("Banana");
    Fruit::printCurrentTypes();
 
    Fruit::getFruit("Apple");
    Fruit::printCurrentTypes();
 
    // az előzőleg már létrehozott példányt adja vissza 
    // ami a "Banana" paraméterrel való első hívásnál létrejött
    Fruit::getFruit("Banana");
    Fruit::printCurrentTypes();
 
    return 0;
}

/*
KIMENET:
A létrehozott példányok száma = 1
Banana
 
A létrehozott példányok száma = 2
Apple
Banana
 
A létrehozott példányok száma = 2
Apple
Banana
*/

Az alábbiakban egy Java példát mutatunk be.

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

public class Program {

    /**
     * @param args
     */
    public static void main(String[] args) {
        Fruit.getFruitByTypeName(FruitType.BANANA);
        Fruit.showAll();
        Fruit.getFruitByTypeName(FruitType.APPLE);
        Fruit.showAll();
        Fruit.getFruitByTypeName(FruitType.BANANA);
        Fruit.showAll();
    }
}

enum FruitType {
    NONE,
    APPLE,
    BANANA,
}

class Fruit {

    private static Map<FruitType, Fruit> types = new HashMap<>();
    
    /**
     * A privát konstruktor kikényszeríti a gyártó metódus használatát.
     * @param type
     */
    private Fruit(FruitType type) {
    }
    
    /**
     * Lusta gyártó metódus, azt a 'Fruit' példányt veszi elő, amire a paraméterrel
     * hivatkoztak. Létrehoz egy új példányt, ha szükséges.
     * @param type Bármilyen a típusnál megengedett gyümölcs név, pl. APPLE
     * @return A 'Fruit' példány, amire a típus-elem névvel hivatkoztunk
     */
    public static Fruit getFruitByTypeName(FruitType type) {
        Fruit fruit;
        // Ennek a programrésznek konkurencia problémái lehetnek.
        // Az alábbiakban a 'types'-hoz való hozzáférés nem szinkronizált, 
        // így a 'types.put' és a 'types.containsKey' egyszerre is meghívásra kerülhet.
        // Nem kell meglepődni, ha az adatok sérülnek... :)
        if (!types.containsKey(type)) {
            // Lusta inicializáció
            fruit = new Fruit(type);
            types.put(type, fruit);
        } else {
            // OK, már elérhető
            fruit = types.get(type);
        }
        
        return fruit;
    }
    
    /**
     * Lusta gyártó metódus, azt a 'Fruit' példányt veszi elő, amire a paraméterrel
     * hivatkoztak. Létrehoz egy új példányt, ha szükséges. Duplán ellenőrzött lockolás
     * mintát használ a konkurens hozzáférés biztonságossá tételére.
     * @param type Bármilyen a típusnál megengedett gyümölcs név, pl. APPLE
     * @return A 'Fruit' példány, amire a típus-elem névvel hivatkoztunk
     */
    public static Fruit getFruitByTypeNameHighConcurrentVersion(FruitType type) {
        if (!types.containsKey(type)) {
            synchronized (types) {
                // Újra ellenőrizzük a 'type' létezését, miután lockoltuk a 'types'-t
                // hogy biztosak legyünk benne, hogy nem került időközben létrehozásra
                // belőle példány egy másik programszálon
                if (!types.containsKey(type)) {
                    // Lusta inicializáció
                    types.put(type, new Fruit(type));
                }
            }
        }
        
        return types.get(type);
    }
    
    /**
     * Kiírja az összes létrehozott 'Fruit' példányt.
     */
    public static void showAll() {
        if (types.size() > 0) {
 
           System.out.println("A létrehozott példányok száma = " + types.size());
            
            for (Entry<FruitType, Fruit> entry : types.entrySet()) {
                String fruit = entry.getKey().toString();
                fruit = Character.toUpperCase(fruit.charAt(0)) + fruit.substring(1);
                System.out.println(fruit);
            }
            
            System.out.println();
        }
    }
}

Kimenet

A létrehozott példányok száma = 1
Banana

A létrehozott példányok száma = 2
Banana
Apple

A létrehozott példányok száma = 2
Banana
Apple

JavaScript

szerkesztés

Az alábbiakban egy JavaScript példa.

var Fruit = (function() {
  var types = {};
  function Fruit() {};

  // visszaadja a paraméterként kapott objektum 
  // tulajdonságainak(property) számát
  function count(obj) {
    return Object.keys(obj).length;
  }

  var _static = {
    getFruit: function(type) {
      if (typeof types[type] == 'undefined') {
        types[type] = new Fruit;
      }
      return types[type];
    },
    printCurrentTypes: function () {
      console.log('A létrehozott példányok száma: ' + count(types));
      for (var type in types) {
        console.log(type);
      }
    }
  };

  return _static;

})();

Fruit.getFruit('Apple');
Fruit.printCurrentTypes();
Fruit.getFruit('Banana');
Fruit.printCurrentTypes();
Fruit.getFruit('Apple');
Fruit.printCurrentTypes();

Kimenet

A létrehozott példányok száma: 1
Apple

A létrehozott példányok száma: 2
Apple
Banana

A létrehozott példányok száma: 2
Apple
Banana

Az alábbiakban egy példa a lusta inicializációra PHP 5-ben:

<?php
header('Content-type:text/plain; charset=utf-8');

class Fruit {
    private $type;
    private static $types = array();

    private function __construct($type) {
        $this->type = $type;
    }

    public static function getFruit($type) {
        // A lusta inicializáció itt történik
        if (!isset(self::$types[$type])) {
            self::$types[$type] = new Fruit($type);
        }

        return self::$types[$type];
    }

    public static function printCurrentTypes() {
        echo 'A létrehozott példányok száma: ' . count(self::$types) . "\n";
        foreach (array_keys(self::$types) as $key) {
            echo "$key\n";
        }
        echo "\n";
    }
}

Fruit::getFruit('Apple');
Fruit::printCurrentTypes();

Fruit::getFruit('Banana');
Fruit::printCurrentTypes();

Fruit::getFruit('Apple');
Fruit::printCurrentTypes();

/*
KIMENET:

A létrehozott példányok száma: 1
Apple

A létrehozott példányok száma: 2
Apple
Banana

A létrehozott példányok száma: 2
Apple
Banana
*/

?>

Az alábbiakban egy példa Python programozási nyelven (2.x verziójú Python).

class Fruit:
    def __init__(self, sort):
        self.sort = sort
    
class Fruits:
    def __init__(self):
        self.sorts = {}
    
    def get_fruit(self, sort):
        if sort not in self.sorts:
            self.sorts[sort] = Fruit(sort)
        
        return self.sorts[sort]

if __name__ == '__main__':
    fruits = Fruits()
    print fruits.get_fruit('Apple')
    print fruits.get_fruit('Lime')

Az alábbiakban egy példa Ruby nyelven, egy távoli szolgáltatástól származó (mint pl. a Google-é) autentikációs token lusta inicializálására.

require 'net/http'
class Blogger
  def auth_token
    @auth_token ||=
      (res = Net::HTTP.post_form(uri, params)) &&
      get_token_from_http_response(res)
  end

  # a 'get_token_from_http_response', az 'uri' és a 'params' később vannak definiálva az osztályban
end

b = Blogger.new
b.instance_variable_get(:@auth_token) # nil értéket ad vissza
b.auth_token # a tokent adja vissza
b.instance_variable_get(:@auth_token) # a tokent adja vissza

Az alábbiakban egy Smalltalk nyelvű példa, egy tipikus változó-elérést biztosító metódusra, amely a változó értékét lusta inicializáció használatával adja vissza.

    height
        ^height ifNil: [height := 2.0].

A fentiek 'nem-lusta' alternatívája egy inicializációs metódus használata, ami már akkor lefut, amikor az objektum létrejön. Ebben az esetben egy egyszerűbb változó-elérésre szolgáló metódus használható az érték kiolvasására.

    initialize
        height := 2.0

    height
        ^height

Megjegyezzük, hogy a lusta inicializáció a nem objektumorientált nyelvekben is használható.

A Scala programozási nyelvnek beépített támogatása van a lusta változó inicializációhoz.[1]

 scala> val x = { println("Hello"); 99 }
 Hello
 x: Int = 99
 scala> lazy val y = { println("Hello!!"); 31 }
 y: Int = <lazy>
 scala> y
 Hello!!
 res2: Int = 31
 scala> y
 res3: Int = 31

Külső hivatkozások (angolul)

szerkesztés

Fordítás

szerkesztés

Ez a szócikk részben vagy egészben a Lazy initialization című angol Wikipédia-szócikk ezen változatának 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.