sobota, 14 lipca 2012

[Wzorce projektowe] Lazy Load

Analogia z życia:

Jednym z codziennie używanych produktów jest cukier. Idąc do sklepu możemy wpaść na pomysł, że zrobimy sobie zapas cukru na cały rok. Zabieramy zatem kilkanaście kilogramów i udajemy się do kasy. Zaletą takiego podejścia jest to, że nie musimy martwić się o to, czy nam zabraknie cukru przez wiele miesięcy, wadami kwota jaką płacimy, kłopoty z przetransportowaniem do domu i brak miejsca do składowania. Może się też zdarzyć, że za tydzień przestaniemy słodzić i wtedy cały zakup okaże się niepotrzebny.



Zastosowanie:

Wzorzec lazy load stosuje się wszędzie tam, gdzie pobranie obiektu wymaga dodatkowego ładowania danych, które nie są używane w momencie, kiedy korzystamy z obiektu. Do tego wzorca należy podchodzić ostrożnie, gdyż np. zbyt wiele zapytań do bazy danych może się okazać niekorzystne pod względem wydajności dla całej aplikacji. Zatem decydując się na zastosowanie tego wzorcu należy brać pod uwagę zarówno ilość zapytań, jak i wielkość danych, jakie zwracają te zapytania. Dzięki odpowiedniej implementacji wzorzec zapewni nam, że dane będą ładowane w momencie, kiedy zajdzie potrzeba ich użycia, a jeśli taka potrzeba nie zajdzie to nie załadowane zostaną wcale (z korzyścią dla wydajności aplikacji).

Zasada działania:

Wzorzec można zaimplementować na cztery sposoby.

Lazy Initialization:

Jest to najprostsze podejście, które wykorzystuje propercje z "backing field". W momencie, gdy odczytujemy wartość propercji, sprawdzane jest czy zainicjowane zostało już odpowiadające jej pole, a jeśli nie, to inicjalizujemy je. Wadą tej metody jest fakt, iż inne operacje klasy nie mogą odczytywać wartości w żaden inny sposób niż przez propercję (pole może nie być zainicjalizowane). W języku C# przy implementacji Lazy Initialization można użyć operatora ??

public class LazyInitializedObject
    {
        private HeavyObject _obj;
        public HeavyObject Obj
        {
            get
            {
                if(_obj == null)
                    _obj = new HeavyObject();
                return _obj;
            }
        }

        private HeavyObject _obj2;
        public HeavyObject Obj2
        {
            get { return _obj2 ?? (_obj2 = new HeavyObject()); }
        }
    }

    public class HeavyObject
    {
        public HeavyObject()
        {
            //symulacja długich operacji odczytu z bazy danych
            Thread.Sleep(300);
        }
    }  

Virtual Proxy:

Ideą tej metody jest zastosowanie klasy, która wygląda dla klienta jak obiekt, który nas interesuje, ale tak na prawdę klasa proxy steruje dostępem do właściwego obiektu. Problemem jest tutaj tożsamość obiektu proxy, który klient może porównywać z innymi rzeczywistymi obiektami. Można ten problem rozwiązać przeładowując metody Equals i GetHashCode. Jeśli chodzi o implementację, to warto zastosować prostą fabrykę. Klient wysyła żądanie do fabryki o nowy obiekt. Fabryka zwraca obiekt proxy, który zawiera w sobie obiekt właściwy. Proxy steruje logiką przy wołaniu usług z właściwego obiektu, a także zapewnia tożsamość obiektu.


public class Student
    {
        public Student()
        {
            //symulacja pobierania danych
            Thread.Sleep(300);
        }
    }

    public class StudentBook
    {
        public int Identity { get; set; }

        public virtual Student Entity { get; set; }

        public override int GetHashCode()
        {
            return Identity.GetHashCode();
        }
    }

    public class StudentBookProxy : StudentBook
    {
        public override Student Entity
        {
            get
            {
                if(base.Entity == null)
                    base.Entity = new Student();
                return base.Entity;
            }
            set
            {
                base.Entity = value;
            }
        }

        public override bool Equals(object obj)
        {
            var st = obj as StudentBook;
            if (st == null) return false;
            return st.Identity == this.Identity;
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }

    public class StudentBookFactory
    {
        public StudentBook GetFromId(int id)
        {
            return new StudentBookProxy()
                       {
                           Identity = id
                       };
        }
    }

Value Holder:

W tej wersji klient ma świadomość funkcjonalności leniwego ładowania, zatem usługi wywoływane są z pełną świadomością typu Value Holdera. Korzystając z dot Neta w wersji 3.5 lub wyższej można do tego celu stworzyć generyczny interfejs, który zwraca obiekt, np poprzez metodę Load. Implementacja metody load może być dokonana na wiele sposobów. Tworzy się także klasę generyczną, która zawiera propercję z obiektem, który nas będzie interesował.


public interface IValueLoader<T>
    {
        T Load();
    }

    public class ValueHolder<T>
    {
        private T _value;
        private readonly IValueLoader<T> _loader;

        public T Value
        {
            get
            {
                if (_value == null)
                    _value = _loader.Load();
                return _value;
            }
        }

        public ValueHolder(IValueLoader<T> loader)
        {
            _loader = loader;
        }
    }

    public class SimpleLoader : IValueLoader<List<Employee>>
    {
        public List<Employee> Load()
        {
            Thread.Sleep(100);
            return new List<Employee>();
        }
    }

    public class Employee
    {
        public int Salary { get; set; }
    }

    public class Salaries
    {
        public int Id { get; set; }

        public Salaries(int id)
        {
            Id = id;
        }

        private ValueHolder<List<Employee>> _employees;

        public List<Employee> GetEmployees()
        {
            return _employees.Value;
        }

        public void SetEmployees(ValueHolder<List<Employee>> loader)
        {
            _employees = loader;
        }
    }

    public class SalariesFactory
    {
        public Salaries GetItem()
        {
            var salaries = new Salaries(1);
            salaries.SetEmployees(
                new ValueHolder<List<Employee>>(new SimpleLoader()));
            return salaries;
        }

    }

Ghosts:

Duchem nazywany jest obiekt znajdujący się w stanie częściowym, początkowo obiekt taki zawiera jedynie Id. W momencie, kiedy odczytywana jest którakolwiek z propercji, obiekt ładuje cały swój stan. Można powiedzieć, że obiekt stanowi zatem wirtualne proxy sam dla siebie, przez co nie pojawiają się problemy z tożsamością obiektu. Obiekt może zatem znajdować się w trzech stanach : załadowany, w trakcie ładowania oraz duch. Implementację wzorca można pozostawić klasie abstrakcyjnej, natomiast reprezentację stanu wartości typu wyliczeniowego.


public class Customer : DomainObject
    {
        public Customer(int id) : base(id)
        {
            
        }

        private string _name;
        public string Name
        {
            get
            {
                Load();
                return _name;
            }
            set
            {
                Load();
                _name = value;
            }
        }

        private int _age;
        public int Age
        {
            get
            {
                Load();
                return _age;
            }
            set
            {
                Load();
                _age = value;
            }
        }

        private List<int> _orderCosts;
        public List<int> OrderCosts
        {
            get
            {
                Load();
                return _orderCosts;
            }
            set
            {
                Load();
                _orderCosts = value;
            }
        }

        public override ArrayList GetDataRow(int id)
        {
            //symulacja pobierania z bazy danych, np.
            //SELECT * FROM CUSTOMERS WHERE CustomerID = id
            var al = new ArrayList();
            al.Add("Marek Nowak");
            al.Add(31);
            al.Add(new List<int> {1, 7});
            return al;
        }

        public override void InjectLineIntoObject(ArrayList arrayList)
        {
            //Mapowanie row na propercje
            Name = arrayList[0] as string;
            Age = int.Parse(arrayList[1].ToString());
            OrderCosts = arrayList[2] as List<int>;
        }
    }

    public abstract class DomainObject
    {
        public int Id { get; set; }
        private LoadStatus Status { get; set; }

        protected DomainObject(int id)
        {
            Id = id;
        }

        public bool IsGhost
        {
            get { return Status == LoadStatus.Ghost; }
        }

        public bool IsLoading
        {
            get { return Status == LoadStatus.Loading; }
        }

        public bool IsLoaded
        {
            get { return Status == LoadStatus.Loaded; }
        }

        public void Load()
        {
            if (!IsGhost) return;
            Status = LoadStatus.Loading;
            var row = GetDataRow(1);
            InjectLineIntoObject(row);
            Status = LoadStatus.Loaded;
        }



        public abstract ArrayList GetDataRow(int id);
        public abstract void InjectLineIntoObject(ArrayList arrayList);

    }

    public enum LoadStatus
    {
        Ghost,
        Loading,
        Loaded
    }
Na koniec warto wspomnieć o klasie Lazy, która również zapewnia opisane powyżej mechanizmy. Opis tutaj

Brak komentarzy:

Prześlij komentarz