四. OOP 规约
【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
【强制】所有的覆写方法,必须加
@Override
注解。
说明:getObject()
与get0bject()
的问题。一个是字母的 O ,一个是数字的 0,加@ Override可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。【强制】相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object 。
说明:可变参数必须放置在参数列表的最后。 ( 提倡同学们尽量不用可变参数编程 )
正例:
public User getUsers(String type, Integer... ids) {...}
- 【强制】外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生
影响。接口过时必须加@ Deprecated
注解,并清晰地说明采用的新接口或者新服务是什么。
- 设计时没有考虑周全,需要改造接口,需要通过增加新接口,迁移后下线老接口的方式实现。
- REST接口只能增加参数,不能减少参数,返回值的内容也是只增不减。
- 【强制】不能使用过时的类或方法。
说明:java.net.URLDecoder
中的方法 decode(String encodeStr) 这个方法已经过时,应该使用双参数 decode(String source, String encode) 。接口提供方既然明确是过时接口,那么有义务同时提供新的接口 ; 作为调用方来说,有义务去考证过时方法的新实现是什么。
明确了责任和义务,接口提供方也有义务推动接口使用方尽早迁移,不要积累技术负债。
- 【强制】 Object 的
equals
方法容易抛空指针异常,应使用常量或确定有值的对象来调用
equals 。
正例:" test " .equals(object);
反例: object.equals( " test " );
说明:推荐使用java.util.Objects # equals
(JDK 7 引入的工具类 )
// JavaSE-1.7源码
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
使用常量对比变量
- 【强制】所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。
说明:对于 Integer var = ? 在-128 至 127 范围内的赋值, Integer 对象是在IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。
Java世界里相等请用equals方法,==表示对象相等,一般在框架开发中会用到。
- 关于基本数据类型与包装数据类型的使用标准如下:
1 ) 【强制】所有的 POJO 类属性必须使用包装数据类型。
2 ) 【强制】 RPC 方法的返回值和参数必须使用包装数据类型。
3 ) 【推荐】所有的局部变量使用基本数据类型。
说明: POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE 问题,或者入库检查,都由使用者来保证。
正例:数据库的查询结果可能是 null ,因为自动拆箱,用基本数据类型接收有 NPE 风险。
反例:比如显示成交总额涨跌情况,即正负 x %, x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。
包装数据类型与基本数据类型相比,增加了一个null的状态,可以携带更多的语义。
- 【强制】定义
DO
/DTO
/VO
等POJO
类时,不要设定任何属性默认值。
反例:POJO
类的gmtCreate
默认值为new Date()
; 但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。
虽然这里反例不太容易看懂,但是要记得持久领域对象之前由应用层统一赋值
gmtCreate
和gmtModify
字段。
- 【强制】序列化类新增属性时,请不要修改
serialVersionUID
字段,避免反序列失败 ; 如果完全不兼容升级,避免反序列化混乱,那么请修改serialVersionUID
值。
说明:注意serialVersionUID
不一致会抛出序列化运行时异常。
不到万不得已不要使用JDK自身的序列化,机制很重,信息冗余有版本。
【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在
init
方法中。【强制】
POJO
类必须写toString
方法。使用 IDE 的中工具: source > generate toString
时,如果继承了另一个 POJO 类,注意在前面加一下super . toString
。
说明:在方法执行抛出异常时,可以直接调用 POJO 的toString()
方法打印其属性值,便于排查问题。【推荐】使用索引访问用 String 的
split
方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛IndexOutOfBoundsException
的风险。
说明:
String str = "a,b,c,,,,,";
String[] ary = str.split(",");
// 预期大于 3,结果是 3
System.out.println(ary.length);
// 输出: [a, b, c]
System.out.println(Arrays.toString(ary));
编程要留心眼,任何不确定的地方都要判断、处理,否则掉到坑里了自己爬出来很费劲。
【推荐】当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读,此条规则优先于第 15 条规则。
【推荐】 类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 >
getter
/setter
方法。
说明:公有方法是类的调用者和维护者最关心的方法,首屏展示最好 ; 保护方法虽然只是子类关心,也可能是“模板设计模式”下的核心方法 ; 而私有方法外部一般不需要特别关心,是一个黑盒实现 ; 因为承载的信息价值较低,所有 Service 和 DAO 的getter
/setter
方法放在类体最后。【推荐】
setter
方法中,参数名称与类成员变量名称一致, this .成员名 = 参数名。在getter
/setter
方法中,不要增加业务逻辑,增加排查问题的难度。
反例:
public Integer getData() {
if (true) {
return this.data + 100;
} else {
return this.data - 100;
}
}
- 【推荐】循环体内,字符串的连接方式,使用
StringBuilder
的 append 方法进行扩展。
说明:反编译出的字节码文件显示每次循环都会 new 出一个StringBuilder 对象,然后进行append
操作,最后通过toString
方法返回 String 对象,造成内存资源浪费。
反例:
String str = "start";
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
- 一行的字符串连接代码性能很不错。
- 对于多行的连接操作,一定要确保使用 StringBuilder。
- 【推荐】
final
可以声明类、成员变量、方法、以及本地变量,下列情况使用final
关键字:
1) 不允许被继承的类,如: String 类。
2) 不允许修改引用的域对象,如: POJO 类的域变量。
3) 不允许被重写的方法,如: POJO 类的 setter 方法。
4) 不允许运行过程中重新赋值的局部变量。
5) 避免上下文重复使用一个变量,使用final
描述可以强制重新定义一个变量,方便更好地进行重构。
尽量多使用final关键字,保证编译器的校验机制起作用,也体现了“契约式编程”的思想。
- 【推荐】慎用 Object 的
clone
方法来拷贝对象。
说明:对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现属性对象的拷贝。
- 通过
clone
方法生成一个对象时,就会不再执行构造函数,只是在内存中进行数据块的拷贝,事实上,一般情况下new生成的对象比clone生成的性能方面要好很多,最好是使用构造函数来重新构造对象。- 使用
clone
拷贝的时候,对象引用关系可能很复杂,不直观,不好理解。
- 【推荐】类成员与方法访问控制从严:
1 ) 如果不允许外部直接通过new 来创建对象,那么构造方法必须是private 。
2 ) 工具类不允许有 public 或 default 构造方法。
3 ) 类非 static 成员变量并且与子类共享,必须是 protected 。
4 ) 类非 static 成员变量并且仅在本类使用,必须是 private 。
5 ) 类 static 成员变量如果仅在本类使用,必须是 private 。
6 ) 若是 static 成员变量,必须考虑是否为 final 。
7 ) 类成员方法只供类内部调用,必须是 private 。
8 ) 类成员方法只对继承类公开,那么限制为 protected 。
说明:任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。
思考:如果是一个 private 的方法,想删除就删除,可是一个 public 的 service 方法,或者一个 public 的成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你会担心的。
没什么好说的,三个词,高内聚,低耦合,功能模块闭包。
额外
六大设计原则解读
- 单一职责原则(Single Responsibility Principle),简称是SRP
SRP的原话解释是:
There should never be more than one reason for a class to change.
应该有且仅有一个原因引起类的变更。
- 里氏替换原则(Liskov Substitution Principle)
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基类的地方必须能透明地使用其子类的对象。
1. 子类必须完全实现父类的方法
2. 子类可以有自己的个性
3. 覆盖或实现父类的方法时输入参数可以被放大
4. 覆写或实现父类的方法时输出结果可以被缩小
目的: 目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑,非常完美!
- 依赖倒置原则(Dependence Inversion Principle)
原始定义:
High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
翻译过来,包含三层含义:
❑高层模块不应该依赖低层模块,两者都应该依赖其抽象;
❑抽象不应该依赖细节;
❑细节应该依赖抽象。
依赖倒置原则是6个设计原则中最难以实现的原则,它是实现开闭原则的重要途径,依赖倒置原则没有实现,就别想实现对扩展开放,对修改关闭。在项目中,大家只要记住是“面向接口编程”就基本上抓住了依赖倒置原则的核心。
- 接口隔离原则(Interface Segregation Principle)
它有两种定义,如下所示:
❑Clients should not be forced to depend upon interfaces that they don't use.(客户端不应该依赖它不需要的接口。)
❑The dependency of one class to another one should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上。)
新事物的定义一般都比较难理解,晦涩难懂是正常的。我们把这两个定义剖析一下,先说第一种定义:“客户端不应该依赖它不需要的接口”,那依赖什么?依赖它需要的接口,客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,那就需要对接口进行细化,保证其纯洁性;再看第二种定义:“类间的依赖关系应该建立在最小的接口上”,它要求是最小的接口,也是要求接口细化,接口纯洁,与第一个定义如出一辙,只是一个事物的两种不同描述。
- 迪米特法则(Law of Demeter)
迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least Knowledge Principle,LKP),虽然名字不同,但描述的是同一个规则:一个对象应该对其他对象有最少的了解。
通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关心。
迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。读者在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚低耦合。
- 开闭原则(Open Closed Principle)
Software entities like classes,modules and functions should be open for extension but closed for modifications.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。)
开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
把这6个原则的首字母(里氏替换原则和迪米特法则的首字母重复,只取一个)联合起来就是SOLID(solid,稳定的),其代表的含义也就是把这6个原则结合使用的好处:建立稳定、灵活、健壮的设计,而开闭原则又是重中之重,是最基础的原则,是其他5大原则的精神领袖。
参考(References)
- 《码出高效 阿里巴巴Java开发手册 终极版(1.3.0)》
- 《设计模式之禅 第1版》
- 《Java技术手册 第6版》
- 《编写高质量代码:改善Java程序的151个建议》
白话阿里巴巴Java开发手册(安全规约) - 李艳鹏 - (https://www.jianshu.com/p/9528c4ea1504)