Hello World
Spiga

语言特性与API设计

2009-09-25 17:12 by 老赵, 11802 visits

我平时的主要工作之一,便是编写一些基础及通用的类库,能够在项目中大量复用。换句话说,我的工作目的,是让其他开发人员可以更好地完成工作。因此,如何设计更容易使用的API是我经常要考虑的东西,偶尔也会有一些体会。而现在这些内容,是我在为Functional Reactive Programing写“参考答案”的时候忽然“总结”出来的想法。可能比较简单,但我想也是设计API是需要考虑的一些内容。

在那篇文章里,我们是在为IEvent<T>对象提供一些封装,其中会有MapEvent和FilterEvent等类型,为了方便调用,我们还定义了对应的扩展方法:

public class MapEvent<TIn, TOut> : InOutEventBase<TIn, TOut>
{
    public MapEvent(Func<TIn, TOut> mapper, IEvent<TIn> inEvent)
        : base(inEvent)
    {
        ...
    }
}

public class FilterEvent<TEventArgs> : InOutEventBase<TEventArgs, TEventArgs>
{
    public FilterEvent(Func<TEventArgs, bool> predicate, IEvent<TEventArgs> inEvent)
        : base(inEvent)
    {
        ...
    }
}

public static class EventExtensions
{
    public static MapEvent<TIn, TOut> Merge<TIn, TOut>(
        this IEvent<TIn, TOut> ev, Func<TIn, TOut> mapper)
    {
        ...
    }

    public static FilterEvent<TEventArgs> Filter<TEventArgs>(
        this IEvent<TEventArgs> ev, Func<TEventArgs, bool> predicate)
    {
        ...
    }
}

MergeEvent和FilterEvent都是对另一个Event对象的封装,您可以当作一种装饰器模式来考虑。不知您观察到没有,这个“待封装”的Event对象在不同的地方(构造函数或扩展方法),出现的位置是不同的。在扩展方法中,它是作为第一个参数出现在参数列表中,而在构造函数中它则是第二个参数。对于扩展方法来说,它是由语言规范强制得出的。但是在构造函数中,这出现的顺序完全可有由我们“自由”确定。那么,我们能否将待封装的Event对象作为构造函数的第一个参数呢?

自然是可以的,只是我在这里倾向于放在最后。原因在于这有利于API使用时的清晰。

假如我们没有扩展方法,也就是说只能使用构造函数进行“装饰”,那么使用现在则是:

var ev =
    new MapEvent<int, string>(
        i => i.ToString(),
        new FilterEvent<int>(
            i => i < 10,
            new MapEvent<DateTime, int>(
                d => d.Millisecond,
                ...)));

有的时候,我会将Lambda表达式写在上一行,这样可以让代码更为紧凑。那么如果MapEvent和FilterEvent都把待封装的Event对象作为构造和函数的第一个参数,又会怎么样呢?

var ev =
    new MapEvent<int, string>(
        new FilterEvent<int>(
            new MapEvent<DateTime, int>(
                ...,
                d => d.Millisecond),
            i => i < 10),
        i => i.ToString());

对比这两者,在我看来它们的信息“呈现方式”是有显著差距的。对于第一种情况(Event作为构造函数最后一个参数),用户看到这个定义时,从上到下的阅读顺序是:

  1. 构造一个MapEvent对象,映射方式是XXX
  2. 包含一个FilterEvent对象,过滤条件是YYY
  3. 包含一个MapEvent对象,映射方式是ZZZ

而对于第二种情况(Event作为构造函数的第一个参数):

  1. 构造一个MapEvent对象
  2. 包含一个FilterEvent对象
  3. 构造一个MapEvent对象
  4. 最内层MapEvent的映射方式为ZZZ
  5. 上一层FiterEvent……
  6. ……

第一种情况,API体现出的信息是流畅的,而第二种情况信息的体现是回溯的。第一种信息如“队列”,而第二种如“栈”。第一种API阅读起来用户视线是单向的,而第二种API用户可能会去努力寻找某个Lambda表达式到底对应着哪个对象——就像我们为什么提倡if/for不应该嵌套太深,因为找匹配的大括号的确是件比较麻烦的事情。我想,应该没有会选择把Event对象放在构造函数参数列表的中间吧(如果有3个及参数),因为这会让API调用看起来成“锯齿状”,实在不利于阅读。

因此,在各种需要“装饰”的场合,我往往都把“被装饰者”作为构造函数的最后一个参数。例如我在构造DomainRoute的时候,便也是把innerRoute作为构造函数的最后一个参数,由于DouteRoute所需要的参数较多,因此如果把innerRoute作为第一个参数,看起来会更加不便一些。同样的,在之前设法“拯救C# 2.0”的时候也使用了这个做法。

当然,这些是我个人的看法,并非所有人都是这样做的。例如在.NET Framework中负责GZip压缩的GZipStream对象,它的构造函数便是将innerStream作为第一个参数出现。幸好,C# 3.0中已经有了扩展方法,如果使用构造函数的话,即使信息再流畅,我想也不如扩展方法来的直观。因此,我一般都会利用扩展方法,让开发人员可以编写这样的API:

dateEvent.Map(d => d.Millisecond).Filter(i => i < 10).Map(i => i.ToString())
route.WithDomain("http://www.{*domain}/blogs", new { ... });
stream.GZip(CompressionMode.Compress).Encrypt(...);

其实许多高级语言都会为了让代码写的更易懂更清晰,因而提供一些看似“语法糖”的东西。例如F#中的|>操作符:

let form = new Form(Visible = true, TopMost = true, Text = "Event Sample")
form.MouseDown
    |> Event.merge form.MouseMove
    |> Event.filter (fun args -> args.Button = MouseButtons.Left)
    |> Event.map (fun args -> (args.X, args.Y))
    |> Event.listen (fun (x, y) -> printfn "(%d, %d)" x y)

其实|>操作符的目的只是把函数的最后一个参数调到之前来,但它能让我们写出“易读”的代码。例如FsTest类库允许我们这样写:

"foo" |> should equal "foo"

但其实,从理论上说,这种写法完全等价于:

should equal "foo" "foo"

正是因为有了|>操作符,F#在这种情况下会将待封装的Event对象作为函数的最后一个参数。这便是语言特性对API设计的影响。此外,F#中的“>>”以及Haskell的“.”可用“`”把一个函数作为中缀操作符来使用。但如果是Java这样的语言,由于缺乏一些灵活的语法特性,开发人员就只能靠框架和类库来构建“Fluent Interface”来度过难关了(如Google Collections)。《卓有成效的程序员》一书中举了这么一个例子,它们为一个Car对象的构造编写了流畅接口:

Car car = Car.describedAs().
             .box()
             .length(50.5)
             .type(Type.INSULATED)
             .includes(Equipment.LADDER)
             .lining(Lining.CORK);

以代替呆板的Java语法:

Car car = new CarImpl();
MarketingDescription desc = newMarketingDescriptionImpl();
desc.setType("Box");
desc.setSubType("Insulated");
desc.setAttribute("length", "50.5");
desc.setAttribute("ladder", "yes");
desc.setAttribute("lining type", "cork");
car.setDescription(desc)

似乎程序员永远不会放弃这方面追求:编写更清晰,更易懂的代码。

Creative Commons License

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

Add your comment

44 条回复

  1. 侯垒
    *.*.*.*
    链接

    侯垒 2009-09-25 17:19:00

    先占个沙发。

  2. Funeral
    *.*.*.*
    链接

    Funeral 2009-09-25 17:20:00

    占个板凳再读

  3. Forrest Liu
    *.*.*.*
    链接

    Forrest Liu 2009-09-25 17:25:00

    勇敢的插入前5~~

  4. yankai
    *.*.*.*
    链接

    yankai 2009-09-25 17:33:00

    放学来瞧瞧。

  5. 道法自然
    *.*.*.*
    链接

    道法自然 2009-09-25 17:39:00

    我毕业之后,除了在课余时间设计类库的时候用C#,很少用了,因此一直还停留在DOTNET 1 和 2。最近刚刚捡起3.5,对于DOTNET 3.5,有很多疑问,顺便请教一下。

    1 一堆串联的函数
    Car car = Car.describedAs().
    .box()
    .length(50.5)
    .type(Type.INSULATED)
    .includes(Equipment.LADDER)
    .lining(Lining.CORK);
    在LinqToObject和Hibernate也有很多类似的语句,但是在异常管理里面,这种将一堆函数关联在一起会引起一场管理麻烦,这样做的优点是什么呢?

    2 大量使用Lambda表达式
    MSDN里面说了,Lambda一般不能超过3行,不过我看过很多超过3行的语句,另外,Lambda类似代理,理解上不是很直观,这玩意有什么优点和使用注意点呢?

  6. killkill
    *.*.*.*
    链接

    killkill 2009-09-25 17:39:00

    代码是给人看的,同意这种做法。

  7. pangxiaoliang[北京]流浪者
    *.*.*.*
    链接

    pangxiaoliang[北京]流浪者 2009-09-25 17:39:00

    为什么赵老大写的东西都这么不平民化那,还是喜欢Headfirst的风格,嘿嘿~~~~~~~~

  8. 老赵
    admin
    链接

    老赵 2009-09-25 17:45:00

    @pangxiaoliang[北京]流浪者
    哪里不平民化了啊,这些东西我觉得都是很明白的,不是吗?我只是提一下。

  9. 老赵
    admin
    链接

    老赵 2009-09-25 17:47:00

    @道法自然
    1、异常管理是指什么啊?Fluent interface的好处在于声明式不是命令时,让代码更为清晰。
    2、Lambda表达式一般不超过3行,什么是“行”?MSDN上的描述能让我看一下吗?你说的“代理”是指“委托”吧,我认为用Lambda表达式构造委托很清晰很直观,可能是习惯问题吧。

  10. 戏水
    *.*.*.*
    链接

    戏水 2009-09-25 17:48:00

    @ 老赵

    幸好,C# 3.0中已经有了扩展方法,如果使用构造函数的话,即使信息再流畅,我想也不如构造函数来的直观

    ----这句话不通 , 请检查检查。

  11. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-09-25 18:08:00

    Car car = Car.describedAs().
                 .box()
                 .length(50.5)
                 .type(Type.INSULATED)
                 .includes(Equipment.LADDER)
                 .lining(Lining.CORK);
    

    链式表达。 该技巧至少可以回溯到C语言了。
    在C语言中, 因为没有这样的语法:
    o.method( ... );

    只有这样的语法:
    function(o, ... );

    会比较难看。
    所以, 到了支持第1种语法的语言中, 链式表达才稍微好看一些。
    比较著名的例子是C++的iostream与boost.assign。


    但也只是"写"代码的技巧, 对可读性没有太大帮助。
    除了写起来方便一些(其实是方便很多^_^),我不觉得上面的代码和下面的代码在可读性上有何区别。
    Car car = new CarImpl();
    MarketingDescription desc = newMarketingDescriptionImpl();
    desc.setType("Box");
    desc.setSubType("Insulated");
    desc.setAttribute("length", "50.5");
    desc.setAttribute("ladder", "yes");
    desc.setAttribute("lining type", "cork");
    car.setDescription(desc)
    

  12. 明轩
    *.*.*.*
    链接

    明轩 2009-09-25 18:18:00

    请问 老赵 我有如下一段代码:
    class abstract BaseEntity
    {

    }

    class Entity<T> : BaseEntity where T : new()
    {
    public Entity()
    {

    }
    }
    class SearchTest
    {
    public static BaseEntity Search<U>()
    {
    Entity<U> objEntity = new Entity<U>();
    return objEntity;
    }
    }
    实际这是编译不通过的,错误原因:
    “U”必须是具有公共的无参数构造函数的非抽象类型,才能用作泛型类型或方法“
    其实我的意图就是在一个范型方法中返回一个范型类的实例。该范型类的类型参数和范型方法的类型参数其实是一样的,要不然我得为每个实现BaseEntity 的子类写一个Search方法。
    public SearchBolgEntity
    public SearchCompanyEntity
    请问我这样走是否合理,我改怎么处理。怎么才能实现我的要求

  13. 妖居
    *.*.*.*
    链接

    妖居 2009-09-25 18:31:00

    现在越来越喜欢Fluent Interface。第一次看到是在Moq里面,原来程序还能像说话一样,而且通过智能感知让使用者一步一步的进行设定。

  14. 妖居
    *.*.*.*
    链接

    妖居 2009-09-25 18:35:00

    @明轩
    (说的不对请纠正)
    这个肯定会出编译错误的,因为你没有指定U必须是new约束的。比如我调用Search的时候使用Search<int>怎么办。为Search方法的反省参数加个new约束就可以了。在实现Repository的时候我也用到了类似的做法。

  15. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-25 18:35:00

    @明轩
    你是需要在public static BaseEntity Search<U>()后面加上 where U : new()然后再是{}包围的方法体……

  16. 老赵
    admin
    链接

    老赵 2009-09-25 18:47:00

    OwnWaterloo:
    除了写起来方便一些(其实是方便很多^_^),我不觉得上面的代码和下面的代码在可读性上有何区别。


    说到好读,是因为我觉得fluent interface的噪音少,所有的代码都是有意义的,更有些DSL意味了,呵呵。

  17. 老赵
    admin
    链接

    老赵 2009-09-25 18:48:00

    妖居:现在越来越喜欢Fluent Interface。第一次看到是在Moq里面,原来程序还能像说话一样,而且通过智能感知让使用者一步一步的进行设定。


    话说,有没有关注过LINQ to Moq?挺有意思的。
    所以我说,在编写代码方面,程序员的追求是没有止境的。

  18. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-09-25 19:06:00

    说起语言特性 我觉得TEventArgs该做成协变的

  19. 老赵
    admin
    链接

    老赵 2009-09-25 19:35:00

    @winter-cn
    到了C# 4,加个in/out就直接变了,呵呵。

  20. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-09-25 19:51:00

    @Jeffrey Zhao
    C#现在不是4.0么?

  21. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-25 21:01:00

    @winter-cn
    C# 4.0还没正式发布呢。不知道今年的PDC前后VS2010有没有机会RTM

  22. 道法自然
    *.*.*.*
    链接

    道法自然 2009-09-26 00:17:00

    Jeffrey Zhao:
    @道法自然
    1、异常管理是指什么啊?Fluent interface的好处在于声明式不是命令时,让代码更为清晰。
    2、Lambda表达式一般不超过3行,什么是“行”?MSDN上的描述能让我看一下吗?你说的“代理”是指“委托”吧,我认为用Lambda表达式构造委托很清晰很直观,可能是习惯问题吧。




    1 一般来讲,友好的异常信息应该是做一个判断,如果判断不满足条件,则抛出一个友好信息的异常。

    if(Car.describedAs() == null)
    {
    throws new Exception("Infor");
    }

    但是对于以下这种风格的代码,如果中间有一个函数返回null,只能抛出一个未处理异常,而且提示都是NullPoint。设计底层API在异常管理方面需要控制更为严格了。

    1.Car car = Car.describedAs().
    2. .box()
    3. .length(50.5)
    4. .type(Type.INSULATED)
    5. .includes(Equipment.LADDER)
    6. .lining(Lining.CORK);

    或许,使用这样的代码只能在特定情况下吧,比如像OwnWaterloo举例子中,每一个函数都是给Car设置属性。我个人还是觉得这种语句应该在底层中少用,你觉得呢?

    2 http://msdn.microsoft.com/zh-cn/library/bb397687.aspx Lambda 语句的主体可以包含任意数量的语句;但是,实际上通常不会多于两个或三个语句。

    因此,我认为Lambda不能是太过于复杂的应用,应该是一看就可以理解,不用反应一下的。

  23. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-09-26 00:27:00

    @道法自然
    那个例子不是我举的, 是老赵原文中的^_^


    其实只要box、length、type、includes等函数是以异常的方式报告错误
    Car c =
    ...
    ...

    这段代码保持异常中立, 就能捕捉到不同的错误, 是这样吧?


    如果我要写代码, 能有链式表达的api会觉得比较爽。
    但如果是读, 我觉得两者其实差不多。 都是看一眼就能理解, 不用反应一下的代码。
    这属于口味问题……

  24. 道法自然
    *.*.*.*
    链接

    道法自然 2009-09-26 00:38:00

    @OwnWaterloo

    其实只要box、length、type、includes等函数是以异常的方式报告错误
    Car c =
    ...
    ...

    这段代码保持异常中立, 就能捕捉到不同的错误, 是这样吧?

    >> 这种想法不错,但是有点别扭,因为需要强制的约束他们“以异常的方式报告错误”。

  25. 老赵
    admin
    链接

    老赵 2009-09-26 00:39:00

    @道法自然
    这是Fluent Interface,每个都返回this,不会为null的。
    每个东西有其使用场景,我们不能说“尽量少用XXX”,应该说“在合适的情况下用XXX”,因为有问题的不是XXX,而是程序员用错了。
    例如,你说的“抛出友好异常”是正确的实践,但是链式的API与它不矛盾,如果没发抛出友好异常,那是因为用错了,而不是链式API的问题。
    一个API,它有前置条件,后置条件。很多API的后置条件就包括非null(你想.NET中有多少API是会返回null?),到了.net 4.0中有了Code Contract,一切就更明显了,呵呵。

    至于你说Lambda表达式,在我看来MSDN上说的有问题。它说得其实是Lambda Statements,也就是构造一个多语句的匿名方法。
    如果在一个需要匿名方法的情况下,如果不用Lambda表达式,应该用什么呢,只能用delegate了。
    那么你觉得delegate(int i) {...}和i => {...}两种方式哪个更清晰呢?我觉得区分不出。
    所以,如果你说不清晰,那也是不该用匿名方法,而不是不该用Lambda表达式。
    其实我一直认为,Lambda表达式是非常清晰的,有人说难懂,但我从来没有见过一个例子是把Lambda表达式用歪了……
    如果说难懂,我觉得还是不熟悉的缘故吧。用C系语言的观点去了解Lambda表达式是否清晰,我觉得不是正确的方式。

  26. 道法自然
    *.*.*.*
    链接

    道法自然 2009-09-26 00:43:00

    @Jeffrey Zhao

    明白,也顺便学到了一点技巧了。因为这种链式函数方便多了。谢谢。

  27. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-09-26 00:50:00

    道法自然:
    因为需要强制的约束他们“以异常的方式报告错误”


    这应该是没办法的办法了…… 有得必有失嘛……

    C++里面这种情况还要多, 因为C++对运算符重载的限制很少。
    而一旦使用运算符重载,参数和返回值通常是有固定搭配的, 要报告就只能异常了。

  28. 道法自然
    *.*.*.*
    链接

    道法自然 2009-09-26 00:58:00

    @OwnWaterloo

    说的有道理。Thanks。

  29. James.Ying
    *.*.*.*
    链接

    James.Ying 2009-09-26 07:04:00

    public class MapEvent<TIn, TOut> : InOutEventBase<TIn, TOut>
    {
    public FilterEvent(Func<TIn, TOut> mapper, IEvent<TIn> inEvent)
    : base(inEvent)
    {
    ...
    }
    }

    嘿嘿,这里构造写错了吧

  30. Hit.zealous[未注册用户]
    *.*.*.*
    链接

    Hit.zealous[未注册用户] 2009-09-26 07:48:00

    老赵的帖子大多是 语法糖 级别的,看着很累啊。不过期望最好都像这篇文章一样,给出一个对比,让人们可以很快地清楚地看到改进。
    最后啰嗦一句“感觉delegate足够了。。。”。

  31. 老赵
    admin
    链接

    老赵 2009-09-26 11:58:00

    @James.Ying
    是啊是啊。

  32. 老赵
    admin
    链接

    老赵 2009-09-26 11:58:00

    @Hit.zealous
    这不叫“语法糖”,这叫“编程实践”,编程工艺方面的东西,比Silverlight,ASP.NET MVC之类的有广泛价值。
    还有,不要说“大多”好不好,这就像一个人冲上来说“就不见你写点有意义的东西”。
    我非语法讨论和有意义的东西,写得还不够多么,我可是什么都写的,嘿嘿。
    // 说“delegate足够”一句话就够了,但要给出理由不容易啊,我的理由在那三篇博客中说得无比详细了。

  33. chenleinet
    *.*.*.*
    链接

    chenleinet 2009-09-26 18:50:00

    算是明白了扩展方法的神奇了

  34. Teddy's Knowledge Bas…
    *.*.*.*
    链接

    Teddy's Knowledge Base 2009-09-27 10:01:00

    lambda确实是很吸引人的语法糖,但是个人认为,作为shared组件的api参数,还是应该非常谨慎的使用,切不可滥用。作为某个framework的代码,维护的人不多,只要有好的编码习惯,或者,好的约定,写出如lz示例的易读的代码并不太难,但是如果作为组件的api接口,因为程序员的的个人编码习惯不太可能在每一行代码的级别上统一,resharper之类的工具又经常帮我们做一些未必易读合理的格式化,会很容易导致不够易读和健壮的代码。如果完全依赖code review,成本也是很难接受的。

  35. 老赵
    admin
    链接

    老赵 2009-09-27 10:03:00

    @Teddy's Knowledge Base
    举一些例子?

  36. Teddy's Knowledge Bas…
    *.*.*.*
    链接

    Teddy's Knowledge Base 2009-09-27 10:23:00

    @Jeffrey Zhao
    最近老加班,太忙,没时间展开这个话题。造成代码不好看遇到最多的情况是lambda表达式的嵌套,简单设想一下,如果调用一个方法有超过三个lambda参数,每个参数的实现里面又有(不过这里code review时应该让他extract为方法)多层lambda嵌套语句。当然,更极端的情况是,delegate的参数又包含多个delegate,当然这个尽可能应该避免了。

    resharper对lambda的格式化最大的问题是,他默认那个缩进实在太大,基本上缩进超过两次(如果lambda嵌套超过两次),一屏幕的宽度都不够显示一行代码了。

  37. 老赵
    admin
    链接

    老赵 2009-09-27 10:30:00

    @Teddy's Knowledge Base
    在我看来,lambda表达式嵌套其实就和if,for太深是一个道理,其实就是block太多,级别太多,倒不是lambda的问题。
    resharper没用过哎,lambda出现那么久了,怎么支持还不好呢。

  38. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-27 11:19:00

    我觉得扩展方法并不是扩展这么简单,扩展方法让我们可以实现“函数的”中缀表达式,这是与自然语言最接近的表达方式。在不支持扩展方法的C++中,我们也经常能看到使用运算符重载使得函数支持中缀表达式的做法。

  39. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-27 11:24:00

    当然扩展方法也不是那么的神奇,因为在没有扩展方法的时候,我们也可以用Wrapper来实现类似的语法效果。

    我现在打算在2.0下重现Enumerable扩展,所采取的办法就是:

    IEnumerbale<int> list = ...;
    Enumerable<int> extList = new Enumerable<int>( list );
    然后,extList具备了Skip、Select等方法。同时extList存在与IEnumerable<int>的隐式类型转换。

    或者说,这样的表达式是成立的:

    IEnumerable<string> result = extList.Where( delegate( item ) { return item != 0; } ).Select<string>( delegate( item ) { return item.ToString() );

  40. 老赵
    admin
    链接

    老赵 2009-09-27 11:25:00

    @Ivony...
    Enumerable<T>也是我“拯救C# 2.0”里的做法
    http://www.cnblogs.com/jeffreyzhao/archive/2009/06/27/try-to-make-a-better-csharp-2.html

  41. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-27 11:34:00

    话说我已经打算在我的模板生成引擎中引入这种函数中缀表达式(这个是我发明的词,特此声明)的应用了。我现在所设想的模板绑定语法类似于这样:

    ${User.Birthday null.未填写 format.yyyy-MM-dd html}

    简单的说,这个表达式要求获取User.Birthday的值,并依次调用三个方法,null、format和html,编译后的C#伪代码为:

    var item = Context.GetItem( "User" ).GetItem( "Birthday" );
    item = null( item, "未填写" );
    string str = format( item, "yyyy-MM-dd" );
    str = html( str );
    return str;

    简洁,易懂。。。。

  42. 老赵
    admin
    链接

    老赵 2009-09-27 13:55:00

    @Ivony...
    汗一下,感觉就是直接用F#写模板了:
    <%= User.Birthday |> null "未填写" |> format "yyyy-MM-dd" |> html %>

    其实好像C#也不错:

    <%= User.Birthday.Null("未填写").Format("yyyy-MM-dd").Html() %>

  43. oraclesun[未注册用户]
    *.*.*.*
    链接

    oraclesun[未注册用户] 2009-09-28 10:49:00

    Car car = new CarImpl();
    MarketingDescription desc = newMarketingDescriptionImpl();
    desc.setType("Box");
    desc.setSubType("Insulated");
    desc.setAttribute("length", "50.5");
    desc.setAttribute("ladder", "yes");
    desc.setAttribute("lining type", "cork");
    car.setDescription(desc)


    不懂JAVA就别来丢人显眼了,有你这么糟蹋JAVA代码的吗,JAVA中照样可以使用
    Car car = Car.describedAs().
    .box()
    .length(50.5)
    .type(Type.INSULATED)
    .includes(Equipment.LADDER)
    .lining(Lining.CORK);
    类似的写法。

  44. 老赵
    admin
    链接

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

    @oraclesun
    你到底有没有看文章?我说的就是用Java实现fluent interface。
    这几个方法都是小写开头的,一看就像Java,真不知道你在想什么。

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我