Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2013-12-22) 分解的粒度

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

这篇日志真不好写,已经重写了两遍,每次都是写到一大半时,发现自己对于问题本质的理解不对,于是删了重写,内容也完全不同。希望这次能把它写完。

日志的起因是我们项目里需要写一个测试工具,我写的代码受到了同事的质疑,他认为我的方案要写太多的类,而且宁愿用if...else来做。

简单的说,这个测试工具,需要对不同Module的不同检查点进行检查。可以假设我们有下面一张表:

 ||---------------||---------------||---------------||------------------||
 || Module Name   ||  Dir Checker  ||  DB Checker   || Storage Checker  ||
 ||---------------||---------------||---------------||------------------||
 ||    ModuleA    ||       Y       ||     OK        ||    /aaa.xml      ||
 ||---------------||---------------||---------------||------------------||
 ||    ModuleB    ||       Y       ||     OK        ||    /bbb.xml      ||
 ||---------------||---------------||---------------||------------------||
 ||    ModuleC    ||       N       ||   FAILED      ||    /ccc.xml      ||
 ||---------------||---------------||---------------||------------------||


我们的测试工具,需要读取这样的一个表,根据其内容对相应的Module和检查点进行检查。

## 我的方案

我的方案是将每个Module的每个检查点,都作为一个独立的类。这样当以后有新的Module或检查点出现时,可以不用修改已有任何代码,只需要创建新类。

对于上表中的ModuleA,定义了三个类:

class ModuleADirChecker {
     void check(String shouldInOrNot);
}
class ModuleADbChecker {
     void check(String expectedStatus);
}
class ModuleAStorageChecker {
     void check(String remotePath);
}


对于ModuleB和C,也将会各有三个类,这样就会有9个类。

如何将每个类与module/checkpoint匹配起来呢?最简单的方式是通过一堆`if...else...`来判断:

public void getChecker(String module, String checkPoint) {
    if(module.equals("ModuleA")) {
         if(checkPoint.equals("Dir Checker")) {
              return new ModuleADirChecker();
         } else if(checkPoint.equals("DB Checker")) {
              return new ModuleADbChecker();
         } else if(checkPoint.equals("Storage Checker")) {
              return new ModuleAStorageChecker();
         }
    } else if(module.equals("ModuleB")) {
         ...
    } else if(module.equals("ModuleC")) {
         ...
    }
}


我想到用注解,可以会让代码更加独立一些,于是写了一个`CheckerAnno`注解,这么用:

@CheckerAnno(module="ModuleA", checkPoint="Dir Checker")
class ModuleADirChecker {
     void check(String shouldInOrNot);
}


每个checker上,都放一个`CheckerAnno`,指示它匹配的是哪个module和checkPoint。在程序运行时,将会扫描代码,根据`CheckerAnno`中的数据,自动把各checker与excel表中的单元格匹配起来,生成实例,传入数据进行检查。这样我们就不需要写上面那个长长的`getChcker`方法了,并且以后添加新的Module或者Column时,也不需要修改**任何**已有代码。

## 同事的方案

同事觉得我的方案不好,因为他觉得我的类太多了。而且他发现,某些检查点虽然要检查多个Module,但它们的逻辑非常相似。比如`Dir Checker`仅仅需要不同的Module提供一个不同的值,实际的检查逻辑是一样的。

他给出的方案是:每个检查点一个类,在类里通过`if...else`来针对不同的Module做不同的检查。比如对于`DirChecker`,他是这么做的:

class DirChecker {
    public void check(String moduleName, String shouldInOrNot) {
        File dir = getDir(moduleName);
        checkDir(dir, shouldInOrNot);
    }
    private File getDir(String moduleName) {
        if(moduleName.equal("ModuleA") {
            return localTempDirOfModuleA();
        } else if(moduleName.equals("ModuleB")) {
            return localTempDirOfModuleB();
        } else if(moduleName.equals("ModuleC")) {
            return localTempDirOfModuleC();
        } else {
            throw new IllegaArgumentException("Unknown module:" + moduleName);
        }
    }
}


对于其它的每一个检查点,也都需要定义一些这样的类,每个类中通过`if...else`来决定哪个Module进行哪种检查。

我觉得他这种方式的关键,不在于是否使用了`if...else`,因为他也可以使用注解来实现,跟我那种一样:

@CheckPoint("Dir Checker")
class DirChecker {
    @CheckModule("ModuleA")
    public void check1(String shouldInOrNot) {
         ...
    }
    @CheckModule("ModuleB")
    public void check2(String shouldInOrNot) {
         ...
    }
    @CheckModule("ModuleC")
    public void check3(String shouldInOrNot) {
         ...
    }
}


然后可使用相同的方式扫描注解,将每个check方法与相应的module和checkpoint匹配起来。虽然每当增加一个新Module时,他需要修改已有的类,添加一个新的方法并加上相应的注解,但由于这样的方法相当于一个入口,不会修改已有的逻辑,所以也是可以接受的。

## 第三种方案

可以很自然的想到第三种方式:以行为类。即为每个Module定义一个类,里面提供多个方法分别对应每个检查点。如果用注解的话,大约是这样的:

@CheckerModule("ModuleA")
class ModuleA {
    @CheckPoint("Dir Checker")
    public void check1(String shouldInOrNot) {}
    @CheckPoint("Db Checker")
    public void check2(String expectedStatus) {}
    @CheckPoint("Storage Checker")
    public void check3(String remotePath) {}
}

问题的本质

所以问题的关键就在于粒度:对于这个例子,哪种粒度更好?行?列?还是单元格?

如果说这三种方案都是差不多的,随便选一个就行,那就省事了,看自己的喜好即可。否则的话,我应该以什么为考虑的重点,来决定采用哪一种粒度呢?


有同学说看不懂我在问什么。

的确我也感觉不知道自己在问什么,因为这个问题产生于真实的项目,很多影响我思考的因素也许在本文外,但我还没有意识到。从一个复杂项目中,把一个问题以最简方式完整的剥离出来,本身就是一件很难的事吧。如果真的能把所有有关联的因素抽出来了,所有没有关联的因素都留下了,也许答案自然就出来了,不需要再问了。

看来这个问题只能等我自己解决了。

一些评论

杨博

你的同事对,你错。

你用的技巧,耦合更低,如果基类要提供给第三方,让第三方自己实现派生类,那就很好。

但这个例子是你们的内部测试代码,属于一个模块内部的实现,当然应该遵循“高内聚”而不是“低耦合”原则。那么你同事代码短,耦合重,都是优点。

当然,如果你们部门决定把测试时常用模式提成一个框架,让所有测试代码遵守这个框架的限制。那么作为框架的设计,也许可以重新考虑你用的基类方式。

@杨博,谢谢你高质量的回复。你提到的几点,我想了一下,现在这个测试工具的确已经成了一个测试框架,由测试组的成员使用,不过仅仅是当前项目的测试组内部使用。他们在未来所写的所有测试代码,都要按同样的规则来写。

另外我刚才又回想了一下,发现当时我这么设计还有一个原因:

虽然会以excel表格的形式来定义一些测试,但同时还需要支持纯手写java代码的方式,来实现一些特殊的测试用例。

这些测试用例通常只会对应某一个module,这样我就可以方便的直接实例化某个特定的类,如new ModuleADbChecker().check(…),非常干净。如果用同事的方法,就会变成new DbChecker().check(“ModuleA”,…),感觉调用了一个很大的方法,实际上只用到了其中很小的一块代码。

这样分析的话,我认为我的粒度还是最合适的。多谢你的意见!

liusong1111

类多类少不重要,是否采用switch/case不重要;代码朴素很重要。你怎么也犯java系的通病了呢,”未来扩展”、”代码优美”,这些事在真正发生之前根本一点都不重要,一点都不应该考虑。面向对象,尤其是java系的面向对象,就是纸老虎,还不如手写utils静态方法接地气。

你的方式合理。那个StorageService就是一个util

@liusong1111,哈哈,在现在的公司的大环境下,朴素不起来啊。这些天正在看设计模式、重构这些书,不自觉就想在工作里练练手。

另外还有两个原因:

一是这个项目的客户要对每一个完成的功能点签字,一旦上线再想修改就非常麻烦,所以只能趁上线前多想一点。

二是我们来之前的代码,一方面到处是完全没用上的扩展点,另一方面是朴素的连单元测试都没法加的代码,已经让我们加了两个月的班。

所以我们正在吸取教训,让现在写的代码方便测试,耦合度低一点儿。

AlsoTang

我也觉得你同事的方法或许比剩下两种方法更加能够抽出共性来。

例子太小,怎么做都足够灵活。
凡是能够用继承做的地方,用组合做其实都很容易。

但在这个地方明显是 is-a 的关系嘛。

这么说吧,如果以后 StorageService 提供了更丰富的功能的话,那么 UserStorageService 用不用跟进去实现?
如果总是要跟进的话,那么就应该是继承的;如果不必跟进,只是简单利用 StorageService 的 save 和 get 接口的话,那么就组合。

在这个地方来说,难道 UserStorageService 不是应该一直跟进 StorageService 的新功能的吗?

回答@AlsoTang的问题,在我们实际的使用中,发现是把UserStorageService当作一个窄化的接口来用的,即只会使用它里面定义的方法,而不会用到父类里的。所以我现在更肯定它用组合好一些,也许命名方面应该改改。

kou_zx

我觉得不用非得定死按行还是按列分粒度,我觉得对于一个checker,如果逻辑相似(比如你说的DirChecker)就写一个类里,逻辑差距比较大就按module分类

comments powered by Disqus