Fork me on GitHub

Clay: 创建和使用深层次对象图

Clay 是 CodePlex 上的一个开源项目,帮助我们创建轻松创建对象,就 JavaScript 或其它动态语言一样简单。Clay 项目的网址是 http://clay.codeplex.com,Clay 目前主要应用于 Orchard 项目。以下2篇文章解释了Clay的背景:

Clay: malleable C# dynamic objects – part 1: why we need it

Clay: malleable C# dynamic objects – part 2

以下内容来自上述2篇的简要摘录:

Orchard需要建立一种合适的数据结构用于在Orchard中承载视图模型(view model)——由许多不同的实体毫无约束地构建而成的时候,很快就变得非常明显必须要用一种动态结构。

我们需要的是一分层结构:一个页面可能包涵一个博客文章列表和一些微件(widgets),而每篇博客文章是由许多部件组成的,比如评论,而评论又包涵作者,作者又包涵头像、等级等等。

这就带出了第二个需求:多个实体必须在互不知道对方的情况下共同构建那个对象图(视图模型)。我们预先并不知道这个对象图的形状,且对象图的每个节点都很容易受到后来扩展节点的影响。

现在的问题是使用C#的静态类型来解决这些需求是非常不爽的。可以使用类似XML DOM API 的 ChildNodes 和 Attributes 集合 以及 NodeName and Value properties 的方式,而且这确实能够解决问题。但是 绝大多数人都会同意长期以来这种形式的API都是使开发人员痛不欲生的重要原因,因此,除非用枪指着我们的头,否则我们都不想使用这种形式。

这种形式的API之所以极其令人厌烦,最主要的原因是它首先让你获取到元数据,而把获取实际数据推到第二步API,比如Value。

现在应该比较清楚C# 中的 XML APIs之所以不爽,是因为静态语言不喜欢不可预知的东西,而想在编译时就知道对象的一切细节。XML APIs 接受预先知道的(节点拥有元数据是固化在结构里的),而把未知的东西推给属性。

换一种说法,元数据应该是对象的一个属性,而最终你得到的真正对象却是元数据结构的一个属性。

C# 4.0 提供了一个非常好的关键词适合各种各样的情况使用,它就是 dynamic

ExpandoObject 实际上是通过一种令人惊讶方式实现的,这使得它非常高效。提示:不是通过字典。又提示:它是一个非常好的东西。

但是,依照API 可用性原则,它不够大胆,尤其是在构建深层次动态对象图方面它并没有给我们多少帮助。它的行为也比较固定且不能被扩展。

另一方面,Clay 是高度可扩展的,且专注于深层次对象图的创建和使用。

通过 Clay 你可以做的第一件事情就是创建一个简单的对象并在它上面设置属性。在此之前,我们将首先实例化一个给我们提供 语法 语义糖衣的工厂。我希望我们能够跳过这一步而使用一些类似静态API的方式(译注:静态工厂方法),但是我们不能。好了,正如你将看到的只需很小的代价:

dynamic New = new ClayFactory();

现在这个“New”对象将帮助我们创建新的 Clay 对象,正如它的名字所暗示一样(虽然这个名字只是一个惯例而已)。

以下是一些简单且没有多少新奇的东西:

var person = New.Person();

person.FirstName = "Louis";

person.LastName = "Dejardin";

这些你都能通过 ExpandoObject 来完成的,但是这里比较有趣的地方是它有多种实现形式,且开启了发掘潜能之窗。

例如,在 Clay 中,索引语法与属性访问器是相等的,就像 JavaScript 一样。当你在写代码通过名字去访问一个属性,而这个属性的名字在编译时刻又是未知的时候,这就非常有用了:

var person = New.Person();

person["FirstName"] = "Louis";

person["LastName"] = "Dejardin";

但还不止于此,你还可以将属性作为链式设置器来使用,像 jQuery那样:

var person = New.Person()

.FirstName("Louis")

.LastName("Dejardin");

或者,如果你喜欢,你还可以传入一个匿名对象:

var person = New.Person(new {

FirstName = "Louis",

LastName = "Dejardin"

});

更加好的是,Clay 还能理解命名参数,我们可以这样写:

var person = New.Person(

FirstName: "Louis",

LastName: "Dejardin"

);

总之,你可以使用很多种方式来设置属性和初始化 Clay 对象。

正如你所料,获取属性值也有多种方式且它们都是相等效果的:

person.FirstName

person["FirstName"]

person.FirstName()

你也可以创建 JavaScript-style 数组:

var people = New.Array(

New.Person().FirstName("Louis").LastName("Dejardin"),

New.Person().FirstName("Bertrand").LastName("Le Roy")

);

这种方式创建的数组也是一个完整的 Clay 对象,这意味着你可以在运行时对它添加属性。

然后,如果你想知道数组里的总项数,或者获取数组第一项的 FirstName 属性值,你可以这样:

people.Count

people[0].FirstName

当你想在一个已经存在的 Clay 对象上创建一个数组属性,这也非常容易:

person.Aliases("bleroy", "BoudinFatal");

如果有多于一个参数被传入,Clay 就会认为你正在初始化的这个属性是数组。但是如果只有0或1个参数,你只虽显式地传入一个数组 (CLR or Clay):

person.Aliases(new[] {"Lou"});

相比 CLR 数组,Clay 数组能动态增长:

person.Aliases.Add("loudej");

而且,它们也能够响应一些方法调用,如 AddRange, Insert, Remove, RemoveAt, Contains, IndexOf, or CopyTo 。

综合起来,我们就可以通过一种非常简洁而又富有表现力的语法来创建一个相当复杂的对象图:

var directory = New.Array(

New.Person(

FirstName: "Louis",

LastName: "Dejardin",

Aliases: new[] { "Lou" }

),

New.Person(

FirstName: "Bertrand",

LastName: "Le Roy"

).Aliases("bleroy", "boudin"),

New.Person(

FirstName: "Renaud",

LastName: "Paquay"

).Aliases("Your Scruminess", "Chef")

).Name("Some Orchard folks");

最后一点我想说明的是,Louis 第一次展示它给我看的时候,我觉得真的非常优雅和惊讶。

想像一下你有一个CLR接口需要实现,例如:

public interface IPerson {

string FirstName { get; set; }

string LastName { get; set; }

}

但是你想使用一个 Clay 对象,比如在上面定义的数组 persons 中一个元素。是的,你可以这样:

IPerson lou = people[0];

var fullName = lou.FirstName + " " + lou.LastName;

这里最特别的是 lou 是一个非常合法的静态类型 CLR 变量,你将获得全部智能感知和编译时检查。虽然我们从未写过实现这个接口的具体类型,但它就是一个实现了 IPerson 的对象。

能够实现如此不可思议的功能,是因为 Clay 重写了转换操作符,并为这个接口创建了一个动态代理(使用 Castle),这个动态代理再委托成员调用给 Clay 对象。

因此,那是一个真正 CLR 类型,但它是在运行时被生成的。

那就是使你能够写以下代码的:

foreach(var person in directory) {

Trace.Write(person.FirstName);

}

这里发生了事情是: “directory” Clay 数组被转换成一个 IEnumerable,而所有相应的方法都通过 Clay 动态数组对象实现。

相关文章:

借助 Clay 编写 不可思议 的 c# 代码

http://weblogs.asp.net/bleroy/archive/tags/Clay/default.aspx

posted @ 2012-10-14 20:22 张善友 阅读(...) 评论(...) 编辑 收藏