`
micc010
  • 浏览: 73143 次
  • 性别: Icon_minigender_1
  • 来自: 广西
社区版块
存档分类
最新评论

Bad Smell & Refactoring

阅读更多
原创作者: 王杲杲

Bad Smells & Refactoring
1 题记
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.——Martin Fowler
(任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。)
2 Bad Smells
2.1 Duplicated Code重复的代码
(重复代码,不需要定义,大家都知道是什么东东。)
1. “重复的代码”有什么不好?(比方说,从可维护性的角度看,很多地方的代码看似一样,又有可能有细微差别,对读代码的人容易产生困扰,于是可以定罪,可读性不好;同样的修改可能需要修改多次,而且容易遗漏,可修改性不好。)(但是,重复代码也有好处啊,代码行数多,千行代码故障率会少,对我们的考核比较有利……开玩笑的是么?)
2. 代码重复到什么程度,Smell才算Bad?(if (filename == null || filename.trim().equals(""))这句话可作为例子,我们认为这句话就算是重复代码了,比如哪天filename.trim().equals("")这个判断我觉得不好,想要换成filename.trim().length() == 0,那么岂不是霰弹式修改?使用绝技“Ctrl-C/ Ctrl-V”、同时还有用“Ctrl-F”,因为大家没有住在一起,要搜索一下才联系得到……)
3. 三种情况的重复代码:
a) 同一个class中的两个method含有相同表达式。(Extract Method)
b) 两个互为兄弟的subclass内含有相同表达式。(Extract Method、Pull up Method、Form Template Method)
c) 两个毫不相关的class内含有相同表达式。( Extract Method、 Extract Class)
2.2 Long Method过长函数
2.2.1 “过长函数”有什么不好?
1. 可读性:使用短函数,高层函数看起来像系列注释、低层函数不超过(比方说)10行,逻辑一目了然,可读性更优。
2. 可重用性:(长函数可能包含逻辑A/B/C,想单独重用A逻辑,不可能。一个类比的例子,发动机的重用机会肯定比车大。大家知道深圳BYD使用的是三菱发动机,标致307/206 1.6系列使用的发动机跟富康、爱丽舍16V系列是相同的。发动机上面的螺丝重用机会更大。很多不同种类发动机上使用的螺丝很可能是同一个厂家生产的相同螺丝。)
3. 可插入性:使用短函数,利用Override等操作,替换处理逻辑会更加容易。比如,更容易应用模板方法模式Template Method,参见Form Template Method章节。
2.2.2  “短函数”难道没有缺点?
1. 调用开销?(现代OO语言几乎已经完全免除了进程内的“函数调用动作的额外开销”。)
2. 看代码的时候,跳来跳去,很心烦?(高层函数看起来像系列注释,而且函数名字起得好,单纯看代码,比如接手模块的时候,低层的函数甚至可以不看。)
2.2.3 函数到了多长,Smell才算Bad?(写多长的函数才比较合适?)
1. 李维说:5行。(李维大家认识么?台湾IT界著名散文家,与侯捷齐名。不得不承认,此处我断章取义了,他说“5行”,是有一定语境的,他是想说“很短”的意思,建议大家不要追究他说的是5还是6。)
2. Martin Fowler说:长度不是问题,关键在于函数名称和函数本体之间的语义距离。(Martin Fowler大家认识么?《重构》《UML精粹》《分析模式》《企业应用架构模式》等经典著作的作者。后面,Martin还是,如果提炼动作可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓。什么叫做“函数名称与函数本体之间的语义距离”?函数名称要能概括函数体的动作,我引用的这句话没有能表达Martin的所有意思。)
3. 王振宇说:应该很短、可以较长,只要函数是在做一件从语义上无法再次拆解的事。(王振宇大家认识么?注意到没有,我们认为这句话是对Martin上一句话的补充,在函数体的组织方式上做了要求。简单说就是,一个函数做一件事儿。Extract Method中的那个例子。)
2.3 Large Class(过大类)
(一个例子)
1. “过大类”有什么不好?(难维护、容易出现重复代码。)
2. “过大类”的常见情况:
a) 本应该是针对不同类的操作放到一个类中。(比如,某些类本应拆出一些小的零件类,该类与零件类之间可能是关联、依赖关系,但没有这么做,而是所有代码放在一个类中。)
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 ("name:" + _name);
  System.out.println ("amount" + getOutstanding());
}
重构为:
void printOwing() {
  printBanner();
  printDetails(getOutstanding());
}
void printDetails (double outstanding) {
  System.out.println ("name:" + _name);
  System.out.println ("amount" + outstanding);
}
(前面说了,函数应该短。重复一下带来的好处:可读性好,高层函数像注释、低层函数行数少;可重用性好,比如上例中的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;
System.out.println (temp);
重构为:
final double perimeter = 2 * (_height + _width);
System.out.println (perimeter);
final double area = _height * _width;
System.out.println (area);
(如果临时变量被赋值超过一次就意味它们在函数中承担了一个以上的责任(循环变量等用途除外)。
例子中,临时变量temp开始被用来记录矩形周长,后来被用来记录矩形面积。应该拆解为多个临时变量,否则:
1. 影响代码可读性。(多用途临时变量通常无法获得合适的命名)
2. 增加代码出错机会。(程序某处,可能都记不清这个临时变量现在是记录什么数值的)
实际操作中,推荐使用final来限定临时变量被赋值次数。)
3.4 Remove Assignments to Parameters
int discount (int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) inputVal -= 2;
重构为:
int discount (int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (inputVal > 50) result -= 2;
1. 不要对参数赋值
void nextDate(Date arg) {
    arg.setDate(arg.getDate() + 1);
}
void nextDate(Date arg) {
    arg = new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}
上面两个函数的写法,哪个是对参数赋值了的?哪个会起到应有的作用?
2. Java是pass by value(传值)的
a) 传进函数体中的参数,是调用语句那个传入参数在内存中的一份拷贝
b) 函数体内对参数的再赋值不会影响调用方的参数原始值(比如,修改int等基本类型参数的数值、修改Object等对象引用的指向)
c) Java中,对参数的再次赋值是一种纯粹降低程序清晰程度的做法
3.5 Replace Method with Method Object
class Order...
      double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// long computation;
...
      }

重构为:

(一个大的函数,提炼出一个专门实现这个函数功能的类。
比如getMoney方法可能就是提炼出一个MoneyGetter类。此处是price方法提炼出PriceCalculator类。
这样做的理由是,price可能是个很大的函数,我们为了获得短函数的优势(前面说过的,可重用、可读、可插入等),想利用Extract Method抽取出多个短函数,但每个短函数可能都需要primaryBasePrice、secondaryBasePrice、tertiaryBasePrice等几个临时变量,把它们都作为参数传递显然太笨了。而把这个大函数提炼成类,这些临时变量就变成了类的Field,在类中是共享的,这样,抽取出来的小函数之间就可以不需要传递参数,就很容易实现Extract Method这个重构过程。)
3.5.1 如此这般之后,类是不是太多了?
面向对象的套路,玩的就是类。函数不嫌多,为什么嫌类多?多个风马牛不相及的函数杂居在一个类中(能够容忍么?),为什么不多弄几个类把它们各自封装?类,对应了现实世界的有机无机物种,把物种分得足够细致,世界才得到了完美地描述。
3.6 Replace Array with Object
String[] person = new String[3];
person[0] = “Robert De Niro";
person[1] = “60";
person[2] = “Actor”;
重构为:
Person robert = new Person();
robert.setName(" Robert De Niro");
robert.setAge(“60");
robert.setProfession(“Actor”);
1. 数组应该容纳一组相似的对象,用户很难记住“数组第一个元素是人名、第二个元素是年龄”这样的约定。
a) 用注释来保证这种约定么?
b) 使用数组是出于效率考虑么?(抬杠?)
2. 同理,能够恰当地利用既有数据结构把接口约束得紧一些(更贴切、更严丝合缝),对大家都有好处。
a) 比如,能够确定是一组Person类实例,就要用Person[]来装,而不用Set、Map这样的“广口”容器。(拒绝“私下、口头约定,注释”等靠不住的协议方式所带来的弊端。)
3.7 Encapsulate Field
public String _name;
重构为:
private String _name;
public String getName() {
    return _name;
}
public void setName(String arg) {
    _name = arg;
}
强调封装:
一般来讲,任何时候都不要将类field声明为public(常量除外)。数据和使用数据的行为被集中在一起,一旦情况发生变化,代码的修改比较容易,因为需要修改的代码都集中在同一块地方,而不是星罗棋布地散落在整个程序中。
3.8 Replace Magic Number with Symbolic Constant
double potentialEnergy(double mass, double height) {
return mass * 9.81 * height;
}
重构为:
double potentialEnergy(double mass, double height) {
return mass * GRAVITATIONAL_CONSTANT * height;
}
static final double GRAVITATIONAL_CONSTANT = 9.81;
宏值定义代替散落在代码中的“魔术数”,没什么好说的。
3.9 Encapsulate Collection

重构为:

依然是强调封装:
1. 类内高内聚:数据和对数据的操作紧密结合在一起,对数据的实施操作比较容易。(比如,餐厅管理系统中的某个类,dishes(Vector类型)作为类的field,装载点菜时被点中的菜目,如果想统计一下哪些菜受欢迎,对于按Encapsulate Collection设计的类就比较容易操作,add方法中做一些手脚即可(分类累加)。)
2. 类之间松耦合:内部数据结构不要暴露的外界,外界也不需要关心。(这样,即便你把内部数据由array换成Vector,外部都不需要知道。)
3.10 Replace Type Code with Class
重构为:
3.10.1 Why?Type Code有什么不好?
1. Type Code会降低可读性。
a) 在定义的地方可能看不出来(定义时使用宏值,可读性挺好),但在使用的地方就会显现问题。上例中,比如有个方法getCharacter获得血型对应的性格描述,参数是血型,使用Type Code时,参数类型为int,重构后,参数类型为BloodGroup,显然后者的可读性好。
2. 使用Type Code失去了使用对象所拥有的独立扩展的机会。
a) 像这个例子,Type Code很容易有它自己的行为,比如根据血型得到性格描述、得到ABO溶血症可能性、判断血型之间的输血匹配可能……于是将其抽取成类是比较好的做法。(还是封装!)
3.10.2 插科打诨,Meilir Page-Jones讲的故事
(故事是讲面向对象的,面向对象的主要特征有哪些?封装、继承、多态)
Meilir Page-Jones在《UML面向对象设计基础》(个人认为此书堪称经典)一书中编了一个故事:
软件界在“面向对象”的定义上,一度很难达成一致。我开始步入面向对象领域时,决定澄清一下“面向对象”的定义。
我把数十位面向对象的老前辈关在一个没有食物和水的房间里。我告诉他们只有当他们的定义达成一致的意见,并且可以在软件世界发布时才允许他们出去。在一小时的喧哗过后,房内一片安静,老前辈们背靠背谁也不理谁了,陷入了僵局。此时,蹦出来一位组织者,让每个人都列出他们认为在面向对象世界中不可缺少的特性,大家同意。一通罗列,每个人都列出了三个五个、十个八个。
此时,刚才蹦出来那位组织者又蹦出来开始讲话,说,现在我们大致有两种做法:一种是建立一个长列表,该列表是每个人列表的并集;另一种是建立一个短列表,该列表是每个人列表的交集。大家选择了后者,产生了一个短列表,该列表中的特性在每个人列表中都有。这个列表确实很短,短到只有一个词,“封装”。
一堆废话告诉大家一个道理,封装,是面向对象最为重要的特性,封装好了,才能做到所谓的高内聚、松耦合。获得面向对象思想许诺的种种优势。
3.11 Replace Type Code with Subclass
(跟前面宠物店的例子是不是很像?)
重构为:
3.12 Replace Type Code with State/Strategy
重构为:
这个就厉害了!清晰地展示了“合成/聚合复用原则”。
上面例子,将Engineer和Salesman弄成并列的子类,是存在问题的。(什么问题?)
1. Salesman明确地从Employee继承,那么就无法再从Male、Newcomer等类继承来获得他们的特性。
2. Salesman的实例被new出来之后,他可能转岗做研发,想变成Engineer,无法实现。
3.12.1 什么是合成/聚合复用原则
1. 要尽量使用合成/聚合,尽量不要使用继承。
2. 从复用角度来说:“合成/聚合复用”比“继承”复用灵活。前者是动态复用(因而具有可插入性)、后者是静态复用(编译时就固定了复用关系),而且后者的复用有“不支持多重继承”的限制。
3.13 Decompose Conditional
if (date.before (SUMMER_START) || date.after(SUMMER_END))
    charge = quantity * _winterRate + _winterServiceCharge;
else charge = quantity * _summerRate;
重构为:
if (notSummer(date))
    charge = winterCharge(quantity);
else charge = summerCharge (quantity);
Extract Method在条件判断语句段中的应用。
3.14 Consolidate Conditional Expression
double disabilityAmount() {
if (_seniority < 2) return 0;
if (_monthsDisabled > 12) return 0;
if (_isPartTime) return 0;
// compute the disability amount
重构为:
double disabilityAmount() {
if (isNotEligableForDisability()) return 0;
// compute the disability amount
这条比较雕虫小技,可视具体情况参考实施。
3.15 Consolidate Duplicate Conditional Fragments
if (isSpecialDeal()) {
  total = price * 0.95;
  send();
}
else {
  total = price * 0.98;
  send();
}
重构为:
if (isSpecialDeal()) {
  total = price * 0.95;
}
else {
  total = price * 0.98;
}
send();
虽然这条也比较雕虫小技,但前面这样的代码确实也有人写得出来。
3.16 Remove Control Flag
set done to false
while not done
    if (condition)
        do something
        set done to true
    next step of loop
Control Flag为什么不好?
影响可读性,程序看起来比较绕。
3.16.1 一个例子
void checkSecurity(String[] people) {
  String found = "";
  for (int i = 0; i < people.length; i++) {
    if (found.equals("")) {
      if (people[i].equals ("Don")){
        sendAlert();
        found = "Don";
      }
      if (people[i].equals ("John")){
        sendAlert(); 
        found = "John";
      }
    }
  }
  someLaterCode(found);
}
无论找到Don还是John都退出循环做其他事儿。注意:这里使用了标志found。
重构为:
void checkSecurity(String[] people) {
  String found = foundMiscreant(people);
  someLaterCode(found);
}
String foundMiscreant(String[] people){
  for (int i = 0; i < people.length; i++) {
    if (people[i].equals ("Don")){
      sendAlert();
      return "Don";
    }
    if (people[i].equals ("John")){
      sendAlert();
      return "John";
    }
  }
  return "";
}
重构之后,去掉了标志,增加了函数的出口,同时增加了程序的可读性。
3.17 Replace Nested Conditional with Guard Clauses
(卫语句:某些条件判断为真时,立即从函数返回。这样的判断就应该首先、单独进行,把这种单独检查称之为“卫语句”。Guard Clauses。是Kent Beck给起的名字,Kent Beck是TDD、XP的第一倡导者。)
double getPayAmount() {
  double result;
  if (_isDead) result = deadAmount(); 
  else {
    if (_isSeparated) result = separatedAmount();
    else {
      if (_isRetired) result = retiredAmount();
      else result = normalPayAmount();
    };
  }
  return result;
};
重构为:
double getPayAmount() {
  if (_isDead) return deadAmount();
  if (_isSeparated) return separatedAmount();
  if (_isRetired) return retiredAmount();
  return normalPayAmount();
};
(好处是明显的,可以减少if/else嵌套的数目,从而强烈地提高程序可读性。比较重要的是,需要习惯“函数有多个出口”这种做法。)
3.18 Replace Conditional with Polymorphism
double getSpeed() {
    switch (_type) {
        case EUROPEAN:
            return getBaseSpeed();
        case AFRICAN:
            return getBaseSpeed() - getLoadFactor() *
                _numberOfCoconuts;
        case NORWEGIAN_BLUE:
            return (_isNailed) ? 0 : getBaseSpeed(_voltage);
    }
    throw new RuntimeException ("Should be unreachable");
}
重构为:

(跟讲Switch那个Bad Smell时举过的例子,基本一样。)
3.19 Introduce Parameter Object
重构为:
3.20 Replace Error Code with Exception
int withdraw(int amount) {
    if (amount > _balance) {
        return -1;
    else {
        _balance -= amount;
        return 0;
    }
}
重构为:
void withdraw(int amount) throws BalanceException {
    if (amount > _balance) throw new BalanceException();
    _balance -= amount;
}
Why?
1. 提高代码可读性。
2. 方便调用方。调用方可以不再判断返回的Error Code,而只是把异常直接抛出去,待最终接受方处理。比如,类C/S的结构,服务端代码在所有环节都可以直接透传Exception,简化处理流程。Exception最终由客户端统一处理。
(注:有些地方无法完全取代Error Code,比如前台回来的消息处理,onMessage函数。)
3.21 Form Template Method

重构为:

(计算金额的步骤,各个子类都一样,即算得基础金额、再算得缴税几多,然后二者想加。于是把这个逻辑上升到父类。子类仅负责其中的子步骤。)
(下面再看一个例子。说明一下Template Method是怎么回事儿。
所有的工作人员,一天的活动,步骤都差不多,无外乎,吃早餐、赶到工作地点……,但不同子类对于各个步骤的实现是不同的。
比如,上午工作,公务员可能是:编写官样文章、聊天、按照规定合理地拒绝刁蛮市民的无理要求……;我司员工可能是:编码、调试、上网看新闻、到匿名论坛发牢骚……;农民工兄弟可能是:扛包、抽烟休息一会、扛包、喝水休息一会……。
比如,吃午餐,公务员是:免费、或者象征性收费的豪华自助餐;我司员工是:别无选择的XLX;农民工兄弟是:其他农民工在廉价出租棚子里制作的不干不净吃了可能得病的方便盒饭。)
分享到:
评论

相关推荐

    不良气味检测和解决时间表:节省精力的新方法

    难闻的气味是代码中潜在问题的迹象。 尽管提出了关于异味检测和重构工具的建议,但是检测和解决异味仍然对软件工程师来说很耗时。 已经识别出许多难闻的气味,但是很少讨论执行检测和解决各种异味的顺序,因为软件...

    机器人路径规划中A*与DWA算法融合的Python实现及应用

    内容概要:本文详细介绍了A*搜索算法和DWA(动态窗口法)算法的基本原理及其在机器人路径规划中的融合应用。A*算法用于全局路径规划,通过启发式搜索找到从起点到目标点的最短路径;DWA算法则专注于局部动态环境下的实时避障,确保机器人在移动过程中能够灵活避开障碍物。两者结合可以在复杂环境中提供高效的路径规划解决方案。文中提供了详细的Python代码实现,帮助读者理解和实践这两种算法的融合方法。 适合人群:对机器人路径规划感兴趣的初学者和技术爱好者,尤其是有一定编程基础的小白。 使用场景及目标:适用于需要在静态和动态环境中进行路径规划的机器人项目。主要目标是使机器人能够在复杂环境中安全、高效地到达目标位置,同时避免障碍物。具体应用场景包括但不限于室内导航、无人驾驶车辆等。 其他说明:文章不仅讲解了理论知识,还给出了具体的代码实现和调试技巧,如启发函数的选择、速度空间采样的优化、评分权重的调整等。此外,还提到了一些常见问题及解决方法,如机器人在遇到突发情况时的行为调整。

    ELk整体介绍整理PPT涵盖了ELk 相干的架构整理

    ELK日志架构部署与应用指南:构建高效、可靠的日志管理与分析系统 本指南全面介绍ELK(Elasticsearch、Logstash、Kibana)日志架构的部署实践与优化策略,旨在帮助技术团队构建可扩展、高容错的日志处理平台。内容涵盖ELK核心组件(Filebeat、Logstash、Kafka、Elasticsearch、Kibana)的工作原理、架构设计、配置案例及最佳实践,重点解析以下关键内容: ● 架构选型与演进:从基础架构到分布式集群,对比单节点、轻量级(Filebeat+Logstash)及引入Kafka的高可用架构优劣,指导根据实际场景选择合适的部署模型。 ● 组件深度解析:详细讲解Filebeat轻量级采集、Logstash灵活处理、Kafka消息缓冲、Elasticsearch分布式索引及Kibana可视化分析的实现机制与性能调优。 ● 日志规范与实战:明确日志打印格式标准(时间、级别、组件、用户信息等),结合配置示例(如Nginx日志采集到Kafka的动态Topic生成)演示数据流转全过程。 ● 运维与扩展:涵盖集群高可用设计、负载均衡策略、日志实时监控及异常报警机制,为系统稳定性与后续扩展提供完整解决方案。 本指南适合日志管理需求的中大型企业技术团队,可作为从架构设计到落地实施的实操手册,助力提升日志分析效率与系统运维能力。

    T型三电平逆变器VSG控制与LCL滤波器的双闭环设计及优化

    内容概要:本文详细介绍了基于T型三电平逆变器的虚拟同步机(VSG)控制技术,涵盖VSG的核心算法、中点电位平衡策略以及LCL滤波器的双闭环控制设计。首先探讨了VSG控制的基本原理,包括虚拟惯量和阻尼特性的模拟,以及有功-频率和无功-电压下垂控制的具体实现。针对T型三电平拓扑特有的中点电位漂移问题,提出了多种平衡控制方法。对于LCL滤波器,讨论了其参数设计和双闭环控制策略,特别是电流环PI参数的选择和避免谐振的方法。文中还提供了多个实用的经验公式和调试技巧,并引用了相关领域的权威文献作为理论支持。 适合人群:从事电力电子、新能源并网系统研究和开发的技术人员,尤其是有一定电力电子基础的研发人员。 使用场景及目标:适用于需要深入了解和掌握VSG控制技术和LCL滤波器设计的研究人员和技术开发者。主要目标是帮助读者理解和实现T型三电平逆变器的VSG控制,提高系统的稳定性和性能。 其他说明:文中不仅提供了详细的理论解释,还有具体的代码实现和调试建议,便于读者进行实际操作和验证。同时强调了调试过程中需要注意的安全事项和常见问题的解决方案。

    Go语言Go语言简介:特性、应用场景与学习资源汇总:服务器端开发、云计算、微服务架构的最佳选择

    内容概要:本文介绍了Go语言(又称Golang)的特点、应用场景和发展背景。Go语言由Google团队于2007年设计,2009年发布,以其简洁的语法、高效的编译性能和强大的并发支持著称。它摒弃了复杂的面向对象特性,采用接口和组合的方式实现代码复用。作为编译型语言,Go语言通过严格的类型检查确保代码质量,并内置了高效的垃圾回收机制。Go语言广泛应用于服务器端开发、云计算和微服务架构等领域,如Google、Docker和Uber等公司均采用Go语言构建后端服务。此外,Go语言拥有丰富的学习资源,包括官方文档、社区支持以及权威书籍和在线课程。; 适合人群:对编程有一定了解,特别是对高性能后端开发感兴趣的开发者。; 使用场景及目标:①希望掌握一门适用于服务器端开发、云计算和微服务架构的编程语言;②想深入了解并发编程和支持高并发场景的开发技术。; 其他说明:学习Go语言不仅可以提升编程技能,还能借助其活跃的社区和丰富的学习资源,快速适应现代互联网应用开发的需求。

    Ollama0.6.2安装文件-2

    Ollama0.6.2安装文件-2

    【Java并发编程】线程同步工具与锁机制详解:CountDownLatch、CyclicBarrier、Semaphore的应用场景及实现方式并发编程中的多个

    内容概要:本文详细介绍了Java并发编程中的多个重要概念和技术,包括CountDownLatch、CyclicBarrier、Semaphore等同步工具类,深入探讨了线程的创建方式、运行状态及线程安全问题。文章还讲解了原子性、可见性、有序性三大特性及其解决方案,如锁机制和原子类的应用。此外,文中对比了悲观锁和乐观锁的特点与适用场景,并阐述了线程池的核心参数配置。最后,简要介绍了Spring框架的核心技术,如IOC、AOP及事务管理的基本概念。 适合人群:具备一定Java编程基础,尤其是对并发编程和Spring框架有一定了解的研发人员。 使用场景及目标:①帮助开发者理解Java并发编程中的同步工具类、线程管理及线程安全问题;②指导开发者在实际项目中选择合适的锁机制和线程池配置;③加深对Spring框架中IOC、AOP及事务管理的理解,提升开发效率。 其他说明:本文不仅提供了理论知识,还结合了代码示例,便于读者理解和实践。建议读者在学习过程中结合实际项目需求进行调试和验证,以更好地掌握相关技术。

    【系统编程语言】Rust语言特性与应用:现代高效安全的系统级开发工具介绍

    内容概要:Rust是一种由Mozilla研究院开发的现代系统编程语言,专注于安全、并发和性能。其设计哲学是“零成本抽象”,即在不影响性能的前提下提供高级抽象。Rust通过所有权系统确保内存安全,避免了垃圾回收的需求,有效防止了悬垂指针、数据竞争和无效内存访问等问题。它提供了强大的并发原语,如线程、通道和原子操作,使并发编程更加简单和安全。此外,Rust编译为机器码,性能接近C/C++,并且支持多种操作系统和架构,包括Windows、Linux、macOS以及嵌入式系统。Rust还拥有活跃的社区和丰富的生态系统,提供了大量涵盖多个领域的库和工具。Rust适用于高性能和安全性的系统级应用,如操作系统、数据库、网络服务器和嵌入式系统,同时也被广泛应用于WebAssembly、区块链和人工智能等领域。; 适合人群:对系统编程感兴趣,尤其是希望在保证性能的同时提升代码安全性的开发者。; 使用场景及目标:①需要开发高性能、安全的系统级应用,如操作系统、数据库、网络服务器等;②希望深入理解内存管理和并发编程的最佳实践;③探索WebAssembly、区块链和人工智能等新兴技术领域。; 阅读建议:Rust不仅适合有经验的系统程序员,也适合希望通过学习现代编程语言提升技能的新手。官方教程《The Rust Programming Language》、实践指南《Rust by Example》和练习项目《Rustlings》都是很好的学习资源,建议结合这些资源进行实践和调试。

    基于一致性算法的直流微电网均流均压二级控制方案及其应用

    内容概要:本文介绍了一种基于一致性算法的直流微电网均流均压二级控制方案。该方案采用分布式二级控制器,通过邻居间的通信来计算控制动作,从而实现高效稳定的均流和均压控制。文中详细讨论了恒功率负载平衡点的存在条件,并介绍了即插即用特性的实现。此外,通过MATLAB/Simulink仿真验证了该方案的电压稳定性和鲁棒性。 适合人群:从事电力电子、微电网控制领域的研究人员和技术人员,尤其是对分布式控制系统感兴趣的读者。 使用场景及目标:适用于直流微电网的设计与优化,旨在提高系统的稳定性和效率,特别是在应对非线性负载和突发扰动的情况下。 其他说明:该方案在理论和实践中展现了显著的优势,如更高的均流精度和更快的动态响应时间。然而,仿真运行时间较长,需考虑计算资源的分配。

    电路仿真:基本电路元件仿真.zip

    电子仿真教程,从基础到精通,每个压缩包15篇教程,每篇教程5000字以上。

    前端开发基于200道高频面试题的JavaScript核心技术解析:互联网大厂面试指南了互联网大厂

    内容概要:本文档《互联网大厂200道高频Javascript面试题.pdf》涵盖了广泛的JavaScript知识点,针对200道高频面试题进行了详细解析。内容涉及JavaScript的核心概念(如闭包、原型链、事件循环)、异步编程(如Promise、async/await)、数据类型(如BigInt、Symbol)、DOM操作(如EventTarget、MutationObserver)、性能优化(如防抖、节流)、以及最新的ES提案特性(如import.meta、top-level await)。每个问题不仅提供了简明的答案,还深入探讨了背后的原理和应用场景,帮助读者全面掌握JavaScript的各种特性和最佳实践。 适合人群:具备一定编程基础,特别是对JavaScript有初步了解的前端开发人员,以及准备面试互联网大厂的求职者。 使用场景及目标:①巩固JavaScript基础知识,理解语言内部机制;②掌握常见面试题及其解答技巧,为技术面试做准备;③通过实际案例和代码示例加深对JavaScript特性的理解,提高编程能力。 阅读建议:此资源内容详实,建议读者根据自身水平选择重点章节进行学习,同时结合实际项目或练习题进行实践,以达到更好的学习效果。对于复杂的概念,可以多次阅读并查阅相关资料,确保彻底理解。

    【C#编程语言】从入门到精通:C#核心概念、开发工具与实战项目解析

    内容概要:本文详细介绍了C#编程语言的基础知识和学习路径。首先阐述了C#的背景和优势,强调其广泛的应用领域和良好的就业前景。接着指导读者如何准备学习环境,包括安装Visual Studio等开发工具。文中深入讲解了C#的基础语法,如数据类型、运算符、流程控制语句等,并探讨了面向对象编程的核心概念,包括类与对象、封装、继承、多态等。此外,还介绍了异常处理机制,并通过一个控制台版的学生管理系统项目,帮助读者将理论应用于实践。最后,推荐了一系列学习资源,鼓励读者持续学习和探索C#的高级特性。 适合人群:对编程有兴趣的初学者,尤其是希望从事软件开发或提升编程技能的人士。 使用场景及目标:①帮助读者从零开始系统学习C#,掌握基本语法和核心概念;②通过实战项目巩固所学知识,培养解决实际问题的能力;③为后续深入学习C#的高级特性和应用领域打下坚实基础。 其他说明:本文不仅提供了详尽的知识点讲解,还注重实践操作和项目经验积累,适合自学或作为培训教材使用。建议读者跟随文章步骤逐步实践,并利用推荐的学习资源深化理解。

    油气开采中液氮压裂的COMSOL热-流-固-损伤耦合模型研究

    内容概要:本文详细介绍了使用COMSOL软件建立液氮压裂的热-流-固-损伤耦合模型的方法和技术细节。液氮压裂作为一种环保高效的油气开采方法,能够利用低温特性减少地层污染并提高裂缝扩展效率。文中探讨了模型的关键组件,如传热、达西流、固体力学以及自定义的损伤演化方程,并展示了如何将这些元素整合在一个统一的多物理场环境中进行仿真。此外,还讨论了网格划分、求解器设置、边界条件处理等方面的具体实现方法,以及如何通过后处理手段来分析和展示仿真结果。 适合人群:从事油气田开发、地质工程、计算力学等相关领域的科研人员和技术专家。 使用场景及目标:适用于需要深入理解液氮压裂过程中复杂的物理机制的研究项目,旨在为优化压裂工艺提供理论支持和技术指导。 其他说明:文中提供了大量具体的数学公式和代码片段,帮助读者更好地理解和重现所描述的技术流程。同时强调了不同参数选择对于最终仿真结果的影响,提醒使用者注意实际应用中的各种挑战。

    ### 【企业AI应用】从DeepSeek到Manus:AI重塑企业价值与应用实践(2025年华中科技大学研究报告)

    内容概要:本文探讨了AI如何重塑企业价值,特别是通过DeepSeek和Manus这两个技术的推动。DeepSeek以其低成本、高质量的特性,打破了AI应用的技术和成本壁垒,实现了AI赋能平权。Manus作为通用智能体,通过全链路自主执行和多智能体协同架构,显著提升了企业效率并降低了成本。文章详细介绍了AI在多个行业(如金融分析、人力资源、零售运营等)的具体应用案例,展示了AI如何通过数据驱动、自动化和智能化手段,帮助企业实现降本增效、商业模式创新和产品迭代。此外,文中还提出了企业在拥抱AI过程中面临的挑战和应对策略,强调了数据基础、技术选择和组织能力建设的重要性。 适合人群:企业高管、AI技术从业者、数字化转型顾问及对AI感兴趣的创业者。 使用场景及目标:①了解AI技术在企业中的应用场景及其带来的变革;②为企业制定AI战略和实施路径提供参考;③帮助企业识别潜在的AI赋能机会,优化业务流程,提升竞争力。 其他说明:文章通过大量实际案例和数据支持,强调了AI技术在企业中的巨大潜力和现实可行性。同时,提醒企业在拥抱AI时需谨慎规划,避免盲目跟风,确保AI项目的成功落地。

    c语言学习资料,共208页

    c语言学习资料,共208页

    无人车路径规划中基于人工势场算法的MATLAB实现及优化

    内容概要:本文详细探讨了基于人工势场的无人车避障路径规划算法。主要内容包括三个主要势场的建立及其MATLAB代码实现:引力势场用于吸引无人车向目标点移动;障碍车斥力势场用于使无人车避开障碍物;道路边界势场确保无人车保持在车道内行驶。此外,文中还介绍了如何通过调整不同势场的增益系数、引入朝向因子和动量项等手段优化路径规划性能,提高路径平滑性和安全性。最终形成了完整的路径规划流程,即计算合力、确定方向并迭代推进,使得无人车能够在复杂环境中顺利导航。 适合人群:对无人驾驶技术和路径规划算法感兴趣的科研人员、工程师及高校相关专业学生。 使用场景及目标:适用于模拟和实际测试无人车在静态或动态环境中的避障能力和路径选择行为,旨在提高无人车的安全性和智能水平。 其他说明:文中提供的MATLAB代码仅为简化版本,实际应用中可根据具体需求进一步调整和完善。

    电力系统中分布式电源接入对电网电压影响的潮流计算研究

    内容概要:本文详细探讨了分布式电源(如光伏、风能、燃料电池等)接入电网时对电压产生的影响。通过具体的潮流计算模型,展示了不同类型的分布式电源(DG)在接入电网时所采用的不同节点类型(PQ节点、PV节点等),并分析了它们对电压稳定性的影响。文中还提供了基于Python的Pandapower库构建的测试电网实例,以及MATLAB中的节点类型动态切换逻辑,进一步解释了不同节点类型在实际应用中的表现及其优缺点。 适合人群:从事电力系统研究、分布式能源规划的技术人员和研究人员。 使用场景及目标:帮助技术人员理解分布式电源接入电网时的电压波动机制,优化分布式电源的接入方式,提高电网电压稳定性和可靠性。 其他说明:文章强调了在进行电网规划时,应根据实际情况选择合适的节点类型,避免盲目依赖理想化模型。同时,提出了混合模式作为一种有效的解决方案,能够在不同工况下保持较好的电压质量。

    三星轮爬楼车sw18可编辑_三维3D设计图纸_包括零件图_机械3D图可修改打包下载_三维3D设计图纸_包括零件图_机械3D图可修改打包下载.zip

    三星轮爬楼车sw18可编辑_三维3D设计图纸_包括零件图_机械3D图可修改打包下载_三维3D设计图纸_包括零件图_机械3D图可修改打包下载.zip

    电子封装材料仿真:复合材料仿真.zip

    电子仿真教程,从基础到精通,每个压缩包15篇教程,每篇教程5000字以上。

Global site tag (gtag.js) - Google Analytics