Question

Let's say I am designing business objects around a poorly optimized database that have no ability to change.

I have a Person object with a ShippingAddress property. The ShippingAddress is very expensive to load and located in a separate table, and a minority of pages use it. There are times when thousands of Person objects need to be loaded on a single page so eating the performance hit is not an option. This makes lazy loading the ShippingAddress ideal for this situation.

However, from a business logic/consumer/signature perspective there is no reason ShippingAddress should be any different the eagerly loaded Name property. For this reason, I have decided to make ShippingAddress' public property signature a regular string while its private backing field is C#'s Lazy. The question is, how should I set it? I have come up with two options that maintain separation of data access logic:

Option 1, using overloaded java style set method seems like better OO and more consistent from a internal perspective since I leverage method overloading:

    Lazy<string> _address;
    public string Address { get { return _address == null ? null : _address.Value; } }
    public void SetAddress(Func<string> address)
    {
        _address = new Lazy<string>(address);
    }
    public void SetAddress(string address)
    {
        SetAddress(() => address);
    }

Option 2 seems much better/consistent from a consumer's perspective, but the two setting methods seem very disjointed in a way that bothers me:

    Lazy<string> _address;
    public string Address 
    { 
        get { return _address == null ? null : _address.Value; }
        set { SetAddress(() => value); }
    }
    public void SetAddress(Func<string> address)
    {
        _address = new Lazy<string>(address);
    }

Does anyone with more experience than I (Most of you I assume) using C#'s lazy class have any ideas as to which is better and why?

Was it helpful?

Solution

When it comes to a BO, the consumer should not see field is lazy loaded or not and it's better to present as any other property (without exposing methods accepting Func parameters)., I'd rather go with the second approach without having the public method SetAddress

Lazy<string> _address;

public string Address 
{ 
    get { return _address == null ? null : _address.Value; }
    set { _address = new Lazy<string>(() => value); }
}

OTHER TIPS

First, an alternative

The ShippingAddress is very expensive to load and located in a separate table, and a minority of pages use it.

In that case, perhaps ShippingAddress doesn't belong in Person in terms of the Business Object model? For the cases where you need to involve ShippingAddress as a first-class concept then you can instead define a ShippingAddress business object, which can of course have a Person as a dependency if needs be, or better yet just represent Shipping Addresses as a service, load them when needed (based on PersonID or whatever) and directly attach them to the Person instance.

Second, an exceptional case

If that doesn't / can't float your goat, then let's stick with Lazy Loading. I slightly disagree with your statement that "from a business logic/consumer/signature perspective there is no reason ShippingAddress should be any different the eagerly loaded Name property".

I totally see the perspective; and in general I would agree - except that in this case you have a specific, real-world situation (horribly slow access to the Shipping Address data store, cannot be changed, these things happen) that unfortunately defies the normal expectancy that if you've seen one getter, you've seen 'em all :)

So I would say that a client accessing the Address** property would be surprised to find that it actually involves a separate database hit, especially considering

  • The other properties don't
  • This is a property on a Business Object, not an obviously "connected" entity.

TL;DR - don't violate POLA - consider what might happen if someone throws a bunch of Person objects into an XmlSerialiser....

Third, a suggested approach

If you are going to use lazy loading from the database for Address, I'd recommend one of two approaches, both of which involve making the actual loader itself an explicitly modelled abstraction.

1. Make the loader function an explicit dependency of Person

public Person(int name, etc, IShippingAddressLoader addressLoader)

2. Make the loader function an explicit dependency of a LoadAddress() method

public void LoadAddress(IShippingAddressLoader addressLoader) { _address=addressLoader.Load(_personID); //or whatever; }

My reasoning is

  • In both ways, I think it's more obvious to client code using the Person class that there is "extra" code involved in getting the Shipping Address; and as a bonus it makes the Person class much more unit-testable because you can mock or stub the loader.

  • With either approach you don't need to declare _address itself as Lazy because you have shifted the "laziness" onto an explicit concept (the loader).

  • When you do find a nice way to cache all of the Addresses (maybe your server gets more RAM, so now it's feasible to load them all into memory at app startup, slap a SqlDependency on them and watch for changes), you only need to change the IShippingAddressLoader implementation, not the Person class.

Finally, a sneaky attempt

to sell you the first idea by suggesting that you just accept that Shipping Address is a tricky piece of data in your case, and that any code that had to deal with it (controllers or whatnot) just get their own IShippingAddressService service, e.g

public ShippingConfirmationModel GenerateConfirmationModel(Person person)
{
    var shippingAddress = _shippingAddressService.GetByPersonID(person.ID);
    person.SetShippingAddress(shippingAddress)
    return new ShippingConfirmationModel(person);
}

This frees you from all shenanigans related to lazy loading in the Person class, allows you to implement caching really easily, and also allows you to pass Person objects around safe in the knowledge that you won't accidentally tip over the database when someone decides to serialise all the people :)

**Minor note - if this is the Person's "ShippingAddress" then I would make sure to refer to it always as "ShippingAddress" in the code, as opposed to just "Address" :)

Licensed under: CC-BY-SA with attribution
scroll top