原文:http://blogs.msdn.com/kfarmer/archive/2007/11/06/linq-and-3-tier-dogma.aspx
One of the most frequent questions we've received about LINQ to SQL deals with fitting it into the classic three-tiered scenario. That is:
Presentation --- Logic --- Data
I know someone somewhere is going to accuse me of heresy for what I'm about to state, but it's something I've wanted to get off my chest for a couple years, since I first encountered LINQ to SQL, and started to realize what it meant...
The 3-Tier Model is just a pattern.
And, like all patterns,
It is meant to serve our needs, not the other way around.
Don't get hung up on academic concerns such as how many tiers you have in your system. You'll just waste the time you could spend doing things you actually need to do. Such as watching Avatar or playing Warcraft.
There, I said it.
And saying that, I think LINQ to SQL fits perfectly well into the 3-Tier Model.
The spirit of the 3TM is that, in keeping your layers seperate, you achieve the ability to swap out data access, business logic, or presentation interfaces without disturbing the other two (or the functionality that depends on them). This, without dispute, is a useful thing. But there are various levels of purity that people seem to think apply:
- contractual purity (there is a set of well-known interfaces between the tiers)
- binary purity (the types are completely seperate in each tier, *and* there is a contract between each)
Frequently, I've seen people get hung up on the idea of binary purity. I'll say it: I don't think it honestly matters. Within the scope of a single project (and I'll submit that a web service and a web client are in fact two different projects), there is no gain in creating parallel sets of objects just for the sake of doing so. That is, there is no longer a compellingly good reason, and in fact I now consider it questionable design, in having "ProductDataObject" and "ProductLogicalObject" in the same project, when the shapes of the two are the same. You'll spend all your time copying property values back and forth, your references won't track well, and you'll forgo any change- or identity- tracking that the data layer happens to give you.
Don't underestimate the value of tracking features in a data layer.
So, let's just ditch the idea of binary purity for purity's sake; it's only real use is when transmitting data packets to foreign systems. Instead, let's talk about contractual purity.
Contractual purity is great. I like it a lot, and I think this is where the true heart of n-tier modelling is to be found. The only question is how you define your contracts.
Really, that's up to you.
Consider LINQ to SQL for a moment. LINQ to SQL was designed for those situations where the schema of your database matches the shape of data in what we'll loosely call your "logic" or "business" layer. (Loosely, because there can be many layers to this, or none at all for a dumb reporting app.) That is:
Presentation --- Logic --- Data
A --- B --- B
LINQ to SQL presents us with this:
A --- B === B
LINQ to SQL's already encapsulated your database access code (that is why it was written); it's all contained in System.Data.Linq.dll and your mapping metadata. It also already talks in terms of your logical model, and that's fine because we already said that was what LINQ to SQL was designed to do.
That leaves us with the question of what we use for presentation. Again, that's entirely up to you. If your project is such that is doesn't require formal contracts, and if the data being displayed has the same shape as what's available, why *not* use your logical model directly?
ui.DataSource = db.Customers;
B === B === B
More often, of course, you'll be displaying composite data, and the shape of that will be somewhat different than your logical data. Fine. We can do that: project into anonymous types (eg, if you don't need to access the properties directly -- if your UI does reflection), or project into a contractually-defined type, and return that:
ui.DataSource = db.Customers.Select(c => new { c.LastName, c.FirstName });
ui.DataSource = db.Customers.Select(c => new CustomerName { Last = c.LastName, First = c.First });
A --- B === B
Ah, you say. You feel its sinful to even expose the tables on your data context. Okay, fine. Remove them. In fact,
Encapsulate your data context.
That is,
public class MyLogicLayer: IDisposable // MyLogicLayer is IDisposable because MyDataContext is IDisposable
{// We retain an instance of the context. It's not static, because contexts are designed to be short-lived.
MyDataContext db;// We want to update data, right?
public void SubmitChanges()
{db.SubmitChanges();
}
// I prefer IQueryable, because I want my filtering and sorting to happen at the server, not the client.
// That is, I want to write: logicLayer.GetCustomersInWa().OrderBy(c => c.Age)
public IQueryable<Customer> GetCustomersInWA()
{return db.Customers.Where(c => c.State == "WA").OrderBy(c => c.LastName).ThenBy(c => c.FirstName);
}
// TVFs are a much more useful alternative to sprocs. In particular, they're composable.
public IQueryable<Customer> TVF_GetCustomersInWA()
{return db.GetCustomersInStateTVF("WA").OrderBy(c => c.LastName).ThenBy(c => c.FirstName);
}
// Of course, getting a single customer is useful
public Customer GetCustomer(int id)
{return db.Customers.Where(c => c.CustomerId == id);
}
// Maybe some admin doesn't want you to know about the customer's birthdate. So make a trimmed-down ICustomer,
// and return based on that interface.
public ICustomer GetCustomer(int id)
{// where the interface is sufficient protection
return db.Customers.Where(c => c.CustomerId == id);or
// where CustomerWithoutBirthdate implements ICustomer, and whose properties defer to the contained Customer
return db.Customer.Where(c => c.CustomerID == id).Select(c => new CustomerWithoutBirthdate(c));}
}
Which, interestingly, looks like:
A === [ A --- B ] === B
B === [ B === B ] === B
B' === [ B' === B ] === B
.. depending on which method you call. In other words,
The encapsulation of the data context is the creation of the logical layer.
I'm sure that in the upcoming years, there will be plenty of other ways people will find to adapt the traditional architectures to new innovations, and I'm certain there will be further innovations that make what I just proprosed easier to swallow. And that's what makes programming interesting. If we have indeed solved all the problems and programming is perfect forever, where's the fun in that?