设计坏味道
1. 设计坏味道概述
为何要关注坏味道,因为坏味道影响了软件的质量;给项目的开发、维护、扩展等带来了影响;
A. 软件质量
i. 可理解性
1. 代码理解的难易程度;
ii. 可以修改性
1. 修改代码时,不会导致连锁反应;
iii. 可扩展性
1. 增加新功能,不会导致连锁反应;
iv. 可重用性
1. 代码可以在相同问题域中,直接复用;
v. 可测试性
1. 支持单元测试
vi. 可靠性
1. 正确实现功能情况下,支持容错性;
2. 坏味道分类
3. 抽象性
A. 缺失抽象
Example:
_ 问题点在JDK1.0中方法printStackTrace()以字符串的方式将栈跟踪打印到标准错误流:
public classThrowabe{ public voidprintStackTrace();
}
在需要以编程方式访问栈跟踪元素的客户程序中,必须要编程代码来获取数据,如行号等,由于客户程度依赖这种字符串格式,JDK设计人员只能在后续版本中兼容这种格式了。
_ 解决方法
public classThrowabe{ public voidprintStackTrace(); public StackTraceElement[]getStackTrace();
}
从Jdk1.4起对JAVA的API进行了改进,StackTraceElement类就是原来设计中缺失的对象,定义如下:
public finalclassStackTraceElement{
public final classStackTraceElement{
public StringgetFilename();
publicintgetLineNumber();
public StringgetClassname();
......
}
Example:
_ 问题点
细粒度的异常处理问题,以前见过系统的异常类只是继承Exception,只保存了message信息;因此如果需要细分并根据不同类型,不同的级别急性控制时,就比较麻烦;
_ 解决方法
_ 细分异常类型
_ 增加errorcode
异常类包含了一个接口,具体异常类中,枚举类型实现该接口;
说明:使用function编程解耦业务异常类的处理,使业务类只关注业务;
B. 命令式抽像
Example:
_ 问题点
说明:每个类只包含一个方法,分别是:create、display、copy;因此存在命令式抽象,会增加类的数量、开发、维护的复杂度;而且把本该内聚的方法,分散到多个类上;没有做到内聚,而增加了耦合;
_ 解决方案
根据高内聚原则,归并到一个类中;
C. 不完整抽象
抽象未支持互补方法,导致不完成抽象,比如一个抽象接口,只有startUp,而没有stopUp方法;
常见互补方法:
D. 多方面抽象
对象被赋予不止一种职责,违背单一职责原则;
Example:
_ 问题点
java.util.Calendar
类承担了多项职责,不仅提供了日期相关的功能,还提供了与时间有关的功能,存大多方面抽象。由于同时支持日期和时间的方法,Calendar类接口很大且难为理解,在JDK7中,java.util.Calendar类包括了2825行代码,有67个方法和71个字段。
_ 解决方案对于Calendar类,一种可能的重构是,将Calendar类与时间相关的功能提取到新类Time中,并将相关方法和字段移到新提取的类中,在Java8中引入了一些支持日期和时间的新类,这些类位于java.time中。
E. 不必要抽象
Example:
_ 问题点
publicinterfaceWindowConstants{
public static finalintDO_NOTHING_ON_CLOSE=0;
public static finalintHIDE_ON_CLOSE=1;
}
注:这个接口是典型的常量接口javax.swing.WindowConstants,为啥用接口来存储常量,因为首先枚举是jdk1.5才引入的,其次通过接口中定义常量,可方便类通过继承而不是委托来使用它们,因为通过实现接口,类可方便的访问接口中的常量,为什么不使用类来存储常量呢,因为接口支持多继承。
那么接口这样定义常量有哪些问题呢?
A
、派生类被无关的常量影响。
B
、这些常量属于实现细节,通过接口暴露它们违反封装原则。
C
、接口中存储常量,修改它们会影响其他使用者。
_ 解决方案将WindowsConstants定义为枚举,直接使用。
F. 重复抽象
根据DRY原则规定:对于每个技术点,系统中都只能有一个明确的表示。导致重复抽象的原因有:
A
、复制-粘贴编程手法
B
、即兴维护
C
、交流不畅
Example1:
_ 问题点
java.util.Date
和其派生类java.sql.Date同名,这两个类位于不同的包中,编译器不会因为它们同名而报错,但这让使用者一头雾水,这样将导致二义性。
_ 解决方案将Date名称前面加上用途限定语,比如java.sql.SQLDate更合适。
Example2:
问题点
相同的类,包名称不一样,不同的包面向不同的使用者;
task-clent中包含com.xx.xx.task.domain.task.Task和task-server 中的com.xx.xx.task.domain.task.Task,一个抽象放到不同的模块,项目中最好也要避免这种情况;
i. DRY原则
用Lambda和Function解决方式:
说明:在该execute基础上实现单、宽index的cud操作;
G. 书籍推荐:
<<软件设计重构>>
4. 总结本次只分享抽象性的坏味道,欢迎感兴趣的同事,结合自己的理解和实践,继续分享其他模块;