Duplicated Code(重复代码)
如果你在一个以上的地点看到相同的程序结构,那么可以肯定:设法将它们合而为一,程序会变得更好。
-
场景1:
同一个类中,两个函数含有相同的表达式-->将相同的表达式抽出一个方法
-
场景2:
两个具有相同父类的子类内含有相同的表达式-->将相同的表达式,放入父类中。
如果两个子类中,有类似的表达式,那么将相同的部分放入父类中,将差异的部分由子类实现。这也是一种模板方法的设计模式。
-
场景3:
如果两个毫无相关的类出现重复代码,可以考虑将重复代码提炼到一个独立的类中。通过组合的方式交给使用方。
Long Method(过长函数)
程序越长越难理解。
你应该更积极地分解函数。我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途命名。
关键点不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。
Large Class(过大的类)
如果用单个类做太多事情,其内往往就会出现太多实例变量,一旦如此,Duplicated Code也就接踵而至了。
Long Parameter List(过长参数列)
"把函数所需的所有东西都以参数传递进去,否则你就只能选择全局数据"。
全局数据是邪恶的东西,它可能会引导你的程序走向错误。
"如果你手上没有所需的东西,可以叫另一个对象给你"。
通过对象传递参数,要比太长的参数列要好理解;太多参数会造成前后不一致、不易使用,而且一旦你需要更多的数据,就不得不修改它。
Divergent Change(发散式变化)
我们希望软件能够更容易被修改。
一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。
如果不能做到这一点,你就嗅出两种紧密相关的刺鼻味道中的一种了。
如果某个类经常因为不同的原因在不同的方向上发生变化,Divergent Change就出现了。
针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该反应此变化。
public class Person{
public void say(SayThing t,String message){
if(t.getType()==null){
System.out.println(message);
}else if(t.getType=='喇叭'){
System.out.println("大声喊:"+message);
}
}
public void do(DoThing t){
if(t.getType()==null){
System.out.println("do sth");
}else if(t.getType().equals("hit")){
System.out.println("hit sth");
}
}
}
针对Person类,如果新增加SayThing或者DoThing都要修改Person类,这种情况就是属于Divergent Change,同一个类受多种变化影响。
如何更改?可以将say以及do方法内的部分,分别抽取出一个类,做到当新增某一个类型的时候,只需要处理一个地方即可。
Shotgun Surgery(散弹式修改)
如果遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是Shotgun Surgery。如果需要修改的代码散布四处,你不但很难找到它们,也很容易忘记某个重要的修改。
Divergent Change是指“一个类受多种变化的影响”,Shotgun Surgery则是指“一种变化引发多个类响应修改”。
例子:
public class A{
@Value("${system.test}")
private String systemTest;
}
public class B{
@Value("${system.test}")
private String systemTest;
}
如例子所示,如A和B都使用了配置文件中的属性system.test
,假如后面配置文件变了,这个system.test的名称改了,改成了systen.name,那么A和B都要修改这个配置信息。
可以通过GoubleConfig类管理通用的配置信息。
Feature Envy(依恋情节)
面向对象技术的全部要点在于:这是一种“将数据和对数据的操作行为包装在一起“的技术。
函数对某个类的兴趣高过对自己所处类的兴趣。
影响:数据和行为不在一处,修改不可控。
public class UserInfoService{
@Autowired
private IUserUnitRelationService userUnitRelationService;
public void addUserRelation(List<Integer> unitIds,Integer userId){
//1. 删除原有的关联关系
userUnitRelationService.deleteRelationByIds(userId);
//2. 新增新的关联关系
userUnitRelationService.addRealtions(unitIds,userId);
//3. 更新用户状态
this.updateUserStatus();
}
}
上面的列子就是一个种不好的方法,因为在UserInfoService中关注了太多UserUnitRelationService的东西了。
更好的方式应该是,将步骤1和步骤2合并,放到UserUnitRelationService中。
public class UserInfoService{
@Autowired
private IUserUnitRelationService userUnitRelationService;
public void addUserRelation(List<Integer> unitIds,Integer userId){
//1. 处理用户关系
userUnitRelationService.addNewRelations(unitIds,userId);
//2. 更新用户状态
this.updateUserStatus();
}
}
Data Clumps(数据泥团)
多个类/方法参数中都有相同的属性,且这些相同的属性的业务意义也是相同的。
一个大对象,装载太多的属性字段。
Primitive Obsession(基本类型偏执)
大多数编程环境都有两种数据:结构类型允许你将数据组织成有意义的形式;基本类型则是构成结构类型的积木块。
对象的一个极大的价值在于:它们模糊(甚至打破)了横亘于基本数据和体积较大的类之间的界限。
如果你有一组应该总是被放在一起的字段,应该将这些字段抽出来一个类。
比如,钱。很多情况下我们都直接使用基本类型来表示钱,比如:Integer money;
但实际上,钱还有其他的属性在。比如:是美元 、日元、人民币等。
所以我们定义钱,可以这样定义:
public class Money{
private Integer money;
private String type;
}
Switch Statements(switch惊悚现身)
面向对象程序的一个最明显特征就是:少用switch(或case)语句。
从本质上说,swtich语句的问题在于重复。你经常会发现同样的switch语句散步于不同地点。如果要为它添加一个新的case语句,就必须找到所有switch语句并修改它们。
面向对象中的多态概念可为此带来优雅的解决办法。
如果你只是在单一函数中使用switch,且不想改动它们,那么多态就有点杀鸡用牛刀了。
Parallel Inheritance Hierarchies(平行继承体系)
Parallel Inheritance Hierarchies其实是Shotgun Surgery的特殊情况。
每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。
如果你发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同,便是闻到了这种坏味道。
如上图所示,如果再出现一个叫MiddleXXXService的类,那么AppService和OrangeService都要有这样一个子类。
Lazy Class(冗赘类)
你所创建的每一个类,都得有人去理解它、维护它,这些工作都是要花钱的。
如果一个类的所得不值其身价,它就应该消失。
项目中经常会出现这样的情况:某个类原本对得起自己的身价,但重构使它身形缩水,不再做那么多工作;或开发者事前规划了某些变化,并添加一个类来应付这些变化,但这些变化实际上没有发生(超前设计??)。不论什么情况,如果这个类没有意义,就让他“去死”吧。
Speculative Generality(夸夸其谈未来性)
我想我们总有一天需要做这事,并因此企图以各式各样的钩子和特殊情况来处理一些非要的事情,这种坏味道就出现了。
这么做的结果往往造成系统更难理解和维护。
如果所有装置都会被用到,那就值得那么做;如果用不到,就不值得。用不上的装置只会挡你的路,所有,把它搬开吧。
Temporary Field(令人迷惑的暂时字段)
有时你会看到这样的对象:其内某个实例变量仅为某种特定情况而设。这样的代码让人不易理解。
在变量未被使用的情况下猜测当初其设置目的,会令人苦恼的。
如果类中有一个复杂算法,需要好几个变量,往往就可能导致坏味道Temporary Filed的出现。由于提供者不希望传递一长串参数,所以他把这些参数都放进字段中,但是这些字段只在使用该算法时才有效,其他情况下只会让人迷惑。这时候你可以把这些变量抽到一个新的类中。
Message Chains(过度耦合的消息链)
示例:
a.getB().getC().getD().doSth();
采用这种方式,意味着代码与查找过程中的结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。
Middle Man(中间人)
对象的基本特征之一就是封装--对外部世界隐藏其内部细节。
封装往往伴随着委托。
比如你询问你的主管是否有时间参加一个会议。他就把这个消息“委托”给他的记事本,然后才能回答你。但是对于你来说,你没必要知道主管到底是使用什么样的记事本。
//“我”类
public Class Me(){
//"我"有一个功能,叫询问会议
public boolean askMetting(Manager manager,Date date){
//"询问"你是否有时间?
if(manager.haveTime(date)){
return true;
}
return false;
}
}
在上面的列子中,询问领导是否有时间,对于“我”来说,我是不关注他从哪里知道自己的日程的,他只需要给我一个答案。
反例
//“我”类
public Class Me(){
//"我"有一个功能,叫询问会议
public boolean askMetting(Manager manager,Date date){
NoteBook noteBook=manager.getNoteBook();
List<Date> dates=noteBook.getSchedule();
for(Date exsists: dates){
if(exsists==date){
return false;
}
}
return true;
}
}
上面这种情况就属于过度运用委托。
Inappropriate Intimacy(狎昵关系--不适当的亲密)
有时你会看到两个类过于亲密,花费太多时间去探究彼此的private成分。
过分亲密的类必须拆散。
Alternative Classes with Different Interfaces(异曲同工的类)
两个类做的同一件事或者同一类事。
为什么是一种坏味道?
- 会有选择上的疑虑,不知道两个类应该调用哪个,而疑虑之下就是时间的浪费。
- 修改代码的时候,只向其中一个类添加了逻辑,后续调用时,就会困扰调用者,而且容易导致两个类容易出现重复代码。
Incomplete Library Class(不完美的库类)
复用常被视为对象的终极目的。
但目前很多时候复用的意义经常被高估--大多数对象只要够用就好。
Data Class(纯稚的数据类)
所谓Data Class是指:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。
Data Class就像小孩子。作为一个起点很好,但若要让它们像成熟的对象那样参与整个系统的工作,它们就必须要承担一定责任。
Refused Bequest(被拒绝的遗赠)
子类应该继承超类的函数和数据。
但如果它们不想或不需要继承,又该怎么办呢?按传统说法,这就意味着继承体系设计错误。
有时候你需要的可能不是继承,而是组合。
Comments(过多的注释)
注释不是坏味道,事实上它们还是一种香味。
但人们经常把它当作除臭剂来使用,常常会有这样的情况:你看到一段代码有着长长的注释,然后发现,这些注释之所以存在是因为代码很糟糕。
Comments可以带我们找到之前提过的坏味道,找到坏味道后,我们首先应该以各种重构手法把坏味道去除。完成之后我们常常会发现:注释已经变得多余了,因为代码已经说明了一切。
如果你需要注释来解释一块代码做了什么,那么就把它抽取出一个方法把。
当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。