• eBen给《一个用C++实现的Dispatcher(二)》提出了一些非常好的问题,修正了一些细节。但有一点需要稍加讨论一下:
    那几个new,可能某些做服务器端程序的人会受不了. handlers倒是可以做成static的.所有对象共用一份就可以了.

    这个问题在实现这个dispatcher的时候,有人提出来过,但我依然坚持我的选择。

    似乎做过服务器应用的人对性能和内存都有着特别的敏感,我也做过,我也一样。如果每个请求都去创建dispatcher的话,不仅是内存,还有创建的成本在里面。

    但是,我们为什么要每次都去创建呢?如果一次创建好,就存放在内存中,不就没有这样的问题了。

    把handlers设计成static,不是一个好的设计,这样的话,这个dispatcher类本身就是有状态的了,一个对象的误操作很容易就影响到另外的对象。一个好的设计应该是尽可能无状态的,这也是全局变量不受欢迎的一个原因,杀伤力太大,且错误不好定位。

    之所以有人曾经给我提出过这个问题,因为他们的代码里有太多这样的代码:
    void Main::run() {
        ...
        MsgDispatcher dispatcher;
        dispatcher.dispatch(&msg);
        ...
    }

    这样的用法去使用这个dispatcher当然会有他们所担心的问题。解决起来很容易,把这个局部变量提取出来,比如做成类成员。

    我们现在知道了正确的做法是只初始化一次。不过,如果这是一个内层的代码就稍微麻烦一些了,即便这个类修正好了,我们不能保证这个代码在外层只调用了一次。面对遗留代码时,事往往不如意。

    在这种情况下,我会为这个dispatcher实现一个singleton:
    class MsgDispatcherSingleton {
    public:
        static MsgDispatcher* getDispatcher() {
            if (NULL == dispatcher) {
                dispatcher = new MsgDispatcher;
                …
            }

            return dispatcher;
        }
    private:
        MsgDispatcherSingleton() {}

        static MsgDispatcher* dispatcher;
    }

    MsgDispatcher* MsgDispatcherSingleton::dispatcher = NULL;

    需要知道的是,这是为了遗留代码所做的妥协,并不是我们真正设计的一部分。所以,我把它独立出来,如果有一天遗留代码被消除了,这个类也该随风而去。

  • 遗留代码就是遗留代码,总会有一些让人意想不到的地方,原以为所有消息都是由一个类(MsgHandler)处理的,可事实上,不是。
    if (msg->id == "open") {
        MsgHandler handler(msg);
        handler.open();
    } else if (msg->id == "close") {
        MsgHandler2 handler(msg);
        handler.close();
    } else if (…) {
        …
    } else {
        // exception handler
        …
    }

    上面的代码里面只有消息处理类的名字不同,其它的处理完全相同。不过,这样就让之前那个dispatcher就显得势单力薄。解决程序设计的问题,有一个很好用的处理手法:加个间接层。于是,
    class DispatchHandler {
    public:
        virtual void execute(Msg* msg) = 0;
    };

    对于前面的两种类型,道理上来说,我们需要分别为两个类型(MsgHandler和MsgHandler2)分别编写对应的子类。不过,我们用的是C++,是的,模板:
    template<typename T>
    class DispatchHandlerImpl : public DispatchHandler {
        typedef void (T::*Func)();
    public:
        DispatchHandlerImpl(Func sourceHandler)
            :handler(sourceHandler) {}

        void execute(Msg* msg) {
            T msgHandler(msg);
            (msgHandler.*(this->handler))();
        }

    private:
        Func handler;
    };

    原来的dispatcher也要相应的调整:
    #include <map>

    class MsgDispatcher {
    public:
        ...
        void dispatch(Msg* msg);
    private:
        std::map<string, DispatchHandler> handlers;
    };

    void MsgDispatcher::dispatch(Msg* msg) {
        DispatchHandler* handler = this->handlers[msg->id];
        if (handler) {
            handler->execute(msg);
        } else {
            // exception handler
            …
        }
    }

    对应的注册代码也就变成:
    handlers["open"] = new DispatchHandlerImpl<MsgHandler>(&MsgHandler::open);
    handlers["close"] = new DispatchHandlerImpl<MsgHandler2>(&MsgHandler2::close);

    有代码洁癖的我们发现类名在这里重复了,于是,定义一个宏对其进行简化:
    #define DISPATCH_HANDLER(className, funcName) \
      DispatchHandlerImpl <className>(&className::funcName)

    handlers["open"] = new DISPATCH_HANDLER(MsgHandler, open);
    handlers["close"] = new DISPATCH_HANDLER(MsgHandler2, close);

  • 又和一个团队合作,面前又摆着一堆分发的代码,不同的是,这次用的是C++:
    if (msg->id == "open") {
        MsgHandler handler(msg);
        handler.open();
    } else if (msg->id == "close") {
        MsgHandler handler(msg);
        handler.close();
    } else if (…) {
        …
    } else {
        // exception handler
        …
    }

    不要问我为什么不是每个消息对应一种处理类,要是知道为什么,就不是遗留代码了。于是,我们尝试着用C++写了一个dispatcher。下面是这个dispatcher的声明:
    #include <map>

    typedef void (MsgHandler::*handlerFunc)();

    class MsgDispatcher {
    public:
        ...
        void dispatch(Msg* msg);
    private:
        std::map<string, handlerFunc> handlers;
    };

    因为要处理遗留代码,这里用到了指向成员函数的指针,也就提高了理解这段代码的门槛。具体实现如下:
    void MsgDispatcher::dispatch(Msg* msg) {
        handlerFunc func = this->handlers[msg->id];
        if (func) {
            MsgHandler msgHandler(pkg);
            (msgHandler.*func)();
        } else {
            // exception handler
            …
        }
    }

    注册很简单:
    handlers["open"] = &MsgHandler::open;
    handlers["close"] = &MsgHandler::close;

  • 用条件语句做分发是一件很常见的事:
    switch(msg->id) {
        case ID1:
            ID1Handler(msg);
            break;
        case ID2:
            ID2Handler(msg);
            break;
        ...
    }

    条件稍微多一些点,函数就会变得冗长不堪。有的团队会直接用if..else,进行判断,于是我们有幸知道了,VC不支持超过128个的选择分支。为了让这种代码的可维护性更好,我们做了一些尝试。

    下面定义了一个dispatcher:
    BEGIN_DISPATCHER(MSG, ID, MsgHandler)
        DISPATCH_ITEM(ID1, ID1Handler)
        DISPATCH_ITEM(ID2, ID2Handler)
    END_DISPATCHER(DisasterHandler)

    首先,用BEGIN_DISPATCH_MAP定义了这个dispatcher的名字(MSG),用做分发键值的类型(ID)和处理函数的类型(MsgHandler)。接下来,用DISPATCH_ITEM定义了几个分发项,也就是说,如果传入的值是ID1,会用ID1Handler进行处理,如果是ID2,则对应着ID2Handler。最后,用END_DISPATCH_MAP定义了一个错误处理函数。这样的话,就把使用的时候,就不必额外去做判空的操作了。这是Null Object模式的一种体现。

    这个dispatcher的使用方式如下:
    dispatch(to(MSG), with(msg->id))(msg);

    这段代码的含义是使用MSG这个dispatcher,根据msg->id找到对应的处理函数,传入的参数是msg。

    这个dispatch框架的实现如下:
    #include
    #define SIZE_OF_ARRAY(array) sizeof(array)/sizeof(array[0])

    /* dispatcher definition */

    #define __DISPATCHER_NAME(name) __dispatcher_##name
    #define __DISPATCH_ITEM_NAME(name) __dispatch_item_##name
    #define __IsMatched(target, source) (0 == memcmp(&target, &source, sizeof(target)))

    #define BEGIN_DISPATCHER(name, key_type, handler_type) \
        struct __DISPATCH_ITEM_NAME(name) {\
            key_type key;\
            handler_type *handler;\
        };\
        \
    handler_type* __DISPATCHER_NAME(name)(key_type key) \
    {\
        static struct __DISPATCH_ITEM_NAME(name) dispatchers[] = {\

    #define END_DISPATCHER(disaster_handler) \
        };\
        int i;\
        int array_size = SIZE_OF_ARRAY(dispatchers); \
        for (i = 0; i < array_size; i++)\
        {\
            if (__IsMatched(dispatchers[i].key, key)) {\
                return dispatchers[i].handler;\
            }\
        }\
        return disaster_handler;\
    }

    #define DISPATCH_ITEM(key, handler) {key, handler},
    #define DISPATCH_ITEM_2(key1, key2, handler) {key1, key2, handler},
    #define DISPATCH_ITEM_3(key1, key2, key3, handler) {key1, key2, key3, handler},
    #define dispatch(name, key) __DISPATCHER_NAME(name)(key)
    #define to(name) name
    #define with(key) key

    从这里可以看出,定义dispatcher,实际上是定义了一个函数,这个函数的返回值是一个函数指针,而这个函数指针的类型是由handler_type定义的。这样的话,就解决了不同dispatcher之间函数参数不同的问题。

    当然,这个处理里面采用了最简单的数组,在分发项不是很多的情况下是适用的。如果分发项较多,可以考虑改为map实现。另外,这里还运用了一些宏的技巧。在写应用代码时,我们并不鼓励多用宏。但写一些框架代码,为了提高使用上的表现力时,宏也会是一个利器。

    如果有兴趣,欢迎讨论!

  • 使用Java实现内部领域特定语言
    One Lair and Twenty Ruby DSLs
    Implementing an Internal DSL

    上面几个文章都是关于DSL的,不过,在这里,我并不是太关心DSL的话题,我更感兴趣的是代码的写法。按照这几篇的分类方法,直接用程序设计语言编写的DSL算是内部DSL,也就是说,所谓内部DSL,也就是一种标准的程序代码。

    Kent Beck在他的《Implementation Patterns》的第三章《A Theory Of Programming》中,谈到了编程的价值观(Value):Communication(沟通)、Simplicity(简单)和Flexibility(灵活)。如果说简单和灵活很容易理解的话,那么把沟通放在价值观中,尤其排在所有价值观的第一位,则显现出Kent Beck对于编程的深刻。在这个软件开发越来越需要协作的年代,写代码的时候,多站在让别人理解的角度考虑一下,会极大提升代码的可读性。在ThoughtWorks的招聘流程中,有一个Code Review的环节,拜这个环节所赐,我看过很多人的代码,不在少数的应聘者其代码唯一的优点就是完成了需求。以沟通为标准进行衡量,这显然是不够的。

    同样,以沟通为标准,那么内部DSL显然在这方面做得更好,因为DSL本身就是为了让人更容易理解而存在的。这几篇文章中提供了很多内部DSL的手法,比如Method ChainingExpression Builder等等。抛开DSL这样的BuzzWord,这些方法应该属于增强程序本身表达能力的方法。

    JDK有一个很好的Method Chaining的例子:StringBuffer的append方法。

    StringBuffer sb = new StringBuffer();
    sb.append("log1").append("log2").append("log3");

    这样的写法显然比下面的写法更为简洁,尤其是需要往StringBuffer中添加很多内容的时候。

    StringBuffer sb = new StringBuffer();
    sb.append("log1");
    sb.append("log2");
    sb.append("log3");

    在我看来,这些内部DSL技术为我们打开了一扇窗,它让我们在编写代码,尤其作为API提供的代码时,有了一个新的思考方向。当然,并不是一味的应用这些内部DSL技术就会写出好代码,作为一个有经验的软件开发人员,我们需要一定的鉴别能力,分辨出究竟怎样做才会真正的提高代码的“沟通”能力。

    之前写过两篇关于程序设计语言表达的blog(12),虽然这篇不像那两篇一样讨论语言的差异,但也算是在语言表达能力上的探讨吧!