很遗憾,之前虽然读过一边《代码大全2》,但是对表驱动却只记住了这个名词,始终没有用到它。这几天在写服务器和客户端的交互,避免不了的要处理各种服务器传递过来的消息,而我使用的却仍是原始的消息类型逻辑判断(也就是if-else),原因是1)我不知道表驱动;2)消息类型的种类在我可接受范围之内。今天突然想到表驱动这个名词,于是又翻开《代码大全2》,却意外发现这个处理消息的利器。
且不说书中的例子。写过客户端和服务器端消息交互的人都有深刻感受:避免不了的if-else或者switch-case,而且很多,很多,很多,还经常变化(消息种类增加),代码越写越长,越写越长,最后整个屏幕都密密麻麻的!最差的一种写法是把逻辑判断和处理代码全部混杂在一起,那你完全可以写出一个上千行的函数出来,好一点的是,将处理代码独立成函数,这样稍微清楚一点(但是如果一个外来者想看你是如何处理消息的,那他从逻辑判断到处理代码的来回跳转查看足够逼疯他),更好一点的是,使用继承一类的手法,这样其实和前面一样,需要不停的来回跳转查看处理和逻辑之间的关系,但是好处就是代码复用,可以为消息生成比较统一的处理结果,将共同点集于父类,不同点分散于子类,缺点就是,一个消息就需要一个子类,而且很多时候,消息之间的差异其实并不是那么大,比如,只是类型不同。但是最后一种方法我觉得是目前我所能做到的最好的处理方式,尤其是你还要在代码中使用处理后的结果。
上面说道处理消息其实有两部分,一部分是逻辑判断消息类型,一部分是处理消息。后者较好的方法我上面已经讲过了,可以用继承。而表驱动则是解决前者的复杂性,帮助我们很快的判断消息类型,建立类型和消息处理代码之间的逻辑关系。
书中有一个很经典的例子,就是求每一个月份的天数。很简单,if-else完全可以实现这个功能,而平年闰年则更不在话下。但是很丑陋,不是么?表驱动则提供了一种非常优雅的方式:建立数组。建立一个包含12个int元素的数组monthsArray[12],依次记录每个月份的天数,这样,你要任何一个月n的天数只要调用如下的方法即可:
1 monthsArray[n-1]
相较于将数据硬编码在代码中,这样保存数据不但清晰,减少代码量,而且更容易维护,更容易理解。
这是一个条件的(月份数就是条件),复杂一点的当然就是多条件,很容易想到的就是多维数组,这里不多做解释,下面要描述的更为重要:那就是如何将条件映射为数组下表(或者称为表的维度描述符)。
月份的例子巧就巧在月份刚好是整数,而且是连续的,可以作为数组的下标。那么如果是一个区间范围呢?方法有以下几种:
1)复制信息,从而能够直接使用键值——最最简单的方法,却不总是奏效;
2)映射:使用一个函数,将区间里面的值全部映射到某一个固定的值,比如max(min(66,age),17)来生成一个17到66之间的表键值;
那么假如是字符串呢?
1)可以将键值转换独立成为子程序(具体的代码视情况而定,后面会讨论);
2)使用语言自带的键值转化功能,比如HashMap(神器);
上面说的是最简单的一种索引情况,我们将一些值映射成为整数,然后做成数组进行查询(个人觉得:这样子的程序需要使用英语全文定义好变量名,否则会缺乏可读性,最好是专门定义一个类保存相关变量)。下面说一些并不那么简单的。
假设我们有100种商品,每种商品的编号是4位数,如何建立索引呢?(一个不可忽略的事实是编号恰好是整数,可以用来作为下标)。
假设我们直接将编号作为下标建立数组,那么我们需要建立一个可以容纳10,000个元素的索引数组,但是除了有100个有元素之外,其余的都是空的。读者可能觉得这边过于浪费,但是问题其实并不在这里,我们假设浪费不可避免(换句话说,上面提到的创建连续键值的方法全部失效,这边的商品编号杂乱无章),那么我们要做的就是如何降低这种浪费。
假设每个商品的信息需要100字节进行存储,那么如果直接创建一个表用来存储这100种商品的信息,我们需要一个100(字节数)*10000(可能的种类)=1,000,000个字节,而浪费的是100 * 9900 = 990,000个字节。现在我们使用索引法解决这个问题:
将100种商品密集存储,放在一个可以容纳100个元素的表中(称为主表),需要字节数:100*100=10,000个字节。接着,我们再创建一个可以容纳10000个元素的表格,这个表格并非存储商品信息,而是存储商品编号和主表的对应关系(比如下标为1234的元素里面存储1,就代表商品编号为1234的信息存储在主表中下表为1的位置),假设每个记录需要4个字节,那么总共需要10000 * 4 = 40,000字节,两张表加起来不过50,000字节,远远少于之前的表所占的空间。
在《代码大全2》中,该方法称之为索引访问表。书上还讲了一种称为阶梯访问表的方法,暂不描述,有兴趣的自己去看。
下面回到最初的主题,我一直在思考这个问题,如何才能将表驱动应用到消息处理中去,在《代码大全2》中,作者举了一个例子,但是我觉得并不典型,因为并非所有的数据都是打印这么简单,很多时候我们需要将数据处理成对象以便后续使用,作者在大作中将消息的每一个Item的类型和处理方式映射成为表,将所有的消息类型混在一起处理,我觉得既不优雅,也太过特殊。作者的终极目的是:如何可以不为每一种消息写一个处理方法。我依然觉得这个目标太过,程序员不应该这么偷懒,使用OO中的继承已经很大程度上减少代码的复杂性,并且独立出了变化的部分,只是!:逻辑判断部分总是要根据消息类型的增减变化不断的修改,这边是完全不符合开闭原则的,如何减轻这里的影响呢:将逻辑独立出代码,以数据(表)的格式进行保存。
因为有HashMap这样子的神器存在,我们很容易将处理代码封装成一个个处理Object,然后将通过HashMap将消息类型(一般是字符串)和处理Object对应起来,这样子,我们只需要将消息通过HashMap传递到处理Object中就可以顺利处理消息,获得任何想要的处理结果。
(注:个人意见:表驱动存在的映射,尤其是需要自己写映射函数的,需要多加测试,否则很难发现BUG,这块映射必须足够健壮才可以。)