Hello World
Spiga

趣味编程:C#中Specification模式的实现(参考答案 - 上)

2009-09-28 10:34 by 老赵, 13205 visits

Specification模式的作用是构建可以自由组装的业务逻辑元素。不过就上篇文章的示例来看,“标准”的Specification模式的实现还是比较麻烦的,简单的功能也需要较复杂的代码。不过,既然说是“标准”的方式,自然就是指可以在任意面向对象语言中使用的实现方式,不过我们使用的是C#,在实际开发过程中,我们可以利用C#如今的强大特性来实现出更容易使用,更轻量级的Specification模式。

当然,有利也有弊,在使用“标准”还是“轻量级”的问题上,还要根据你的需求来进行选择。

Specification模式的关键在于,Specification类有一个IsSatisifiedBy函数,用于校验某个对象是否满足该Specification所表示的条件。多个Specification对象可以组装起来,并生成新Specification对象,这便可以形成高度可定制的业务逻辑。从中可以看出,一个Specification对象的关键,其实就是一个IsSatisifiedBy方法的逻辑。每种对象,一段逻辑。每个对象的唯一关键,也就是这么一段逻辑。因此,我们完全可以构造这么一个“通用”的类型,允许外界将这段逻辑通过构造函数“注入”到Specification对象中:

public class Specification<T> : ISpecification<T>
{
    private Func<T, bool> m_isSatisfiedBy;

    public Specification(Func<T, bool> isSatisfiedBy)
    {
        this.m_isSatisfiedBy = isSatisfiedBy;
    }

    public bool IsSatisfiedBy(T candidate)
    {
        return this.m_isSatisfiedBy(candidate);
    }
}

嗯嗯,这也是一种依赖注入。在普通的面向对象语言中,承载一段逻辑的最小单元只能是“类”,只是我们说,某某类中的某某方法就是我们需要的逻辑。而在C#中,从最早开始就有“委托”这个东西可用来承载一段逻辑。与其为每种情况定义一个特定的Specification类,让那个Spcification类去访问外部资源(即建立依赖),不如我们将这个类中唯一需要的逻辑给准备好,各种依赖直接通过委托由编译器自动保留,然后直接注入到一个“通用”的类中。很关键的是,这样在编程方面也非常容易。

至于原本ISpecification<T>中的And,Or,Not方法,我们可以将它们提取成扩展方法。有朋友说,既然有了扩展方法,那么对于一些不需要访问私有成员/状态的方法,都应该提取到实体的外部,避免“污染”实体。不过我不同意,在我看来,到底是用实例方法还是扩展方法,还是个根据职责和概念而一定的。我在这里打算使用扩展的目的,是因为And,Or,Not并非是一个Specification对象的逻辑,并不是一个Specification对象说,“我要去And另一个”,“我要去Or另一个”,或者“我要造……取反”。就好比二元运算符&&、||、或者+、-,左右两边的运算数字有主次之分吗?没有,它们是并列的。因此,我选择使用额外的扩展方法,而不是将这些职责交给某个Specification对象:

public static class SpecificationExtensions
{
    public static ISpecification<T> And<T>(
        this ISpecification<T> one, ISpecification<T> other)
    {
        return new Specification<T>(candidate =>
            one.IsSatisfiedBy(candidate) && other.IsSatisfiedBy(candidate));
    }

    public static ISpecification<T> Or<T>(
        this ISpecification<T> one, ISpecification<T> other)
    {
        return new Specification<T>(candidate =>
            one.IsSatisfiedBy(candidate) || other.IsSatisfiedBy(candidate));
    }

    public static ISpecification<T> Not<T>(this ISpecification<T> one)
    {
        return new Specification<T>(candidate => !one.IsSatisfiedBy(candidate));
    }
}

此外,使用扩展方法的好处在于,如果我们想要加一个逻辑运算(如“异或”),那么是不需要修改接口的。修改接口是一件劳民伤财的事情

至此,我们使用Specification对象就容易多了,因为不需要为每段逻辑创建一个独立的ISpecification<T>类型。但是,其实还有更简单的:直接使用委托。既然整个Specificaiton对象的逻辑可以使用一个委托直接表示,那为什么我们还需要一个“外壳”呢?不如直接使用这样的委托类型:

public delegate bool Spec<T>(T candicate);

当然,您也可以直接使用Func<T, bool>。我在这里创建Spec的目的,是因为我想“明确”这里其实是一个Specification,而不是一个普通的“接受T作为参数,返回bool的方法”。于是现在,我们便可以用这样的扩展方法来编写And,Or和Not:

public static class SpecExtensions
{
    public static Spec<T> And<T>(this Spec<T> one, Spec<T> other)
    {
        return candidate => one(candidate) && other(candidate);
    }

    public static Spec<T> Or<T>(this Spec<T> one, Spec<T> other)
    {
        return candidate => one(candidate) || other(candidate);
    }

    public static Spec<T> Not<T>(this Spec<T> one)
    {
        return candidate => !one(candidate);
    }
}

用它来编写上次的示例便容易多了:

static Spec<int> MorePredicate(Spec<int> original)
{
    return original.Or(i => i > 0);
}

static void Main(string[] args)
{
    var array = Enumerable.Range(-5, 10).ToArray();
    var oddSpec = new Spec<int>(i => i % 2 == 1);
    var oddAndPositiveSpec = MorePredicate(oddSpec);

    foreach (var item in array.Where(i => oddAndPositiveSpec(i)))
    {
        Console.WriteLine(item);
    }
}

由于有C#的扩展方法和委托,在C#中使用Specification模式比之前要容易许多。不过,在某些时候,我们可能还是需要老老实实按照标准来做。创建独立的Specification对象的好处是在一个单独的地方内聚地封装了一段逻辑,因此适合较集中,较“重”的逻辑,而“委托”则适合轻便的实现。委托的另一个优势是使用方便,但它的缺点便是难以“静态表示”。如果您在使用Specification模式时,需要根据外部配置来决定进行何种组装,那么可能只有为每种逻辑创建独立的Specification对象了。此外,使用委托还有一个“小缺点”,即它可能会“不自觉”地提升对象的生命周期,可能会形成一些延迟方面的陷阱

当然,我并不是说独立Specification对象就不会造成生命周期延长——只要功能实现一样,各方面也应该是相同的。只不过独立的Specificaiton对象给人一种“正式”而“隆重”的感觉,容易让人警觉,因而缓解了这方面问题。

不过还有一个问题我们还没有解决——我们现在组装的是委托或Specification对象,但如果我们需要组装一个表达式树,组装完毕后交给如LINQ to SQL使用,又该怎么做呢?我们的“下”便会设法解决这个问题。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

12 条回复

  1. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-28 10:45:00

    韩信点兵 就看到个大旗

  2. dongzz
    *.*.*.*
    链接

    dongzz 2009-09-28 10:56:00

    问个问题,比如一个表达式:
    Expression<Func<int,bool>> f=(arg)=>arg==3;
    如果f作为参数传递到一个方法中,能不能在该方法中将f中的ConstantExpression,也就是3分析出来,当然这个ConstantExpression的值是不确定(我想根据ConstantExpression的值做缓存),谢谢大家。

  3. 老赵
    admin
    链接

    老赵 2009-09-28 10:57:00

    韦恩卑鄙:韩信点兵 就看到个大旗


    啥啥?

  4. 老赵
    admin
    链接

    老赵 2009-09-28 10:58:00

    @dongzz
    当然可以,用整个Expression结构作Key都可以缓存,我当时写了一系列文章讨论过的,可以搜索一下“表达式树缓存”。

  5. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-28 11:07:00

    Jeffrey Zhao:

    韦恩卑鄙:韩信点兵 就看到个大旗


    啥啥?


    传说韩信刚被萧何追回来 刘邦刁难他 让他在一张纸上画出所有的兵 画多少就给他多少。。。

    丫画了个城门 开条缝 里面伸出个大旗 上面写着"韩" 好像是千军万马要出征前 但是聂 一个兵没有 刘邦一看你这不是臭无赖么,埃我也是臭无赖 我最喜欢臭无赖 就给了丫十万


    民间传说。。。。


    你这篇太不过瘾了 感觉你还有一大堆东西在后头 卖关子ing

  6. 老赵
    admin
    链接

    老赵 2009-09-28 11:11:00

    @韦恩卑鄙
    没一大堆,就个“下”了。

  7. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-28 11:14:00

    那必须得是一篇很长~~~~的"下"
    不然你就赢得了我的bs 嘿嘿~~~

  8. dongzz
    *.*.*.*
    链接

    dongzz 2009-09-28 11:15:00

    @Jeffrey Zhao
    您那个系列当初没学Expression就跳过了,看了看确实很有启发。
    您提到的不同缓存空间怎么实现的?比如你说UserService与AdminService的不同缓存容器,我那个项目直接用全局的System.Web.Cache,本来考虑分布式缓存,但是因为只有一台服务器,就放弃了。

  9. 老赵
    admin
    链接

    老赵 2009-09-28 12:01:00

    @dongzz
    就比如在Enterprise Library中构造不同的CacheManager。

  10. dongzz
    *.*.*.*
    链接

    dongzz 2009-09-28 12:25:00

    @Jeffrey Zhao
    测试了下您的ExpressionCache Library,很满意,我想直接拿来用了,再次谢谢您。

  11. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-28 12:49:00

    提两个题外的小问题:
    1. 委托是不是在某种意义上可以看成是“轻量级”的类,这个类仅仅封装一段逻辑,即一个方法。因此很多设计模式的“轻量级”实现都可以用委托来完成。
    2. 使用接口+扩展方法,似乎可以部分替代抽象类,当然,除了override。

  12. 老赵
    admin
    链接

    老赵 2009-09-28 12:54:00

    @麒麟.NET
    嗯嗯,有了一些语言特性之后,在某些场景下可以提到原来的一些模式。
    至于你说抽象类,我就不清楚了。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我