WCF 真乃神器也

受到 uTorrentAPI 的影响,我尝试使用了一下 WCF。之前在老周的一系列文章中读到过一点,不过由于讲的是比较深层的东西,所以感觉云里雾里。这次试用了一下,真是不错。


序列化的话,我们知道有 DataContractAttributeDataMemberAttribute。用上这两个特性之后,一个普通类摇身一变,成了一个实体类。其中重要的是的契约(contract)思想,也即声明形式后,填充和检查由框架完成。

所以对应于数据上的实体类,WCF 遵循类似的思想,提出了操作接口。在一个接口上加上 ServiceContractAttribute,给各操作加上的是 OperationContractAttribute。这就相当于借用了接口的“实现”语义,来表达操作的“可用”语义。根据不同的具体操作类别,接口方法还会附加其他的特性。最后根据接口声明,类工厂创建出一个具体的实现以供使用。

多么美妙,这就包含了“描述问题,而不是描述解答过程”的思想。不再需要关心具体的业务逻辑,只需要声明;无法满足要求(如响应错误)时则会立即抛出异常。这样业务逻辑的代码就得到了极大的简化。

本次的软件,我是作为第一次真正的架构设计,认真写了文档的。其中考虑到实际场景,引入了 MEF 进行插件化。MEF 的核心仍然是契约思想——指定 ExportAttributeImportAttribute,由 MEF 自动在目录(catalog)中进行搜索和实例化。所以我觉得这次的经历改变了我对某些问题的思考方式。

看看我此次的设计。作为实际操作的载体,IUTorrentProxy 被分离了出来,通过一个代理类(NativeUTorrentClient)进行产生和调用。

图1 新版 UML 图(部分)

接口(IUTorrentProxy)声明(部分):

[JsonResponse]
[OperationContract]
[WebGet(
    BodyStyle = WebMessageBodyStyle.Bare,
    ResponseFormat = WebMessageFormat.Json,
    UriTemplate = "/gui/?action=getsettings")]
[NotNull]
JsonObject GetSettings();

[OperationContract]
[WebGet(
    BodyStyle = WebMessageBodyStyle.Bare,
    ResponseFormat = WebMessageFormat.Json,
    UriTemplate = "/gui/?action=setsetting&s={settingName}&v={settingValue}")]
[NotNull]
EmptyResponse SetSetting([NotNull] string settingName, [NotNull] string settingValue);

使用(UTorrentClient):

public override string GetAutoLoadPath() {
    EnsureTokenIsNotNull();

    var obj = _nativeClient.Proxy.GetSettings();
    var settings = (JsonArray)obj["root"]["settings"];

    foreach (var t in settings) {
        var tuple = (JsonArray)t;
        var key = tuple[0];
        if (key == UTorrentSettingKeys.DownloadPath) {
            string downloadPath = tuple[2];
            return downloadPath.Replace(@"\\", @"\");
        }
    }

    return null;
}

public override bool SetAutoLoadPath(string path) {
    EnsureTokenIsNotNull();

    if (path.Length > 0) {
        path = Path.GetFullPath(path);
    }

    _nativeClient.Proxy.SetSetting(UTorrentSettingKeys.DownloadPath, path);

    var setPath = GetAutoLoadPath();
    return setPath == path;
}

注意 IBtClient 才是本工程中对各 BT 客户端的操作的抽象,所以在更上一层的调用中调用的是 IBtClient 的方法,接着分发到 IUTorrentProxy 的方法。

以前的架构是这样的:

图2 旧版 UML 图(部分)

不同在于,中间有一个 WebAccessBase 层。这个类的作用就是提供一些 protected 函数和字段,专门用于网络访问(因为可能要涉及 cookie 等与状态相关的东西)。

使用上:

public override string GetAutoLoadPath() {
    EnsureTokenIsNotNull();

    var options = Options;
    var uri = Uris.GetSettings(options.BaseUri, Token);

    var responseText = HttpGetGetText(uri);
    if (responseText == null) {
        return string.Empty;
    }

    if (!DownloadPathRegex.IsMatch(responseText)) {
        return string.Empty;
    }

    var downloadPathMatch = DownloadPathRegex.Match(responseText);
    var downloadPath = downloadPathMatch.Groups["value"].Value;

    if (string.IsNullOrEmpty(downloadPath)) {
        return string.Empty;
    }

    return downloadPath.Replace(@"\\", @"\");
}

public override bool SetAutoLoadPath(string path) {
    EnsureTokenIsNotNull();

    path = Path.GetFullPath(path);

    var options = Options;
    var uri = Uris.SetSetting(options.BaseUri, Token, UTorrentSettingKeys.DownloadPath, path);
    HttpGetAndDone(uri);

    var setPath = GetAutoLoadPath();
    return setPath == path;
}

嗯?HttpGetAndDone() 不是很简单么?不,这展开那代码和抛异常就多了。然而这些异常本来也应该属于标准过程的一部分,因为响应是要符合数据契约的,手写解析代码难以验证,容易错漏。比如,网络超时是不是要抛出异常?报文过长是不是要抛出异常?流读取过程中出错(比如编码无法识别)是不是要抛出异常?反序列化过程中结果不符是不是要抛出异常?如此等等。每一处都要好好想可能发生的情况,然后决定抛出什么样的异常,附加消息是什么,异常链上种类是否太多……头疼。

可以见到,旧版本中间被许多额外的代码干扰了。比如值的检测、URI 生成、状态管理(见 Token)。为什么新的实现少了这些东西呢?其实没少,只不过移动到了其他的地方。比如 token 的管理,就是由 IOperationBehavior.ApplyClientBehavior() 进行拦截和储存(到一个实现了 IChannel 接口的匿名类型的字段中),然后由 IContractBehavior.ApplyServerBehavior() 进行对请求的修改(查询字符串中加入 token=...)。这个 token 的验证机制是在背后通过这种过滤器管道来完成的,所以在业务代码中觉察不到它的存在——也不需要关心。(不过这个方法和 WCF 方面的代码不是我的,是 uTorrentAPI 的作者 Mike Davis 写的。这些代码正好给我作为 WCF 方面的入门材料来学习。)

JSON 的反序列化也是类似的,这里就不举出例子了。

所以你看,只要实现了合适的特性,贴上去,就可以对通信报文进行加工,从而远程调用就变得跟本地调用一样简单——RPC 的精髓嘛。在 WCF 的架构下消息定制化十分自然,契约化的思想、加工管道的设计,太美妙了。

分享到 评论