b) 大量静态方法放在“*Comm”类中。 i. 现象:大量静态方法放在“Comm”类中。很像C的函数库。(这个现象常见么?好不好?) ii. 评述:这是一种使用面向对象语言编写面向过程代码的尝试。我个人觉得这类尝试是一种倒退,拒绝面向对象所带来的所有特性。没有很好地封装,程序的结构化不好;使用方与之的依赖是静态的;没有可插入性。
iii. 措施:解决这个问题的做法是,按照操作实施在哪个对象身上来把操作规划到对象所在的类里面;如果保持静态方法不变,也不要所有方法放到一个类中,最好按照语义来划分到合适的类。JDK类库提供了那么多方法,很少出现静态函数库的现象。当然,也不是说不存在,比如Math、ArrayList等类,不过至少他们在语义上分得很清晰。一个例子,比如,String的substring方法,很可能被一些程序员设计成public static,因为他们可能觉得无法把这个方法归属到哪个类中,于是放到“Comm”类中。(大家体会一下?是不是这么个事儿。) iv. 疑问:那不是想调用某个方法的时候就要new一个实例(性能问题!)?首先,这又是面向过程的思维方式,想的是过程、调用,而不是对象、依赖、关联。其次,轻量级的对象,创建、回收成本很低。我曾经对一些不同算法、策略从性能角度做过相应的比较试验,通常,执行次数在10w-100w以上量级时,才有差别。我使用面向对象的做法,可以明显地获得更优的可维护性(可读、可扩展、可改、可插入、可重用),而且面向对象本身不会造成什么性能问题。当然,具体情况要具体分析,如有明显的性能隐患,最好能够做一个简单的试验,用数据来说话。做个类比,说某些政客在很多场合的潜台词中认为,民主、自由会破坏安定的大好局面,显然不能够让人信服;同样,说面向对象增加了对象的创建、销毁成本,会影响性能,影响软件系统的稳定局面,也是不能够让人信服的。于是在编程时尽可能地使用静态方法,这种做法,不可取。
3. 类长到多大才算“Large”?类,应该较小、可以较大,只要该类从语义上无法再次拆解。(“发动机”类,可以包含对“螺丝”类的引用(关联),但不要把“螺丝”类的操作也放到“发动机”类中来实现。)
2.4 Long Parameter List(过长参数列)
1. “过长参数列”有什么不好?(难读、难用、难记,还有一点,无法获得对象所带来的优势。比如,参数之间的约束关系没有得到很好地封装。例如,startTime/endTime,他俩作为参数来讲,可能不算长,这里仅做示例来说事儿。这对表示时间范围的参数可能多处使用,在没有包装成对象的时候,如果要保证“startTime < endTime”这个约束,就需要所有用到这对参数的地方都做判断;包装成对象,情况就好多了,比如叫做TimeRange,在类的构造函数中可以做这个判断。显然,使用对象,把变化封装得好一些。)
2. 想一下,JAVA类库的函数,比起C类库的函数,传递的参数是不是大都短很多?(应该是。这体现了面向对象的优势。) 3. “参数太多”这个Smell如何去除?
a) Introduce Parameter Object,无非是把多个参数封装成一个对象。 2.5 Divergent Change(发散式变化) 1. 什么是“发散式变化”?
a) “某一个类受到多种变化的影响”,A/B/C/D……多种功能变化的时候它都需要修改。 2. 为什么会造成“发散式变化”?哪儿没弄好?
a) 大致是由于这个类担负了多项任务,太操心了,不该他做的事儿也来做,越俎代庖。很可能需要再拆分几个类出来,把变化封装得更细。 3. 历史教训(反面教材)(以前我写配置MAF端代码的时候,写过一个P_Unit类,他处理所有BSC单元的逻辑,但各种单板的逻辑是不一样的,于是DTB改逻辑的时候要修改P_Unit、ABPM改的时候要修改P_Unit、IPCF、UPCF、GCM……所有具有特殊逻辑的单
板修改功能的时候,他都要修改,甚至HDLC/UID等逻辑修改的时候P_Unit都要改。显然该类管得太多了。后来,我看了一本书,翻然悔悟,痛下把代码决心做了重构。其实早在03年,徐峰(据说徐峰要离开公司,这么牛的人离开了对我们整个OMC损失很大,我在这里提一下他的名字,简陋地送别一下。)做配置CAF的时候建议我针对每种有特殊逻辑的板子弄一个类,我完全不以为然。显然,当时没有理解“封装变化”这四个字。) 2.6 Shotgun Surgery(霰弹式修改) 1. 什么是“霰弹式修改”?
a) “一个变化引发多个类的修改”,完成某个需求的时候,A/B/C/D……多个类都需要修改。 2. 为什么会造成“霰弹式修改”?哪儿没弄好?
a) 大致是多个类之间的耦合太严重。很可能是类没有规划好,没有把变化封装得足够令人满意。
3. 一个插曲:记得此前讨论这个Bad Smell的时候,严钧认为,去掉这个Bad Smell不好强求,而且举出Abstract Factory模式作为例证。也有道理。我在这一点上是这么认为的:我们要清楚的认识到我们努力的方向,Abstract Factory模式同样不完美,它没有满足Open-Close原则。我们可以在某些条件(包括技术条件)受限的时候写出不完美的代码,但一定要知道它是不完美的。
a) Factory Method模式(工厂方法)代替Abstract Factory来说事儿。
每增加一种Produce的实现类,就要同时增加一个对应该类的Creator类。当时严钧可能说的是Abstract Factory模式,我用Factory Method模式来说事儿,因为他简单些,但同样可以说明问题。 b) Open-Close原则
软件实体应该对扩展开放,对修改关闭。Open-Close原则是一个愿景性质的原则,如果系统能够达到Open-Close原则描述的情形就比较理想了,对扩展开放、对修改关闭,即,不修改原有代码即可完成对系统的扩展。系统可以获得最大可能的稳定性,加功能的时候旧有代码不修改,当然不会带入BUG。 ? 玉帝招安美猴王的故事
齐天大圣美猴王,想当初可是功夫了得,从东海龙王那儿拿了根棍儿,大闹天宫……叫嚣得不行(后来怎么不灵了,一根灯草、一根头绳、一条看大门的狮子狗都整不过),喊出来一些革命口号:“皇帝轮流做,明年到我家”,“只教他搬出去,将天宫让与我!”。有一些农民起义领袖的风范。
太白金星给玉皇大帝打了个报告出主意:“把他宣来上界……与他籍名在箓……一则不动众劳师,二则收仙有道也”。
玉皇大帝遵循Open-Close原则招安了美猴王。不动众劳师,不破坏天规,是关闭对已有系统的修改,不修改,是Closed。收仙有道,是对已有系统的扩展,可扩展,是Open。
同时应用了依赖倒换原则,合成/聚合复用原则,以后有机会给大家讲讲面向对象的设计原则。
4. 讲回霰弹式修改这个smell,很多程序在接手时,前辈一再嘱咐,改什么功能的时候,一定要注意,什么什么……一堆地方必须同时修改,要细心不要漏了……这很可能是设计水平的问题给维护造成的难度。其实如果程序设计得好,此后的工作将愉快很多。(插播广告)我记得,刚看到斯诺克比赛在电视上转播的时候(那时候还小,初中吧,还没见过真的斯诺克台球桌),很不屑,觉得他们大部分时候在打一些比较近距离的球,最多半张台子距离吧,我甚至盲目自信,感觉那些球我都能打进,电视上的人有什么了不起。其实大家都知道,母
球走位是难度更大、更重要的工作(要经过全盘性思考的),走位走好了,下一杆就好打;就好比做软件,程序结构设计得好,维护就更容易。 2.7 Data Clumps(数据泥团) 1. 什么是“数据泥团”?
a) 某些数据项经常黏在一起行动,称之为“数据泥团”。时间长了就应该考虑是不是该把他们封装到一个对象中,来封装这个泥团所可能具有的逻辑。(比如一堆表示定位信息的字段,system、subsystem、unit、rack、shelf、slot……总是一起出现。)(这个Bad Smell在在很多时候与Long Parameter List,是一样的,但Data Clumps的涵盖范围比Long Parameter List要大一些,比如,某些类的Field,可能没有当作参数来传递,但是总是黏在一起,也可能出现数据之间的逻辑,于是也需要绑成一个对象,来做封装。) 2. 如何判断是否属于“数据泥团”?
a) 删掉这些数据项中的其中之一项,其他数据有没有因此失去意义?(比如startTime/ endTime,就是成对表示时间范围的,去掉其中一个,另一个失去意义。) 2.8 Switch Statements(Switch惊悚现身)
(惊悚现身,很像香港翻译好莱坞电影片名的风格是么?) 1. Switch语句有什么不好?
a) 容易形成“长函数”(比较容易理解) b) 容易形成“霰弹式修改”
2. 如何替换掉Switch语句?
a) 多态(使用Pet的例子的第三个版本来说明) 3. 是不是使用多态可以去掉所有Switch语句?
a) 不是。比如,根据消息号,分发把消息分发到相应的处理函数(处理类)来处理。(原因是某些情况下,调用端无法动态创建确切(子类的)实例,于是依然需要分发过程。即,需要Switch语句分发、或者“配置文件+反射”的方式分发。) 4. 对Switch语句有什么要求?
a) Switch语句可以存在,但每个case的处理语句不应超过2-3行。 2.9 Comments(过多的注释)
(首先,注释本身没有错,很多时候注释是必须存在的。但,注释过多,就是坏味道了。) 1. Why?为什么?
a) 过多的注释,是降低代码可读性的帮凶。(如果,代码只有通过大量注释才能被理解,那么说明代码的可读性不好。事实上,很多文章也就此有些说法:代码要写得“自解释能力强”、自己解释自己;代码就是文档。这就要求,类、方法的编写要清爽,类名、方法名、变量名要起得好。) 2. How?如何写好注释?(Why?How?想起那个关于两个渔夫和一个美人鱼的荤笑话,由于有未成年人士在场,我不便当众详细讲。) a) 写“why”。(注释应该写代码的编写思路,特别是某些地方没有按常理出牌,要写注释来说明。比如,对数组做for循环遍历,边界一般是数组的length,如果某一次出于某种特殊考虑,没这么做,就需要注释说明。) b) 不写“what”。(注释不要写代码是干什么的,“what”这样的信息应该尽量包含在类名、方法名、变量名中。) c) 不写充数注释。(不要为了写注释而写注释,不要往猪肉里注水,虽然没什么大碍,但终归是没品味的做法。比如,“String a = null;//创建一个String实例。”看到这样的注释,我胃口都不舒服。虽然部门有注释比例的要求,但像我们这样的高级程序员、高级工程师,还是不要充数用的注释。)
3 Refactoring
3.1 Extract Method void printOwing() { printBanner(); //print details
System.out.println (\
System.out.println (\}
重构为:
void printOwing() { printBanner();
printDetails(getOutstanding()); }
void printDetails (double outstanding) { System.out.println (\
System.out.println (\}
(前面说了,函数应该短。重复一下带来的好处:可读性好,高层函数像注释、低层函数行数少;可重用性好,比如上例中的printDetails,可能别处也能用,重构前是无法被重用的;可插入性好,子类可能写一个新的printDetails,使用不同格式打印。) 3.1.1 抽取函数时候,参数、临时变量如何处理
1. Replace Temp With Query 去掉临时局部变量,代之以查询类的方法,拆开的小函数需要此临时变量的时候,就调用这个查询方法。
2. Introduce Parameter Object 让长参数列变得简洁。
3. Replace Method with Method Object 去掉多个参数、局部变量。为待重构的大函数创建一个对象,这样,所有方法内的临时变量就变成对象的field,于是大函数拆开的所有小函数就共享这些field,不必再使用参数传递。 3.1.2 起名字很重要!名字应该:(此处的名字包括函数、类等) 1. 清晰、恰当。表达的信息涵盖函数的所作所为。
a) 当一个函数名字为了涵盖函数所为“必须”起成“do1stThingAndDo2ndThing”的时候,就有必要实施Extract Method来抽取函数了。 b) 一个OMC代码中的例子,某函数叫做checkParameter,但函数体中除了检查参数之外,还“顺便”为几个类属性赋值,虽然此函数很短、很超值,但我们认为他的命名是不恰当的,甚至他的函数设计也是不恰当的,一个函数要干单纯的一件事儿,函数内部从语义上无法再次分解。
2. 尽量简短、可以较长。但应该首先满足上一条要求。 a) compareToIgnoreCase(String类的方法)、getDisplayLanguage(Locale类的方法)、getTotalFrequentRenterPoints(《重构》书中的示例代码),这些函数名长不长?(重要的是把信息表述清楚,名字长一点没关系。) 3.2 Replace Temp with Query
double basePrice = _quantity * _itemPrice; if (basePrice > 1000)
return basePrice * 0.95; else
return basePrice * 0.98; 重构为:
if (basePrice() > 1000)
return basePrice() * 0.95; else
return basePrice() * 0.98; …
double basePrice() {
return _quantity * _itemPrice; }
(例子很容易理解,basePrice是临时变量,临时变量的问题在于:它们是暂时的,而且只能在所属函数内使用。由于临时变量只有在所属函数内才可见,所以它们会驱使你写出更长的函数,因为只有这样你才能访问到想要访问的临时变量。如果把临时变量替换为一个查询式(query method),那么同一个class中的所有函数都将可以获得这份信息。为拆解大函数提供了方便。)
3.2.1 一个借助Replace Temp with Query来提炼函数的例子 double getPrice() {
int basePrice = _quantity * _itemPrice; double discountFactor;
If (basePrice >1000) discountFactor = 0.95; else basePrice = 0.98;
return basePrice * discountFactor; }
重构为:
double getPrice() {
return basePrice() * discountFactor(); }
private int basePrice() {
return _quantity * _itemPrice; }
Private double discountFactor() { If (basePrice() >1000) return 0.95; else return 0.98; }
(重构前,在getPrice方法中,先计算基础价格、再计算折扣因子、再计算最终价格,做了语义上可以再次拆解的三件事儿,不符合“函数只做一件事儿”的要求,于是使用Extract Method方法来重构。借助Replace Temp with Query重构方法,将临时变量basePrice、discountFactor用相应的查询函数来替代。 需要指出的是,此次重构,查询函数basePrice被调用了两次,损失的一点点性能可以忽略,我们认为这样做是值得的。) 3.3 Split Temporary Variable
double temp = 2 * (_height + _width); System.out.println (temp); temp = _height * _width;