SUMTEC -- There's a thing in my bloglet.

But it's not only one. It's many. It's the same as other things but it exactly likes nothing else...

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

我们考虑有以下类:

class Order

{

public Customer Customer;

        public double Price;

        public int Amount; 

}


class Customer

{

        public string Name;

        //... 


再假设有GetOrders()函数,可以获取订单集合。现在我们有一个任务:

获取所有订单总价值在10000元以上的客户的姓名。

 

如果我们用Linq写,代码如下: 

var orderList = GetOrders();

var keyAccountNames = from item in orderList

                                   where item.Price * item.Amount > 10000

                                   select item.Customer.Name;

keyAccountNames = keyAccountNames.Distinct();

 

如果没有Linq,我们可能得写成这样:

var orderList = GetOrders();

var keyAccountNames = new HashSet<string>();

foreach(var item in orderList)

    if (item.Price * item.Amount > 10000 &&

        keyAccountNames.Contains(item.Customer.Name)

        )

    {

        keyAccountNames.Add(item.Customer.Name);

    } 

 

这两个例子中,我们刨去GetOrders(),刨去花括号,使用Linq和不使用,代码行数的区别是4:5。这也许不是一个很大的区别,但首先这是一个最简单的示例,后面我会给出更多复杂的情况,差别就会更加巨大。和你理解的相反,引入Linq从总体上讲,并不会使得水平差的人理解困难,反而是提高可读性的。这是因为linq把我们需要考虑的问题,提高了一个抽象的层次。还是看上面的例子,我们可以看到,linq的描述比非linq描述更接近于本质。(其实,我还嫌linq语法里面缺少top、skip、distinct等,以至于我需要多出一个Distinct的调用。)如果我们阅读那段非Linq代码,很容易发现,里面涉及的是如何使用HashSet,以及通过循环做一件事情。这样的代码,阅读的时候还需要通过如何做的细节“揣测”开发者的原意是什么。或者如Jeffrey Zhao所说的,Linq让我们在写程序的时候,更加专注于描述“要干什么”,而不是“如何做一件事情”。

 

事实上,对于水瓶欠缺的人来说,使用非Linq的方式开发,很多时候会引入很多错误的实施细节,以至于以后阅读的时候很难猜测其原意。比如说,同样的代码,还可以写成:

 

 

var orderList = GetOrders();

var keyAccountNames = new List<string>();

foreach(var item in orderList)

    if (item.Price * item.Amount > 10000)

    {

        keyAccountNames.Add(item.Customer.Name);

    } 

 

var keyAccountNamesDistinct = new List<string>(); 

for(int i = 0; i < keyAccountNames.Count; i++)

{

    int j;

    for (j = 0; j < i; i++)

    {

        if (keyAccountNames[i] == keyAccountNames[j])

        {

            break; 

        } 

    } 

    if (i == j)

    {

        keyAccountNamesDistinct.Add(keyAccountNames[i]); 

    } 

 

上述写法也算不上错误,但是首先肯定性能会有问题,其次其目的含义更加“稀释”了——你需要读更多的代码,用你的大脑执行一些更加复杂的模拟过程,然后才能得出相同的结论。更不要说,我见过很多水平更低下的人会在这里面埋下Bug或者命名含义的问题。反之,用Linq来写,基本上不会出现更多不同的写法。

 

衡量一个编程语言的某个特征是好还是坏,我有一个很简单的办法:她给你带来的自由度是否恰当。或者说,每一个语言特性都会给你带来一定的开发自由,同时也有可能减少另一些自由。这些变化对于你所需要的工作是否合适,就显得很重要。就Linq而言,她给你带来了一些自由度(后面会讲到),但是同时却限制了一些不良的自由度,即如前面所述,限制了一些如何做的问题。把这如何做的细节封装起来,绝对是一个很好的事情。面向对象等等现代的开发方法,包括很多大师级人物,不都是提倡“将问题的细节封闭起来”吗?减少这种自由度,肯定会带来这么几个重要的好处:隐藏了实现细节,减少代码行数,提高可读性,进而减少产生Bug的机会。

 

当然了,减少自由度一定会带来另外一个问题,即,如果我想用不同的方式来实现,可能就悲剧了。幸好,Linq并非让你毫无机会更改实现方式,否则又怎么会有Linq2Sql、Linq2Object、Linq2Xml、Linq2EF甚至PLinq呢?这个问题有点复杂,我感觉可能在这里谈着一个部分很可能并不适合。

 

说到这里, 顺便再次纠正Linq==Linq2Sql的错误理解。尽管已经有很多人多次阐明了这个问题,还是有人会犯同样的错误,而且前仆后继。也许你会说:“我想说的应该是Linq2Sql……”但愿你不会这么说,因为这和你的说法是矛盾的“删除C#以下功能:7. Linq(去掉,还是用Sql语句+存储过程来的实际)”。Linq2Sql并不是C#的特性,而是一个类库,所以你这个要求可以说是风牛马不相及的。

 

其实,有可能你并不清楚Linq2Sql的意义,否则你可能就不会这么憎恨这个玩意儿了,而是会抱怨你们的团队为何无法使用这种东西了。我承认其实这种技术仍然不是很成熟,其具体的实现方式很可能有很多争议,但这并不妨碍其某些亮点。你的观点是,直接拼写sql来的更直观。确实,这毋庸置疑。我想很多人也会站在同样的立场上赞同这一观点,包括我自己。但是如果平衡了很多其它问题再来看,则未必是最好的。

 

我来写一下直接使用Sql的几个大问题:

1、Sql源代码管理的问题:要想和程序源代码继承管理,还是有点麻烦的;

2、部署的问题:显然,如果用存储过程,其部署和程序本身的部署就是两个过程。当然,可以做一个自动化的集成部署过程,但这确实带来的额外的复杂度;

3、开发和维护的问题:开发存储过程,通常需要专人来处理,越大的团队和项目,越是如此。人各有专攻,要找到Db开发能力与Code开发同样非常强悍的人,一个是难找,二个是成本高,三个是稀缺资源容易产生“等待”甚至是“死锁”。把这两部分的工作分别交由不同的人来开发,一定会带来更多的沟通问题,进而产生更多的稳定性、维护、可读性、概念统一性问题,最终导致效率问题;

4、运行效率问题: 如果不是由专门的人来写Sql,而是谁开发这段代码谁来写,还有可能涉及一个Sql是否有效率的问题(当然,到目前为止,如果你不做一些额外的开发工作,Linq2Sql不会解决这个问题,甚至有可能会恶化);

5、安全性问题:注入攻击等等,不仅限于此;

6、数据库抽象问题:不多说,分页如何实现,在不同的数据库中截然不同。传统方法必然面临开发,用linq则完全不需要去考虑,一定是根据实际数据库自动选择。 

7、纯粹开发的问题。

 

前面几个都很好理解,Linq都会有所改善,或者有可能加以改善。这里面解释一下最后一个存粹开发的问题。

考虑有这么一个任务:获取所有单个订单成交量在100个标准单位以上的客户对象。传统的做法是,在业务层设计一个GetCustomersAboveAmount的方法,在该方法里面执行Sql "Select c.* from customer as c inner join order as o on c.CustomerId = o.CusomerId where o.Amount > 100"。也许不是Sql,是存储过程。嗯,看上去很美,不是么?

 

接下来,有另一个需求:根据用户的姓名来排序,进行翻页。传统的做法是,再做一个GetCustomersAboveAmountOrderByName方法,该方法执行另一段根据用户姓名来排序分页的Sql,或者存储过程。原来的方法假如发现其实是设计不良,没有其它地方使用,则删除之。但如果还有地方需要用该方法,则不删除。目前为止,还凑活。

 

接下来,用户又提出了很多其他需求:根据年龄、所在地区、登录时间、所有成交订单总额等等进行排序分页……于是,就需要有一堆的GetCustomersAboveAmountOrderByXXXX的方法。现在问题就来了,随着业务需求变化,你不得不牵动整个系统:从UI到BLL到DAL。假如说这些不同的排序字段都在一个表,你还可以使用比较丑陋的sql拼接方式来完成(无论直接拼sql,还是在存储过程中)。但是如果涉及到另一个表,比如成交订单总额等,难度就会增加。不是说没机会,而是看着会更丑陋。

 

如果用Linq2Sql/EF,我只需要一个函数:

 

public IQueryable<Customer> QueryCustomerAboveAmount(int amount)
{

return from customer in entities.Customers

                  join order on order.CustomerId equal customer.CustomerId

                  where order.Amount > amount

                  select customer; 


而排序的需求则可以直接交给表现层去处理:

var customers = from customer in businessObject.QueryCustomerAboveAmount(10000)

                        order by customer.Name 

                        select item;

customers = customers.Skip(itemCountInOnePage * pageIndex).Take(itemCountInOnePage);

 

任何这方面的变化,都可以留到最后一个层面再去解决。这样做的好处有几个:

1、适应变化的成本更低。你不需要为此类变化触动业务逻辑,更不需要触动更底层的数据库(越往底层的东西,复杂度越高,牵扯的东西越多,越难以分割,沟通成本越高,代价越大);

2、可读性非常高,很好理解。QueryCustomerAboveAmount(10000),显然要比QueryCustomerAboveAmountOrderByName(10000, itemCountInOnePage, pageIndex)好理解;

3、机有效地降低了复杂性,尤其是业务逻辑层的。在考虑如何设计业务逻辑层之前,需要考虑清楚,什么才是你的业务。你可以说,所有逻辑都是业务逻辑,也可以说,最常见的、通用的、有意义的才是业务逻辑。业务逻辑层虽说业务的,业务也虽说是变化的,但是很多时候变化还是越少越好。如果把所有的逻辑都称之为业务逻辑,这一层会变得臃肿不堪不说,其变化频率也会非常高,最后就完全失去了分出着一个层次的意义了。在这里,我认为“根据单笔业务的数量来获取客户”可以姑且认为是一种业务需求。至于如何排序,则很难成为业务需求,因为排序的根据是在是太多了,变化起来也太容易了。甚至可以说这未必是业务的本质,而是为了方便观看。

 

这种问题,不知道使用拼Sql的方式,能做到有多优雅,我对此持以极大的怀疑。不过,上面我提到的一种解决办法,也未必是最好的,可能存在值得商榷的地方。不过,目前也确实在自己的项目中进行着尝试,目前看来还是达到一些预期效果的。(具体项目具体分析,如果你的项目很固定,都做到BLL中也未尝不可。如果你这些方面的需求变化非常明显和频繁,那将这种需求从业务逻辑中剔除,也未必是一件坏事。) 


最后说一句实话,从您的这篇文章中,我看到两个可能性:对C#,甚至是.NET体系的了解还是有欠缺;或者,语言组织不严谨,以致产生了很大的歧义,这在Linq上面表现最为明显。说实话,讨论这个层面的问题,需要很高的功力才能进行,稍有不慎就会露出极大破绽。如果不得已而需要讨论,最好还是一个一个来。如您的文章中一下提出要砍那么多的语言特性,真的是非常有胆量。这个招架起来,恐怕不是一件轻松的事情。

 

posted on 2010-06-21 15:07  Sumtec  阅读(2482)  评论(33编辑  收藏  举报