软⼯实验:常见的代码坏味道以及重构举例
⼀、常见的代码坏味道及其重构⽅法
1.神秘命名(Mysterious Name)
整洁代码最重要的⼀环就是好的名字,好的名字要让⼈见名知意,所以我们要深思熟虑如何给函数、模块、变量和类命名,使它们能清晰地表明⾃⼰的功能和⽤法。
因此,改名可能是最常⽤的重构⼿法,包括改变函数声明(⽤于给函数改名)、变量改名、字段 改名等。
2. 重复的代码(Duplicated Code)
重复的代码的三种类型以及重构⽅法:
于同⼀个类:提炼成新⽅法进⾏调⽤
位于 不同的⼦类:提炼成⽅法放进⽗类
位于完全不相⼲的类:提炼出⼀个新的类,将重复代码放进新的类中
3.过长函数(Long Method)
与过⼤的类的原因类似,过长的函数违反了单⼀职责原则,每个⽅法应该精简地完成某⼀项任务。
因此,最常见的重构⽅法是到函数中适合集中在⼀起的部分,将它们提炼出来形成⼀个新函数。⽐如现有⽅法中有⽤到 if() 或 for() 语句来验证⽤户输⼊或检查⽤户是否已登录的情况时,如果⼀定要做这些验证,应该创建⾃⼰的⽅法,由该⽅法去调⽤验证⽅法。
4.过长参数列(Long Parameter List)
最开始的时候我们通常会把函数所需的所有东西都以参数的形式传递进去。因为除此之外就只能选择全局数据,⽽全局数据很快就会变成邪恶的东西。但过长的参数列表本⾝也经常令⼈迷惑。如果可以向某个参数发起查询⽽获得另⼀个参数的值,那么就可以使⽤以查询取代参数(324)去掉这第⼆个参数。如果你发现⾃⼰正在从现有的数据结构中抽出很多数据项,就可以考虑使⽤保持对象完整(319)⼿法,直接传⼊原来的数据结构。如果有⼏项参数总是同时出现,可以⽤引⼊参数对象(140)将其合并成⼀个对象。如果某个参数被⽤作区分函数⾏为的标记(flag),可以使⽤移除标记参数(314)。
使⽤类可以有效地缩短参数列表。如果多个函数有同样的⼏个参数,引⼊⼀个类就尤为有意义。你可以使⽤函数组合成类(144),将这些共同的参数变成这个类的字段。如果戴上函数式编程的帽⼦,我们会说,这个重构过程创造了⼀组部分应⽤函数(partially applied function。
5.痴迷基本类型(Primitive Obsession)
当应⽤程序的所有地⽅都使⽤基本数据类型时,就会出现这种代码坏味道,重构⽅法是使⽤对象把基本类型封装起来。
6.依恋情结(Feature Envy)
当⼀个类的函数为了计算经常调⽤另⼀个类的⼀⼤堆的函数时就表⽰出现了依恋情节。将此部分出现依恋情节的代码提炼成函数放到另⼀个类⾥⾯
7.数据泥团(Data Clumps)
数据泥团指的是经常⼀起出现的数据,两个类中相同的字段、许多函数签名中相同的参数,这些总是绑在⼀起出现的数据应该拥有属于它们⾃⼰的类。⽐如⽰例⼆中每个⽅法的参数⼏乎相同,处理⽅式与过长参数列的处理⽅式相同,⽤Introduce Parameter Object(引⼊参数对象)将参数封装成对象。
8.发散式变化(Divergent Change)
如果某个类经常因为不同的原因在不同的⽅向上发⽣变化,发散式变化就出现了。
⽐如,如果新加⼊⼀个数据库必须修改这3个函数;新出现⼀种⾦融⼯具(举例说明),我必须修改这4个函数。这就是发散式变化的征兆。数据库交互和⾦融逻辑处理是两个不同的上下⽂,将它们分别搬移到各⾃独⽴的模块中,能让程序变得更好:每当要对某个上下⽂做修改时,我们只需要理解这个上下⽂,⽽不必操⼼另⼀个。
如果发⽣变化的两个⽅向⾃然地形成了先后次序(⽐如说,先从数据库取出数据,再对其进⾏⾦融逻辑处理),就可以⽤拆分阶段(154)将两者分开,两者之间通过⼀个清晰的数据结构进⾏沟通。如果两个⽅向之间有更多的来回调⽤,就应该先创建适当的模块,然后⽤搬移函数(198)把处理逻辑分开。如果函数内部混合了两类处理逻辑,应该先⽤提炼函数(106)将其分开,然后再做搬移。如果模块是以类的形式定义的,就可以⽤提炼类(182)来做拆分。
9.霰弹式修改(Shotgun Surgery)
霰弹式修改类似于发散式变化,但⼜恰恰相反。如果每遇到某种变化,你都必须在许多不同的类内做出许多⼩修改,你所⾯临的坏味道就是霰弹式修改。这种坏味道可以⽤Move Method(搬移函数)和Move Field(搬移值域)把相同业务的代码放进同⼀个类。
⽐如,想要创建⼀个新的⽤户规则(如“Supper-Admin”),然后你发现,为了增加这个规则还需要修改 Profile、Products 和Employees 类中的某些⽅法。在这种情况下,可以考虑将这些⽅法放在⼀个单
独的类中。
三、坏味道代码重构举例
1.神秘命名
我到了⼤⼆上学期练习数据结构时做的⼀道题⽬:寻数组中的主元(出现次数⼤于n/2),当时图省事就把项⽬名命名为findZY,但这个命名很明显就是代码的坏味道。在leetcode上到了这道题⽬的英⽂描述,更科学的命名为findMaj_Element。
2.重复的代码
⾃⼰在学习Mybatis框架时,dao层实现类中每个⽅法都要先⽤⼏⾏代码拿到SqlSession对象,⼤量重复代码如下
@Override
public Student getById(String id){
String resource ="l";
InputStream inputStream = null;
try{
inputStream = ResourceAsStream(resource);
}catch(IOException e){
e.printStackTrace();
}
sqlSessionFactory =new SqlSessionFactoryBuilder().build(inputStream);
session = sqlSessionFactory.openSession();
//以上代码要在每个⽅法中重复出现
Student s = session.selectOne("ById",id);
return s;
}
因此,要把重复的代码提取出来放到⼀个⽅法中,新建SqlSessionUtil类,对外提供getSession⽅法
static{
String resource ="l";
InputStream inputStream = null;
try{
inputStream = ResourceAsStream(resource);
}catch(IOException e){
e.printStackTrace();
}
sqlSessionFactory =new SqlSessionFactoryBuilder().build(inputStream);
}
private static ThreadLocal<SqlSession> t =new ThreadLocal<>();
public static SqlSession getSession(){
SqlSession session = t.get();
if(null == session){
session = sqlSessionFactory.openSession();
t.set(session);
}
return session;
}
这样,dao层实现类中每个⽅法就变得简洁了
@Override
public Student getById(String id) {
SqlSession session = Session();
Student s = session.selectOne("ById",id);
return s;
}
3.过长函数
在⼤⼀做C语⾔课设——贪吃蛇时,在⼩蛇移动时需要判断是否撞墙,是否吃到等。当时把这些判断逻辑放在了⼩蛇移动函数⾥⾯。现在看来这就是代码的坏味道,⼀个函数⾥的功能太多,显得过于臃肿。
因此重构时要把判断⼩蛇的⽣死抽取出来放⼊⼀个新的函数⾥⾯,在⼩蛇移动函数⾥⾯调⽤这个函数判断。
void death()//判断⼩蛇⽣死
{
Snake *temp4;
temp4 = head->next;
//撞墙
if(head->s_pos.x <10|| head->s_pos.x>560|| head->s_pos.y <10|| head->s_pos.y>560)
snake.isAlive =0;
if(map ==1)
{
if(head->s_pos.x <150&& head->s_pos.y ==285)
snake.isAlive =0;
if(head->s_pos.x >420&& head->s_pos.y ==285)
snake.isAlive =0;
if(head->s_pos.y <150&& head->s_pos.x ==285)
snake.isAlive =0;
if(head->s_pos.y >420&& head->s_pos.x ==285)
snake.isAlive =0;
}
if(map ==2)
{
if(head->s_pos.x <250&& head->s_pos.y ==80)
snake.isAlive =0;
if(head->s_pos.x >350&& head->s_pos.y ==160)
snake.isAlive =0;
if(head->s_pos.x <250&& head->s_pos.y ==240)
snake.isAlive =0;
if(head->s_pos.x >350&& head->s_pos.y ==320)
snake.isAlive =0;
if(head->s_pos.x <250&& head->s_pos.y ==400)
snake.isAlive =0;
}
//头碰到蛇⾝
if(snake.body >=4)
{
while(temp4->next !=NULL)
{
if(temp4->s_pos.x == head->s_pos.x&&temp4->s_pos.y == head->s_pos.y)
{
snake.isAlive =0;
break;
}
else temp4 = temp4->next;
}
}
}
4.过长参数列
在⽹上到的例⼦:打印基本信息的⽅法printBasicInfo,我们每次要传四个参数,如果汽车增加⼀个属性,⼊参要增加参数,这样需要改动多出代码,这时可以⽤Introduce Parameter Object(引⼊参数对象)进⾏重构,创建⼀个汽车对象在⽅法之间进⾏传递.
原来的代码:
// 提炼打印基本信息⽅法
private void printBasicInfo(String brand, String model, Integer price,double power){
System.out.println("品牌"+ brand);
System.out.println("型号:"+ model);
System.out.println("动⼒:"+ power);
System.out.println("价格:"+ price);
}
重构后的代码:
private void printBasicInfo(CarEntity carEntity){
System.out.println("品牌"+ Brand());
System.out.println("型号:"+ Model());
System.out.println("动⼒:"+ Power());
System.out.println("价格:"+ Price());
}
5.痴迷基本类型
使⽤对象把基本类型封装起来,下⾯是⼀个订单类,包含⽤户名、⽤户性别、订单价格、订单id等信息。利⽤Replace Data Value with Object(以对象取代数据值)把⽤户相关信息提炼成⼀个单独的Custom类,再在订单类中引⽤Custom对象。
原来的代码:
// 订单
public class Order {
private String customName;
private String customSex;
private Integer orderId;
private Integer price;
}
重构后的代码:
// 订单
public class Order {
private Custom custom;
private Integer orderId;
private Integer price;
}
// 把custom相关字段封装起来,在Order中引⽤Custom对象
public class Custom {
private String name;
private String address;
}
6.依恋情结
在类中到⼀个⼤量使⽤另⼀个类的⽅法。在这种情况下,你可以考虑将这个⽅法移动到它使⽤的那个类中。例如,要将
getFullAddress() 从 User 类移动到 ContactInfo 类中,因为它调⽤了 ContactInfo 类的很多⽅法。
public class User{
private ContactInfo contactInfo;
public User(ContactInfo contactInfo){
}
public void getFullAddress(ContactInfo contactInfo){
System.out.println("StreetNumber:"+ contactInfo.StreetNumber);
System.out.println("StreetName:"+ contactInfo.StreetName);
System.out.println("ZipCode:"+ contactInfo.ZipCode);
System.out.println("Country:"+ contactInfo.Country);
}
}
7.数据泥团
数据泥团指的是经常⼀起出现的数据,处理⽅式与过长参数列的处理⽅式相同,⽤Introduce Parameter Object(引⼊参数对象)将参数封装成对象。
例⼦与过长参数列相同,重构后的好处很明显,除了参数列变短,就算汽车有新的属性进⾏扩展,也不需要修改参数列,这样更便于我们对代码的维护。
8.发散式变化
发散式变化可以采⽤桥接模式重构
假设某个汽车⼚商⽣产三种品牌的汽车:Big、Tiny和Boss,每种品牌⼜可以选择燃油、纯电和混合动⼒。如果⽤传统的继承来表⽰各个最终车型,⼀共有3个抽象类加9个最终⼦类,如果要新增⼀个品牌,或者加⼀个新的引擎(⽐如核动⼒),那么⼦类的数量增长更快。
⾸先把Car按品牌进⾏⼦类化,但是,每个品牌选择什么发动机,不再使⽤⼦类扩充,⽽是通过⼀个抽象的“修正”类,以组合的形式引⼊。
//Car
public abstract class Car {
// 引⽤Engine:
protected Engine engine;
public Car(Engine engine){
}
public abstract void drive();
}
//Engine
public interface Engine {
void start();
}
在⼀个“修正”的抽象类RefinedCar中定义⼀些额外操作:
public abstract class RefinedCar extends Car {
public RefinedCar(Engine engine){
super(engine);
贪吃蛇的编程代码}
public void drive(){
System.out.println("Drive "+getBrand()+" ");
}
public abstract String getBrand();
}
这样⼀来,最终的不同品牌继承⾃RefinedCar,例如BossCar
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论