连贯接口

来源:互联网 发布:免费象棋软件排名 编辑:程序博客网 时间:2024/05/02 12:39

原文:FluentInterface    设计        2005年12月20日            Bliki 索引

译注:可结合“领域专用语言(DSL)”和“界定DSL”读本文。


更新:Piers Cawley做了精彩的后续讨论。

几个月前,我和Eric Evans参加了一个研讨会,他发言的主题是一种特殊风格的接口,我们决定把它命名为“连贯接口”。连贯接口的风格并不常见,但我们觉得应该让更多人了解它。还是用实际例子说说连贯接口的妙处吧。

一个最现成的例子就是Eric的timeAndMoney库。如果不用timeAndMoney库,要构造一个时间间隔(time interval),常会看到类似下边的代码:

TimePoint fiveOClock, sixOClock;
...
TimeInterval meetingTime = new TimeInterval(fiveOClock, sixOClock);

如果用了timeAndMoney库,就可以这么做:

   TimeInterval meetingTime = fiveOClock.until(sixOClock);

再举个例子(有点俗):为客户生成订单。一个订单可有多个订单项,一个订单项又记录了哪种商品要多少。订单项能被设为可略的,意思是如果这种商品不巧缺货就把这一项略掉,免得耽误配送整个订单。另外,还可以把订单设成加急配送状态。

做这件事最常见的办法就像下面:

    private void makeNormal(Customer customer) {
        Order o1 = new Order();
        customer.addOrder(o1);
        OrderLine line1 = new OrderLine(6, Product.find("TAL"));
        o1.addLine(line1);
        OrderLine line2 = new OrderLine(5, Product.find("HPK"));
        o1.addLine(line2);
        OrderLine line3 = new OrderLine(3, Product.find("LGV"));
        o1.addLine(line3);
        line2.setSkippable(true);
        o1.setRush(true);
    }

一言以蔽之:把各个object构造出来,再把小object填到大object里。如果不能直接用构造函数构造出填满了货的大object,就得先用临时变量把小object保存起来,再填到大object里——只要是类似这种给一个集合添单个元素的情况,八成就得这么做。

要是换成连贯风格,事情就变成这样:

   private void makeFluent(Customer customer) {
        customer.newOrder()
                .with(6, "TAL")
                .with(5, "HPK").skippable()
                .with(3, "LGV")
                .priorityRush();
    }

观察这种风格,最重要的是抓住一点:它是操着一种内部DSL连贯地说事。这就是为什么我们拿“连贯”这个词来形容它。这样的API第一设计目标就是连贯性强、可读性强。连贯不会天上掉下来,对连贯的思考以及API自身的实现都要耗费更多的脑细胞。“constructor+setter+填充方法”这种傻瓜式API写起来就省劲多了。要想打造出一套精致的连贯 API,真得好好动动脑筋。

没错,我举的这个例子实在是个小玩儿闹,我是在卡尔加里的一个咖啡馆里吃着早点想出来的,设计出地道的连贯API着实得花点时间。如果你想一睹精心雕琢出来的连贯API的风采,那就看jMock吧。和任何Mock库一样,jMock可以制定复杂的函数调用规范。最近几年Mock库像雨后春笋,jMock与众不同,它的连贯API用起来行云流水。下面看一个表示预期函数调用的例子:

mock.expects(once()).method("m").with( or(stringContains("hello"),
                                          stringContains("howdy")) );

在JAOO2005上,Steve Freeman和Nat Price做了一个讲jMock API演化的报告,我听了——非常精彩。他们答应把他们的经验总结成文字,到现在还是空头支票,有哪位壮士愿意去虏走他们的编译器——直到他们兑现

到现在,我们看到的连贯API大都是用来装配objects的,这些objects又多是Value Objects。它们都是在一个声明性的上下文里——尽管我怀疑这一点会有些说法,但拿不准这是不是它的一个必然特点。对我们用户来说,评价一套API连贯性的关键在于它那种DSL的品质——一套API越用越觉得这种DSL行云流水,那么它的连贯性就越强。

打造一套连贯API会培养出一些不同寻常的API设计习惯,最明显的一条就是setter有返回值。(在前边下订单的例子里,调用addLine方法给一个order添一个订单项会再返回这个order。)在{花括号}世界里有条不成文的规矩:对object做改动的方法是void的——我觉得这条规矩挺好,因为它恪守Command与Query分离原则。但这条好规矩却妨碍我们打造连贯接口,为了连贯就不惜“出轨”了。

返回什么类型,要看你那一串连贯的操作下一个是啥,下一步操作是谁身上的就返回谁——jMock对这一点非常强调。这种风格有一个妙处:你的IDE就像会了魔法,方法智能补齐能帮忙告诉你下边敲哪几个键。一般而言,我觉得动态语言语法不那么混乱,更适合做DSL;而方法补齐功能却给静态语言加了分。

世事无完美,连贯接口的方法有一个麻烦:单独一个方法根本没有意义。方法文档和方法浏览窗口能把一条条方法列出来,但没什么意义——我的意思是单独看一个方法,它的名字无法反应它的意图,只有在一串连贯操作的上下文里,才能显出它的强处。因此,最好做一些object生成器,它们专门用在这类上下文里。

Eric曾提到,目前他接触到的连贯接口都是用来装配Value Object的,Value Object没有具有业务领域意义的实体与之对应,它们可以被随心创建随心抛掉。因此,实际上连贯性依靠的是能够用老Value Object创建出新Value Object,这么看来,那个下订单的例子并不那么有代表性,因为按Evans氏分类法订单和订单项都属于Entity。

目前我能见得到的连贯接口并不多,还属稀缺品种,所以我推断我们对它的长短强弱还知之甚少。尽管到目前,所有关于如何使用连贯接口的规诫还都只是初步性的,但我觉得它已经成熟到可推广试验的程度了。
原创粉丝点击