`
fixopen
  • 浏览: 83902 次
文章分类
社区版块
存档分类
最新评论

依赖顺序是罪恶

阅读更多
C和C++语言是流行和常用的依赖于顺序的语言。C++略为有点改进,在类范围内部不依赖于顺序。依赖于顺序是一种惩罚,是一种不重视程序员感受的表现,并且在深层次要求采用采用声明和实现分开,因而也就不能维持DRY原则。违背DRY会导致维护复杂。
我详细说明一下为什么依赖顺序是罪恶的。
大家都知道抽象是一种强大的能力,但是,依赖于顺序强迫我们只能从低层次的细节开始,这跟现如今流行的IDE的智能完成一样。我们不得不把所有的细节描述完备,才能构造更大的构件。
我还是用一个实例来说明吧。
我为了练习的目的,试图写一个HTTP Client,自然,我是从阅读RFC规范开始的。通过扫描RFC的目录和快速掠过其内容,我们发现HTTP是一个请求响应模式的简单协议,其请求和响应都是所谓的Message,显然,只要我们能解析和构造这个Message,我们就大致完成任务。
于是就有了:

namespace Http
{
    class Message
    {
    public:
        MessageHeader headers;
        CRLF crlf;
        MessageBody body;
        std::string toString(void)
        {
            return headers.toString() + crlf.toString() + body.toString();
        }
    };

    class MessageHeader
    {
    public:
        std::vector<Field> fields;
        std::string toString(void)
        {
            std::string result;
            for (int i = 0; i < fields.size(); ++i)
            {
                result += fields[i].toString();
            }
            return result;
        }
    };

    class Field
    {
    public:
        FieldName name;
        Colon colon;
        FieldValue value;
        CRLF crlf;
        std::string toString(void)
        {
            return name.toString() + colon.toString() + value.toString() + crlf.toString();
        }
    };

    class FieldName
    {
    public:
        FieldNameValue name;
        std::string toString(void)
        {
            std::string result;
            switch (name)
            {
            case CacheControl:
                result = "Cache-Control";
                break;
                ......
            }
            return result;
        }

        FieldNameValue fromString(std::string fieldName)
        {
            FieldNameValue result;
            if (fieldName == "Cache-Control")
                result = CacheControl;
            else if (...)
                ...
            return result;
        }
    };

    enum FieldNameValue
    {
        //generic headers
        CacheControl,
        Connection,
        Date,
        Pragma,
         ......
}

我想,上面的代码非常的直接了当。几乎不需要解释,类似于递归下降法制作解析器,而事实上,确实是下降的,但是递归没有,因为HTTP的Message格式还没有那么强大和灵活。稍微有点奇特的是FieldName和FieldNameValue这一对,它们合作构成了我设想的枚举,这也说明了C/C++语言的枚举并不强大到可以作为一个抽象机制使用,当然,这不是我们焦点。暂且不论。

由于其中的toString都是很简单的,所以我准备把它们做成内联函数,这个无可厚非,我们看看就是这个要求是怎么导致我们需要扭曲我们的代码的。

首先需要说明的是:上面的代码不能通过C++编译器的编译。原因很简单:编译器看到MessageHeader被使用的时候还没有发现其定义,会给出一大堆抱怨。怎么办?我们给出一个前向声明吧。也很简单,在使用之前声明一下就行了:class MessageHeader。把它加在使用之前的任意地方,编译器就应该不说话了吧。

哦,还是不行,原因是:对了,它要求必须有完整的类定义才能定义该类的对象,别忘了,C++是值语义的语言。好吧,我们改造成指针……,类型后面加上*,一连串的.变成->,应该没有问题了吧(如果你做过实际的项目,你就知道:这个修改的代价是非常高的,不过区分指针和原始对象不是我们批判的焦点,这里不再深入)。啊……还是不行,原因仍然很简单,就算可以定义指针而不是真正的对象,我们仍然不不能够调用该指针对象的方法,原因是:我(编译器)没有看到类型的定义,怎么知道有没有该方法可以调用呢?

现在我们的选择有三个,但是都不够优雅。
一、我们调整类型定义的顺序,把低层次的构件往前挪。
二、我们把成员函数的实现挪到最后,这时候它已经看到所有的定义了。
三、这是二的彻底化,干脆移动其实现到另一个文件。
方法一的缺陷就是,我们不再能够自然的从上而下看出我们设计的脉络。这跟我看HTTP的RFC一个缺陷是一样的,HTTP的RFC就不是采用自上而下描述的,而是先描述了一些基础的构件,在描述了整体结构,看得人晕头转向,另外,协议中有大量的用途和功能分析,我不是说这不好,但是作为协议这让人分神。可以把功能或者需求放在开始诱导大家构思HTTP应该怎样设计。而不是放在HTTP协议都描述了以后才描述功能。
方法二和方法三本质上是一样的,但是方法三的可维护性就更差一些了,因为所有的修改都得涉及到两个文件,而且,方法三不再能够是inline的,这个代价让我觉得非常不快。而方法二确实仍保持inline,但是,一旦修改我还是得从头到尾的查找。让我写两遍相同的函数头也让我不爽。
而最本质的原因就是:我没有办法让一个决策(函数签名)在一个地方说明,必须出现在两个地方,这是对DRY或者叫做SPOT原则的公然违反,它们会导致大量的微妙的问题和产生难以维护的代码。
C++在很大程度上鼓励我们采用第三种方案,这也是C++中的正统方案,只要我们坚持顺序依赖,我们就不得不倾向于这种方案。顺序依赖把维持依赖关系的责任推给了程序员而不是编译器自己去解决,这当然简化了编译器,但是却使得程序员的日子更艰难。那么,顺序依赖究竟应该是谁的责任呢?是该由程序员处理的呢,还是该由编译器处理的?
我们先大而化之描述一下依赖。依赖表示一个构件(这儿的构件包括函数,类,全局变量,源文件等等任意可以用来搭建整个系统的部分)对别的构件的使用。我们很自然很直观的能感受到,只要我们使用了别的构件,编译器显然知道我们的构件依赖于那些使用的构件,所以,让程序员维持依赖是不必要的。
但是还有一个问题:构件A依赖于构件B,这个编译器知道,但是构件B在哪儿?这个编译器不知道。显然,我们需要而且也只需要在某个地方描述一下这个构件跟位置的映射,我们的编译器就可以工作了。如果我们把这种映射放在任何构件里面,这样势必会导致这种映射关系描述上的重复,导致大量的维护问题。显然,最适合描述构件跟位置的映射关系的地方是工程的配置文件。它是整个工程级别的属性。
那么顺序依赖是什么呢?顺序依赖本质上是希望把构件和位置的映射关系消除。它要求所有依赖的东西必须在它之前出现,这样,被依赖方也就自然不需要指定位置了,它就在前面。这个有利于分块编译(不知道大家注意没有,分块编译的用处似乎并不是很大,无论是大工程还是小工程,因为:毕竟编译只是一个阶段,而且维持构件是否是最新编译版本似乎也一点都不难),但是实现分块编译导致需要大量的重复编译,毕竟任何被多次依赖的组件都必须多次被编译。并且分块编译要求类似于Makefile之类的支持环境,现代的语言及其开发环境都完全不支持分块编译了,它们都采用运行时动态装载这个更灵活的模式。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics