Introduction
Implementing the Lazy Loading design pattern in your application comes in handy when you're dealing
with large quantities of objects.
Background
Let's imagine that some data is provided to you by a database or by a service, and let's suppose that you're going to work with objects that have a huge amount of related objects.
Loading all of them in one fell swoop when your application starts may significantly hurt performances.
What if you could load those objects only when you actually need them? That's what Lazy Loading is for.
Using the Code
There are four main ways through which you can implement the Lazy Loading pattern:
- Lazy Initialization
- Virtual Proxies
- Value Holders
- Ghost Objects
Lazy Initialization
The Lazy Initialization technique consists of checking the value of a class' field when it's being accessed: if that value equals null
,
the field gets loaded with the proper value before it is returned. Here is an example:
public class Customer
{
public int CustomerID { get; set; }
public string Name { get; set; }
private IEnumerable<Order> _orders;
public IEnumerable<Order> Orders
{
get
{
if (_orders == null)
{
_orders = OrdersDatabase.GetOrders(CustomerID);
}
return _orders;
}
}
public Customer(int id, string name)
{
CustomerID = id;
Name = name;
}
}
Pretty simple, isn't it? However, there's actually an alternative method through which we can accomplish the same goal.
The .NET Framework defines a class named
Lazy<T>, which itself incorporates the Lazy Loading behaviour.
The Lazy<T>
class needs two basic information regarding the object that's going to be "lazily" initialized:
- Its type (specified in the type parameter)
- How to load its value (i.e. which function should be called)
Using the parameterless constructor, the operations performed when the Value
property of the Lazy<T>
class is being accessed are the ones
contained in the default constructor of the object whose type is specified in the "T
" parameter. Here's an example below:
Lazy<IEnumerable<Order>> lazyOrders = new Lazy<IEnumerable<Order>>();
IEnumerable<Order> actualOrders = lazyOrders.Value;
The default constructor for the Order
class is invoked, and
actualOrders
gets filled.
If we wanted to invoke a custom function, instead, we would simply pass that same function in the constructor. That's exactly what I did in my example:
Lazy<IEnumerable<Order>> lazyOrders = new Lazy<IEnumerable<Order>>(MyFunction);
IEnumerable<Order> actualOrders = lazyOrders.Value;
Virtual Proxy
A Virtual Proxy represents an object that is quite similar to the one that we wish to lazily initialize (they implement the same interface),
yet substantially different. The proxy object, indeed, is actually empty. When the user tries to access the proxy object for the first time, the Virtual Proxy creates
a new instance of the real object and properly fills it (this can be seen as an On Demand initialization).
One of the disadvantages of this approach is that you may need to create lots of virtual proxies if you're going to proxy a large number of objects.
Note: The user is obviously unaware that they're dealing with the "proxy" object instead of with the "real" one.
Value Holder
Adopting a Value Holder, instead, you basically set up a sort of surrogate for the real object. When the user needs to access it,
they simply ask the value holder for its value by calling the GetValue
method. At that time (and only then), the value gets loaded from a database or from a service.
The main drawback of this approach is that the user has to know that a value holder is expected.
Here's an example definition of a value holder:
public class ValueHolder<T>
{
private T _value;
private readonly Func<object, T> _valueRetrieval;
public ValueHolder(Func<object, T> valueRetrieval)
{
_valueRetrieval = valueRetrieval;
}
public T GetValue(object parameter)
{
if (_value == null)
_value = _valueRetrieval(parameter);
return _value;
}
}
Ghost Object
A Ghost Object corresponds to the real object, but not in its full state. It may be empty, or it may contain just some fields (such as the ID).
When the user tries to access some fields that haven't been loaded yet, the ghost object fully initializes itself (this is not always needed).
History
- 07-14-2013: First version.