Java编程规范

文章目录

前言

部分未尽详情参考《阿里巴巴Java开发手册V1.6(泰山版)》:https://download.csdn.net/download/zhiyuan411/12602917

另有著书《阿里巴巴Java开发手册》,《码出高效:Java开发手册》,可自行百度电子版。

编程规范的存在意义:

  • 标准统一,提升沟通效率和研发效能。
  • 防患未然,提升质量意识和系统可维护性,降低故障率。
  • 工匠精神,追求极致的卓越精神,打磨精品代码。

规范分为三个级别:

  • 【强制】必须遵守。是不得不遵守的规定,违反本规定或将引起严重的后果。
  • 【推荐】尽量遵守。长期遵守这样的规定,有助于系统稳定性和合作效率的提升。
  • 【参考】充分理解。技术意识的引导,是个人学习、团队沟通、项目合作的方向。

对应的Java开发规约IDE插件:https://github.com/alibaba/p3c

编程规范

命名规范

【强制】命名不能以下划线或美元符号开头或者结尾。

【强制】命名不允许使用拼音与英文混合的方式。

【强制】类目使用UpperCamelCase风格。

以下情况例外:DO/BO/DTO/VO/AO/UID等。

【强制】方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格。

【强制】常量命名应该全部大写,单词间使用下划线隔开。

力求语义表达完整,不要嫌名字太长。

【强制】抽象类命名使用Abstract或Base开头;异常类命名使用Exception结尾;测试类命名以它要测试的类目开始,以Test结尾。

【强制】定义数组时,类型和[]紧挨。

【强制】POJO类中的任何布尔类型的成员变量,不要加is前缀。

否则这些成员变量的get/set方法命名会仍然是isXXX(),部分框架在序列化时会出现解析错误。

【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英文单词。

【强制】包名统一使用单数形式;类目可以使用复数形式。

【强制】避免子类父类使用同名的成员变量;同一方法的不同代码块内使用同名的局部变量;非get/set方法的参数和成员变量同名;

【推荐】命名可以做到代码自解释。

避免a,b,c等无意义命名。而是使用可以表达意义的单词组合来命名。

【推荐】如何使用了设计模式,在命名时要体现出具体模式。

【推荐】接口中的方法不用加任何修饰符,并尽量避免定义变量。

接口方法默认就是public abstract。
JDK8中规定接口允许有默认实现。

【强制】对于Service和DAO类,实现类使用接口类名+Impl命名。

【参考】枚举类名加上Enum后缀,枚举成员名称需要全大写并下划线分割。

注释规范

【强制】类、类属性、类方法的注释必须使用javadoc注释方式,不能使用//方式。

优点:

  1. 生成javadoc可以正确输出相应注释;
  2. 在IDE中,调用方法时,不进入方法即可以悬浮提示方法、参数、返回值的意义,提高效率;

【强制】所有的抽象方法(包括接口中的方法)必须要用javadoc注释。

并且要说清楚对子类的实现要求或者调用注意事项等。

【强制】所有的类都必须添加创建者信息和创建日期。

正例:

/**
  * @author xxx
  * @date xxxx/xx/xx
  */

【强制】方法内部单行注释,在被注释语句上方另起一行,使用//注释。

【强制】方法内部多行注释使用/* */注释,注意与代码对齐。

【强制】所有的枚举字段必须要有注释。

【推荐】使用中文注释。

【推荐】代码修改的同时,注释也要进行相应的修改。

尤其是参数、返回值、异常、核心逻辑等的修改。

【推荐】在类中,删除未使用的任何字段和方法;在方法中,删除未使用的任何参数声明与内部变量。

【参考】谨慎注释掉代码。如果以后可能恢复,在上方添加详细注释说明;如果无用,则删除。

【参考】对于注释的要求:第一,能够准确反映设计思想和代码逻辑;第二,能够描述代码背后的业务含义。

【参考】注释应该精简准确、表达到位,避免出现一个极端:过多过滥的注释,造成维护负担。

好的命名、代码结构是自解释的。

【参考】特殊注释标记,请注明标记人与标记时间。

例如:

  • 待办事宜:@TODO: xxx
  • 错误,不能工作:@FIXME: xxx

格式规范

【强制】大括号内为空写为{};非空则Java式大括号风格。

Java式大括号风格:
左大括号:前不换行,后换行;
右大括号:前后都换行;例外:有else等代码块则后不换行;

【强制】小括号外侧前后都加空格,内侧前后都不加空格。

正例:

if (a == b) {
    return a + b;
}

【强制】类型强制转换时,右括号之后不加空格。

【强制】任何二目、三目运算符的左右两侧都加空格。

【强制】采用4个空格缩进,禁止使用tab字符。

【强制】双斜线注释符后面有且仅有一个空格。

【强制】单行字符数不超过120个。

【强制】换行后的所有行都缩进4个空格;运算符、方法调用的点符号之前换行;逗号(例如多个参数)之后换行;

【强制】定义或使用时,多个参数的逗号之后加空格。

【强制】IDE的text file encoding设置为UTF-8,换行符使用Unix格式。

【推荐】没有必要增加若干空格使得多个变量赋值时按照等号来对齐。

正例:

int one = 1;
long two = 2L;

【推荐】单个方法的总行数不超过80行。

除注释之外的其他,包括代码、左右大括号、空行等,总行数不超过80行。
将分支逻辑抽离子方法。

【推荐】不同逻辑/语义/业务的代码之间插入一个且仅一个空行分割来提高可读性。

常量定义

【强制】不允许任何魔法值。

魔法值即未经预先定义的常量。

反例:String key=“str_biz_” + id;
其中的字符串即魔法值,会在其他地方引用时可能产生拼写错误,例如少了下划线。

【强制】在long/Long赋值,使用大写L。

  1. 使用时不使用L默认是整型或者浮点数;
  2. 使用小写l容易和1弄混;

【推荐】不用使用一个常量类维护所有常量,而是按照业务/功能进行归类。

【推荐】如果变量值仅在一个固定范围内变化用enum类型来定义。

控制语句

【强制】在一个switch块内,每个case要么通过 break / continue / return 等来终止,要么注释说明程序将继续执行到哪一个case为止。

【强制】每个switch块都必须包含一个default语句并放在最后,即使它为空。

【强制】当switch括号内的变量类型为String,必须先进行NPE判断。

【强制】在 if / else / for / while / do 语句中必须使用大括号,即使只有一行代码。

【强制】三目运算符 condition ? exp1 : exp2 中,高度注意 exp1 和 exp2 在涉及算术计算或数据类型转换时,可能抛出因自动拆箱导致的NPE异常。

以下2种场景会触发自动拆箱操作:

  1. exp1 或 exp2 的结果值是原始类型;
  2. exp1 和 exp2 的结果值的类型不一致,会强制拆箱升级成表示范围更大的那个类型

反例:

Integer a = 1;
Integer b = 2;
Integer c = null;
Boolean flag = false;
// a*b的结果是int类型,所以c会自动拆箱成int类型,故抛出NPE异常
Integer result = (flag ? a*b : c);

【强制】在高并发场景中,避免使用“等于”判断作为中断或退出的条件。

如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用小于等于的区间判断条件来代替。

【推荐】表达异常分支时,少用if-else方式,而是用卫语句。

正例:

if (condition) {
    // 异常分支逻辑
    ...
    return obj;
}
// 接着写else的业务逻辑代码
...

【强制】超过3层的if-else的逻辑判断代码,需要使用卫语句、策略模式、状态模式等来实现。

【推荐】当某个方法的代码总行数超过10行时,return / throw 等中断逻辑的右大括号后需要加一个空行。

【推荐】不要在条件判断中执行复杂的语句,将复杂逻辑判断的结果赋值给一个命名有意义的布尔变量,以提高可读性。

【推荐】不要在其他表达式(尤其是条件表达式)中,插入赋值语句。

赋值语句应该清晰地单独成为一行。

【推荐】避免采用取反逻辑运算符。

取反逻辑一般是不利于快速理解,取反逻辑写法一般都存在对应的正向逻辑的写法。

【推荐】循环体内的语句要考量性能,定义对象/变量、获取数据库连接、不必要的try-catch都尽量移至循环体外处理。

【参考】推荐进行参数校验的场景如下:

从性能消耗影响,稳定性,安全性等方面考虑,推荐进行参数校验的场景:

  1. 调用频次低的方法。
  2. 执行实际开销很大的方法。
  3. 需要极高稳定性和可用性的方法。
  4. 对外提供的开放接口。
  5. 敏感权限接口。

【参考】推荐不必进行参数校验的场景如下:

从性能等方面考虑,推荐不必进行参数校验的场景如下:

  1. 极有可能被循环调用的方法;但在方法说明里必须注明外部参数检查。
  2. 位于底层,且调用频度比较高的方法;例如DAO的参数校验,可以忽略。
  3. 被声明为private,并能够确定自己代码在调用时入参肯定没问题。

异常处理

【强制】可以通过预检查方式规避的RuntimeException异常不应该通过catch的方式来处理,例如:NPE。

【强制】异常捕获不要用来做流程控制、条件控制。

异常处理的初衷是解决意外情况,其效率比条件判断方式要低很多。

【强制】稳定代码不要加catch;非稳定代码尽可能区分异常类型,再做对应的异常处理。

【强制】捕获异常后应该进行处理或者将异常抛给它的调用者;最外层业务使用者必须处理异常,将其转化为用户可以理解的内容。

【强制】事务场景中,catch到异常后,如果需要回滚,一定要手动回滚事务。

【强制】finally块必须对资源对象、流对象进行关闭,并内部进行try-catch处理关闭异常。

在JDK7支持try-with-resources语法糖方式。

【强制】不要在finally块中使用return。

finally的return会丢弃掉try块中的返回点。

反例:

private int x = 0;
public int checkReturn() {
    try {
        // 返回1
        return ++x;
    } finally {
        // 返回的结果变为了2
        return ++x;
    }
}

【强制】捕获异常类型与抛异常的类型匹配或者是其父类。

【强制】在调用RPC、三方包或动态生成类的相关方法时,捕捉异常必须使用Throwable类来进行拦截。

需要捕捉例如 NoSuchMethodException 异常。

【推荐】方法的返回值可以为null,不强制返回空集合/空对象时,必须添加注释充分说明什么情况下会返回null值。

【推荐】NPE产生的常见场景如下:

  1. 方法的返回类型为基本数据类型,return的是包装数据类型的对象,此时会进行自动拆箱,有可能NPE。
  2. 数据库查询结果可能为null。
  3. 集合里的元素取出来的可能值为null。
  4. 远程调用返回对象时,可能为null。
  5. session中获取的数据,可能为null。
  6. 级联调用 obj.getA().getB().getC(); 容易产生NPE。

【推荐】对公司外开放的接口必须使用错误码;内部跨应用RPC调用优先使用Result方式:封装isSuccess()方法、错误码、错误信息;应用内部推荐抛出异常。

内部跨应用RPC调用不推荐使用异常的原因在于:栈信息比较大,数据序列号、传输的性能损耗比较大。

【推荐】定义时区分unchecked/checked异常,避免直接使用RuntimeException异常,而是使用有业务含义的自定义异常。

日期时间

【强制】日期格式化时,传入pattern中表示年份统一使用小写的yyyy。

yyyy表示当天所在的年;YYYY表示当周的周末所在的年。

【强制】日期格式化时,分清楚大小写的M和H。

M:月份
m:分钟
H:24小时制的小时
h:12小时制的小时

【强制】获取当前毫秒数:System.currentTimeMillis();而不是new Date().getTime();

纳秒级时间使用System.nanoTime。
JDK8中,针对统计时间等场景,推荐使用Instant类。

【强制】不允许在程序任何地方使用 java.sql.Date/Time/Timestamp

java.sql的Date不记录时间,Time不记录日期,Timestamp使用属性fastTime和nanos分别存储秒和纳秒信息。都会和其父类java.util类存在一些方法上的不兼容。

【强制】禁止在程序中写死一年为365天;应该使用动态获取。

正例:

// 获取今年天数
LocalDate.now().lengthOfYear();
// 获取某年的天数
LocalDate.of(2020, 1, 1).lengthOfYear();

【推荐】避免闰年2月问题。2月29日的一年后是3月1日。

【推荐】使用枚举值来指代月份。如果使用数字,注意Date、Calendar等日期相关类的month取值为0-11。

正例:

// 1月,实际上值为0
Calendar.JANUARY

OOP规范

【强制】避免通过对象来引用静态变量或静态方法。

会无谓地增加编译器解析成本。
直接使用类名引用。

【强制】所有的覆写方法,必须加 @Override 注解。

添加注解后会在编译时检查是否覆盖正确。

【强制】相同参数类型,相同业务含义,才可以使用Java的可变参数,且要避免使用Object类型。

不推荐使用可变参数。
正例:

public void fun(String type, String... params) {}

【强制】外部调用的接口,不允许修改方法签名。

【强制】接口过时必须加 @Deprecated 注解,并在接口文档注释中清晰地说明采用的新接口是什么。

【强制】不能使用过时的类或方法。

【强制】应使用常量或确定非null的对象来调用equals。

推荐使用JDK7引入的 java.util.Objects#equals(Object a, Object b);

【强制】所有整型包装类的对象之间的值比较,使用equals方法。

Integer 的-128至127之间的赋值,在 IntegerCache.cache 产生,会复用已有对象。
其他的会在堆上产生,无法使用 == 比较。

【强制】浮点数之间的等值比较,不能使用 == 或者 equals 来判断。

浮点数采用“尾数+阶码”的编程方式,二进制无法精确表示大部分的十进制小数。
反例:

float a = 0.9f;
if (1 - a == 0.1f) {
    // 实际上,不会走到此代码块中
}

正例:
1)指定一个误差范围,比较差值是否在此误差范围内;
2)使用BigDecimal来定义值。

【强制】BigDecimal的值的比较使用compareTo()方法,而不是equals()方法。

equals()的方法会比较值和精度,而compareTo()则会忽略精度。例如1.0和1.00使用2种方法的比较结果会不同。

【强制】禁止使用构造方法BigDecimal(double)的方式把double值转化为BigDecimal对象。

BigDecimal(double)构造方法存在精度损失风险。推荐使用BigDecimal(String)的构造方法或者BigD.valueOf(Double)方法;valueOf()方法内部其实执行了Double的toString(),而Double的toString()按double的实际能表达的精度对尾数进行了截断。

反例:

// 实际的存储值为:0.10000000149
BigDecimal bd = new BigDecimal(0.1f);

正例:

BigDecimal bd = new BigDecimal("0.1f");
BigDecimal bd = BigDecimal.valueOf(0.1f);

【强制】任何货币金额,均以最小货币单位且整型类型来存储。

【强制】定义数据对象DO类时,属性类型要与数据库字段类型相匹配。

正例:
bigint对应Long类型。

【强制】POJO类的属性,RPC方法的参数和返回值,必须使用包装数据类型。

数据库的查询结果可能为null,使用基本数据类型接收会因为自动拆箱而有NPE风险。
而远程调用的返回值为null,可以表示额外的信息,如异常失败。

【推荐】局部变量推荐使用基本数据类型。

【强制】定义POJO类时,不要定义任何属性的默认值。

反例:
DO的createTime被定义为默认值new Date();在从数据库读取不含该字段的数据后,再更新时,会对该字段造成数据污染。

【强制】序列化类新增属性时,不能修改serialVersionUID值;完全不兼容升级,需要修改serialVersionUID值。

serialVersionUID不一致会抛出序列化运行时异常。

【强制】构造方法中禁止加入任何业务逻辑;初始化逻辑应该添加到init方法中。

【强制】POJO类必须覆写toString()方法;子类注意添加super.toString()。

【强制】禁止在POJO类中,同时存在对应属性xxx的isXxx()和getXxx()方法。

不同框架在调用属性xxx的提取方法时,并不确定是优先调用的哪个方法。

【推荐】使用索引访问String的split方法得到的数组时,注意长度检查。

String str = "a,b,c,,";
String[] arr = str.split(",");
// 实际上,arr.length == 3

【推荐】一个类的多个构造方法或同名方法应该顺序放置在一起。

【推荐】类内方法的推荐按照信息价值顺序:公有方法 > 保护方法 > 私有方法 > getter/setter方法。

【推荐】构造函数/setter方法的参数名称与成员变量名称一致;getter/setter方法中不含业务逻辑。

【推荐】循环体内,字符串的联接方式,使用StringBuilder的append方法。

使用String的话,每次拼接都会new出一个StringBuilder对象,然后进行append操作,最后通过toString返回String对象。造成性能和内存的浪费。

【推荐】不允许被继承的类和不允许被覆写的方法,都使用final来修饰。

【推荐】慎用Object的clone方法来拷贝对象,默认是浅拷贝。

【推荐】类成员与方法访问控制从严,方便维护重构。

  1. 不允许外部new来创建对象,那么构造方法必须是private。
  2. 工具类不允许有pbulic或者default构造方法。
  3. 成员变量/成员方法仅在本类使用时、仅与子类共享时,private和protected。
  4. static成员变量必须考虑是否为final。

集合处理

【强制】只要覆写equals,就必须覆写hashCode。

【强制】Set存储的对象、Map的key对象,必须覆写equals和hashCode。

【强制】使用Map的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作。

否则会抛异常。

【强制】在使用 java.util.stream.Collectors 的toMap()方法时,选择带 BinaryOperator mergeFunction 参数的方法来处理重复key。

该方法的作用是当key重复时,自定义对value的处理策略。

【强制】在使用 java.util.stream.Collectors 的toMap()方法时,注意当value为null会抛NPE异常。

原因是其使用了HashMap的merge方法,而该方法中在判断value为null时抛出NPE异常。

【强制】判断集合内部元素是否为空,使用isEmpty()方法,而不是size() == 0的方式。

在某些集合中,前者的时间复杂度为O(1),且可读性好。

【强制】返回值不能使用Collections类返回的对象

Collections类返回的对象,如:emptyList() / singletonList() 等都是immutable list,不可进行添加或删除元素的操作。

【强制】ArrayList的subList结果不可强制转换成ArrayList,否则会抛异常。

subList()返回的是ArrayList的内部类SubList,相当于是ArrayList的一个视图,对于SubList的所有操作会影响原ArrayList。

【强制】在subList场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生异常。

【强制】使用集合转数组的方法,必须使用集合的带参toArray(T[] array)方法,传入的是类型完全一致、长度为0的空数组。

直接使用toArray()无参方法存在问题,此方法返回值只能是Object[]类,若强转其他类型数组将报异常。
使用toArray带参方法,数组长度为0时,会动态创建与size相同的数组,性能最好。数组长度大于实际元素个数时,会补充插入null值,有NPE隐患。

正例:

List<String> list;
String[] array;
...
array = list.toArray(new String[0]);

【强制】在使用Collection接口任何实现类的addAll()方法时,都要对输入的集合参数进行NPE判断。

如果为null会抛NPE。

【强制】使用Arrays.asList()把数组转换成集合时,不能对其进行修改,否则会抛异常。

asList()返回的是Arrays内部类,没有实现集合的修改方法。它体现的是适配器模式,底层数据仍然是原数组。当数组的元素发生变化时,集合的对应元素也会变化。

【强制】泛型通配符<? extends T>来接收返回的数据,此泛型集合不能使用add方法;而<? super T>不能使用get方法。

PECS(Producer Extends Consumer Super)原则:
频繁往外读取内容的,适合用<? extends T>。
频繁往里插入内容的,适合用<? super T>。

【强制】在无泛型限制的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行instanceof判断,避免抛出类型转换异常。

反例:

List<String> generics = null;
List notGenerics = new ArrayList(10);
notGenerics.add(new Object());
notGenerics.add(new Integer(1));

generics = notGenerics;

// 此处抛出ClassCastException异常
String str = generics.get(0);

【强制】不要在foreach循环里进行元素的添加/删除操作;删除操作应该使用Iterator方式,如果并发操作,需要对Iterator迭代器对象加锁。

【强制】实现Comparator时必须单独处理相等情况。

在JDK7版本以上,Comparator要满足如下3个条件,否则sort时会抛异常。
如下3个条件:
1)x,y的比结果和y,x的比较结果相反;
2)x>y,y>z,则x>z;
3)x=y,则x、z比较结果和y、z比较结果相同;

如果不处理相等情况,就不满足第1个条件。

【强制】泛型集合使用时,在JDK7及以上,使用diamond语法或全省略。

菱形泛型即diamond,直接使用<>来指代前边已经制定的类型。

正例:

// diamond方式
Map<String, String> map = new HashMap<>(16);

// 全省略方式
Map<String, String> map = new HashMap(16);

【推荐】集合初始化时,指定集合初始值大小。

例如Map,当元素个数很大,在元素增加时被迫需要不断扩容,反复重建哈希表和数据迁移,性能消耗很大。

【推荐】使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历。

因为使用keySet方式,还需要再次取value,会造成性能浪费。
对于JDK8,使用Map.forEach()方法。

【推荐】高度注意Map类集合的K/V能不能存储null值的情况。

如下表格:

集合类KeyValueSuper并发
HashTable不允许为null不允许为nullDictionary线程安全
ConcurrentHashMap不允许为null不允许为nullAbstractMap锁分段技术(JDK8:CAS)
TreeMap不允许为null任意AbstractMap线程不安全
HashMap任意任意AbstractMap线程不安全

【参考】避免集合的无序性和不稳定性带来的负面影响。

有序性sort是指遍历的结果是按照某种比较规则依次排序的。
稳定性order是指集合每次遍历的元素次序是一定的。

ArrayList是order/unsort
HashMap是unorder/unsort
TreeSet是order/sort

【参考】利用Set元素唯一的特性,可以快速对另一个集合进行去重操作。

并发处理

【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。

资源驱动类、工具类、单例工厂类都需要注意。

【强制】创建线程或线程池时指定有意义的现场名称,以方便追查问题。

【强制】线程资源必须通过线程池提供,不允许显式创建线程。

减少性能消耗和资源开销。

【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。

Executors返回的线程池对象,其允许的请求队列长度/创建线程数量为Integer.MAX_VALUE,可能会导致OOM。

【强制】SimpleDateFormat是线程不安全的类,一般不要定义为static;如果定义为static,必须加锁,或者使用DateUtils工具类。

如果是JDK8,可以使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat。

【强制】必须回收自定义的ThreadLocal变量。

通过try-finally块进行回收,以避免线程复用时造成问题。

【强制】高并发时,同步调用应该去考量锁的性能损耗。

尽量减少锁的范围。
尽可能使加锁的代码块工作量尽可能小,避免在锁代码块中调用RPC方法。

【强制】对多个资源、数据库表、对象同时加锁,需要保持一致的加锁顺序,否则可能会造成死锁。

【强制】在使用阻塞等待获取锁的方式中,在加锁方法后紧跟try代码块,并在finally中解锁。

正例:

Lock lock = new XxxLock();
...
// 加锁,放在try之外,防止自身调用发生异常,没有加锁成功也进入finally解锁而抛异常
lock.lock();
// 加锁后紧跟着try块,防止两者中间有代码抛出异常,没有进入finally解锁
try {
...
} finally {
    // 在此处解锁
    lock.unlock();
}

【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。解锁方式同阻塞等待方式。

正例:

Lock lock = new XxxLock();
...
// 尝试获取锁,放在try块之外,防止没有获取到锁也进入finally块解锁而抛异常
boolean isLocked = lock.tryLock();
// 进入业务代码块前先判断是否持有锁
if (isLocked) {
    // 加锁后紧跟着try块,防止两者中间有代码抛出异常,没有进入finally解锁
    try {
    ...
    } finally {
        // 在此处解锁
        lock.unlock();
    }
}

【强制】并发修改同一记录时,为避免更新丢失,要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version作为更新依据

如果每次访问冲突概率小于20%,推荐使用乐观锁;否则使用悲观锁。

【强制】多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中一个任务没有捕获抛出的异常,其他任务便会自动终止运行。

【推荐】资金相关的金融敏感信息,使用悲观锁策略。

乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞。
乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成数据异常。

悲观锁遵循 一锁二判三更新四释放 的原则。

【推荐】使用CountDownLatch后,在线程退出前必须调用countDown方法。

注意,子线程抛出的异常不能在主线程的try块catch到。

【推荐】避免Random实例被多线程使用,JDK7之后可以使用ThreadLocalRandom。

java.util.Random和Math.random()均如此。
虽然共享该实例是线程安全的,但会因竞争同一个seed导致性能下降。

【推荐】通过双重检查锁实现延迟初始化,存在优化问题隐患。

反例:

public class LazyInitDemo {
    // 此处应添加volatitle关键字;
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized (this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }
    ...
}

【参考】volatile解决多线程内存不可见问题。对于一写多读可以解决变量同步问题。

在JDK8,count++操作推荐使用LongAdder,比AtomicLong性能更好(减少了乐观锁的重试次数)。

【参考】HashMap是线程不安全的;在容量不够进行resize时可能出现死锁,导致CPU飙升。

【参考】ThreadLocal对象使用static修饰,它无法解决共享对象的更新问题。

前后端规范

【强制】前后端交互的API,需要明确:协议、域名、路径、请求方法、请求内容、状态码、响应体。

路径:

  1. 代表一种资源,只能为名词,推荐使用复数;
  2. 全部小写,使用下划线分割单词;
  3. 进制携带表示请求内容类型的后缀,比如".json",".xml"等,通过accept头表达即可。

请求内容:

  1. URL带的参数必须无敏感信息或符合安全要求;
  2. body带参数时必须设置Content-Type头;

【强制】前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合{}。

减少前端琐碎的null判断。

【强制】服务端发生错误时,返回给前端的响应信息必须包含:HTTP状态码、errorCode、errorMessage、用户提示信息。

【强制】在前后端交互的JSON格式数据中,key必须为lowerCamelCase风格。

【强制】errorMessage用于错误定位,可以在前端输出在type="hidden"的文本类控件中,或者用户端的日志中;注意不要包含敏感信息。

【强制】对于需要使用超大整数的场景,服务端一律使用String字符串类型返回,禁止使用Long类型。

Java服务端如果直接返回Long整型数据给前端,JS会自动转换为Number类型。该类型是双精度浮点数,等同于Java的Double,在数值过大时会出现精度损失。

【强制】HTTP请求通过URL传递参数时,不能超过2048字节。

不同浏览器对于URL的最大长度限制和超长的处理策略都不同,该值为所有浏览器的最小值。

【强制】HTTP请求通过body传递内容时,必须控制长度,否则超长会导致后端解析出错。

例如,Nginx默认限制是1MB,tomcat默认限制为2MB。

【强制】在翻页场景中,用户没有输入参数,则前端返回第一页参数给后端;后端发现用户输入的参数大于最后一页,则直接返回最后一页。

【强制】服务器内部重定向必须使用forward;外部重定向地址必须使用redirect方式。

【推荐】服务器返回信息必须被标记是否可以缓存。

在http1.1中,Cache-Control头的s-maxage值告诉服务器进行缓存,时间单位为秒
正例:

response.setHeader("Cache-Control", "s-maxage=" + cacheSeconds);

【推荐】服务端返回的数据,使用JSON格式而非XML。

application/JSON是一种通用的MIME类型,具有实用、精简、易读的特点。

【推荐】前后端的时间格式统一为"yyyy-MM-dd HH:mm:ss",统一为GMT。

【参考】在接口路径中不要加入版本号,版本控制在HTTP头信息中体现,有利于向前兼容。

其他

【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。

正例:

// 定义为static final静态变量,以避免执行多次预编译
private static final Pattern pattern = Pattern.compile(regexRule);
 
private void func(...) {
    // 反例:Pattern.matches(regexRule, content) 则没有用到预编译
    Matcher m = pattern.matcher(content);
    if (m.matches()) {
        ...
    }
}

【强制】避免用Apache BeanUtils进行属性的copy。

Apache BeanUtils的性能较差。推荐使用:Spring BeanUtils,Cglib BeanCopier,注意均是浅拷贝。

【强制】velocity调用POJO类的属性时,直接使用属性名取值即可,模板引擎会自动按照规范调用其对应的getter/setter方法。

【强制】后端输送给页面的变量必须加 $!{var} 中间的感叹号。

如果var为null或者不存在,那么${var}会直接显示在页面上。

【强制】注意 Math.random() 返回的是0<=x<1的double类型,可能有除零异常。

【强制】如果需要整数类型的随机数,不要使用 Math.random() 去乘以10的次数,而是直接使用Random对象的 nextInt() 或者 nextLong() 。

【推荐】不要在视图模板中加入任何复杂的逻辑运算。

根据MVC理论,视图的职责只是展示。

【推荐】及时清理不再使用的代码段或配置信息。

【参考】避免出现重复的代码,即DRY(Don’t Repeat Yourself)原则。

日志规范

错误码

【强制】错误码制定原则:快速溯源、沟通标准化。

【强制】错误码不应体现版本号和错误等级信息。

错误码以不断追加的方式进行兼容。
错误等级由日志和错误码本身的释义来决定。

【强制】全部正常,但不得不填充错误码时返回5个0:00000。

【强制】错误码为字符串类型,共5位,分为2个部分:1位标识错误产生来源 + 4位数字编号标识详细分类原因。

错误来源分为A/B/C:

  • A:用户错误,比如参数错误等;
  • B:当前系统错误,比如业务逻辑处理出错等;
  • C:依赖的服务,比如调用上游服务超时等;

4位数字编号从0001到9999,大类之间的步长间距预留100。数字编号不与公司业务架构、组织架构挂钩,表示错误的详细分类。
错误码的后三位编号与HTTP状态码没有任何关系。

错误码和业务架构、组织架构无关,可以通用;当有相近意义的错误码时要复用,避免随意新增错误码。

具体错误码参考前言中的文档的具体内容。

【强制】错误码不能直接输出给用户作为提示信息使用。

【推荐】错误码之外的业务独特信息由错误信息errorMessage来承载;错误码本身不涵盖具体业务信息。

【推荐】在获取第三方服务错误码时,向上抛出时应该做转义,例如C转B,且在错误信息中带上原有的第三方错误码。

日志

【强制】应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架(SLF4J、JCL)中的API。

【强制】日志文件推荐至少保存15天;对于当天日志,以 {logname}.log 保存,过往日志以 {logname}.log.{yyyy-MM-dd} 保存。

保存15天便于发现以“周”为频次发生的问题。

【强制】根据国家法律,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存的日志不少于6个月,并且进行网络多机备份。

【强制】应用日志命名推荐 {应用名}_{类型}_{日志描述}.log 的格式。

类型举例:stats、monitor、visit、error等。

【强制】在日志输出时,字符串变量之间的拼接使用{}占位符方式。

使用占位符可以有效提升性能。

【强制】对于 trace / debug / info 级别的日志输出,必须进行日志级别的开关判断。

主要目的是为了防止参数中有字符串拼接、方法调用等额外的运算造成性能浪费。

正例:

if (logger.isDebugEnabled()) {
    logger.debug("debug info.");
}

【强制】避免重复打印日志,浪费磁盘空间,务必在 log4j.xml 中设置additivity=false。

【强制】生产环境禁止使用 System.out/err 输出日志,禁止使用 e.printStackTrace() 打印异常堆栈。

标准日志输出/标准错误输出文件每次Jboss重启时才滚动,容易在日志量大时造成单个文件大小超过操作系统大小限制。

【强制】异常信息应该包括2类信息:案发现场信息和异常堆栈信息。

【强制】日志打印时禁止直接用JSON工具将对象转换成String。

存在抛出异常的风险,会造成业务流程中断。

【推荐】可以使用warn日志级别来记录用户输入参数错误的情况,便于追溯。

不推荐此场景使用error级别,因为error级别只记录系统逻辑出错、异常或者重要的错误信息。

【推荐】生产环境禁止输出debug日志;有选择地输出info日志。?

单元测试

【强制】好的单元测试必须遵守AIR原则。

AIR原则是指:

  • A:Automatic 自动化
  • I:Independent 独立性
  • R:Repeatable 可重复

【强制】自动化:单元测试应该使用assert来验证,而非交互式的打印信息来人肉验证。

【强制】独立性:单元测试用例之间不能相互调用或者依赖执行的先后次序。

【强制】可重复:可以重复执行,不能受到外界环境的影响。

【强制】要保证测试粒度足够小,一般到方法级别,有助于精确定位问题。

【强制】核心业务、核心应用、核心模块的增量代码确保单元测试通过。

【强制】单元测试代码必须写在如下目录:src/test/java,不允许写在业务代码目录下。

【推荐】单元测试的基本目标:语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率都要达到100%。

【推荐】编写单元测试遵守BCDE原则,以保证交付质量。

BCDE原则:

  • B:Border,边界测试。
  • C:Correct,正确的输入,并得到预期的结果。
  • D:Design,与设计文档相结合编写单元测试。
  • E:Error,强制错误信息输入,并得到预期的结果。

【推荐】和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。

【推荐】对于不可测的代码在适当时机做必要的重构,使代码变得可测。

【推荐】在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好覆盖所有测试用例。

【推荐】单元测试作为一种质量保证手段,建议在项目提测前完成,不建议项目发布后再来补充。

安全规范

【强制】隶属于用户个人的页面或者功能必须进行权限控制校验。

【强制】用户敏感数据禁止直接展示,需要进行脱敏。

【强制】用户输入的SQL参数严格使用参数绑定或者METADATA字段值限定,以防止SQL注入;禁止字符串拼接SQL访问数据库。

【强制】用户请求传入的任何参数必须做有效性验证。

参数引起的常见的问题有:

  • page size过大导致内存溢出
  • 恶意order by导致数据库慢查询
  • 缓存击穿
  • SSRF
  • 任意重定向
  • SQL注入
  • Shell注入
  • 反序列化注入
  • 正则输入源串拒绝服务ReDOS

【强制】表单、AJAX提交必须执行CSRF安全验证。

【强制】URL外部重定向传入的目标地址必须执行白名单过滤。

【强制】在使用平台资源,如短信、邮件、电话、下单、支付,必须实现正确的防重放的机制,如数量限制、疲劳度限制、验证码校验,从而避免被滥刷而导致资损。

比如,注册时发送验证码到手机,如果没有防重放限制,可能会被恶意利用来骚扰其他用户,并造成资金损失。

【推荐】发帖、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过滤等风控策略。

MySQL规范

建表规范

【强制】表名、字段名必须使用小写字母或数字,并使用下划线分割;禁止数字开头,禁止2个下划线中间只有数字。

MySQL在Windows下不区分大小写,但在Linux下默认是区分大小写的。

【强制】表名不使用复数名词。

【强制】禁用保留字来命名表名、字段名,如desc、range、match、delayed等。

【强制】表达是与否概念的字段,必须使用is_xxx的方式命名,数据类型是unsigned tinyint(1表示是,0表示否)。

【强制】唯一索引名为uk_字段名;普通索引名则为idx_字段名。

【强制】小数类型为decimal,禁止使用float和double。

float和double都存在精度损失的问题。

【强制】如果存储的字符串长度几乎相等,则使用char定长字符串类型。

【强制】varchar是可变长字符串,不预先分配存储空间,长度不要超过5000;如果长度超过5000,则定义字段类型为text,独立出来一张表,用主键来对应,避免影响其他字段索引效率。

【强制】表必备三字段:id,create_time,update_time。

其中,
id为主键,类型为bigint unsigned、单表时自增、分表时不自增且要程序确保全局唯一;
create_time、update_time分表表示创建和更新时间,类型为date_time,前者主动创建,后者被动式更新。

【推荐】表的命名最好遵循“业务名称_表的作用”.

【推荐】库名与应用名尽量一致。

【推荐】如果修改字段含义或者对字段的状态值进行追加时,需要即时更新注释。

【推荐】字段允许适当冗余,以提高性能,但是必须考虑数据同步的情况。

冗余字段应遵循:

  1. 不是频繁修改的字段。
  2. 不是唯一索引的字段。
  3. 不是varchar超长字段,更不能是text字段。

【推荐】单表行数超过500万行或者单表容量超过2GB,才推荐进行分库分表。

【推荐】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。

索引规范

【强制】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。

原因:

  1. 唯一索引对insert速度的影响可以忽略,但提高查找速度是非常明显的;
  2. 在数据库层面对业务唯一特性进行强约束。

【强制】超过3个表禁止join;需要join的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段有索引。

【强制】在varchar字段上建立索引时,必须制定索引长度,没必要对全字段建立索引,而是要根据实际文本区分度来决定索引长度。

区分度计算方法:count(distinct left(列名, 索引长度))/count(*)

【强制】页面搜索禁止左模糊或者全模糊,如果需要则要使用搜索引擎来解决。

因为索引文件具有B-Tree的最左前缀匹配特性;如果左边的值未确定则无法使用此索引。

【推荐】如果有order by的场景,注意利用索引的有序性。

正例:

# 以下会使用索引:a_b_c
where a=? and b=? order by c;

【推荐】利用覆盖索引来进行查询操作,来避免回表操作。

InnoDB聚集索引的叶子节点存储行记录,因此, InnoDB有且只有一个聚集索引。

  1. 如果表定义了主键,则主键就是聚集索引;
  2. 如果表没有定义主键,则第一个not NULL unique索引是聚集索引;
  3. 否则,InnoDB会创建一个隐藏的row-id作为聚集索引;

回表查询是指需要扫描2遍索引树:先通过普通索引定位主键值,再通过聚集索引定位行记录。

覆盖索引是指,只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。
explain的输出结果Extra字段为Using index时,就是触发了索引覆盖。

【推荐】利用延迟关联或者子查询优化超多分页场景。

MySQL并不是跳过offset行,而是读取offset+N行,然后舍弃前offset行,返回N行。
所以,当offset特别大的时候,效率就非常的低下。

正例:

# 先快速定位需要获取的id段,再关联:
select a.* From 表1 a, (select id from 表1 where 条件 limit 100000, 20) b where a.id=b.id;

【推荐】SQL性能优化目标:至少要达到range级别,要求是ref级别,如果可以是const最好。

【推荐】建组合索引的时候,区分度最高的在最左边。

例外:存在非等号和等号混合判断条件时,在建组合索引时,要把等号条件的列前置。

【推荐】防止因字段类型不同造成的隐式转换,导致索引失效。

SQL规范

【强制】不要使用count(列名)或count(常量)来替代count(*)。

count(*)就是SQL92定义的标准统计行数的语法,跟数据库无关,也会统计值为NULL的行。
而count(列)不会统计此列为NULL值的行。

【强制】count(distinct col)计算该列除NULL之外的不重复数量。注意count(distinct col1,col2)如果其中一列全为NULL,那么即使另一列有不同的值,也返回为0。

【强制】当某一列的值全是NULL时,count(col)的返回结果为0;但sum(col)的返回结果为NULL,需要注意NPE问题。

正例:

// 如下查询方式可以避免NPE
select ifnull(sum(col), 0) from table;

【强制】使用ISNULL()来判断是否为NULL值。

NULL与任何值的直接比较结果都为NULL,例如NULL=NULL,NULL<>1的返回结果都是NULL。
并且isnull(col) 比 col is null的执行效率更高。

反例:

select * from table where col1 is null and col2 is not null;

【强制】对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名或表名进行限定。

如果操作列没有限定表的别名或表名,且在多个表中存在时,会抛异常。

【强制】在代码中写分页查询逻辑时,若count为0应直接返回,避免执行后面的分页语句。

【强制】不得使用外键与级联更新,一切外键概念必须在应用层解决。

外键与级联更新适用于单机低并发,不适合分布式、高并发集群;
级联更新是强阻塞,存在数据库更新风暴的风险;
外键影响数据库的插入速度;

【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。

【强制】在线上执行数据更新前,要先select,确认无误才能执行数据更新。

【推荐】SQL语句中表的别名前加as,并以t1、t2、t3…的顺序依次命名。

【推荐】in操作能避免就避免;否则,需要评估in的集合数量,控制在1000个以内。

【参考】存储采用utf8字符集,字符计数方法要注意length()得到的是存储长度不是汉字个数。

select length(“中国汉字”); 返回为12
select character_length(“中国汉字”); 返回为4

【参考】需要存储表情,选择utf8mb4进行存储。

【参考】不建议使用truncate语句。

truncate语句速度快,使用的系统和事务日志资源少;但是它无事务、不触发trigger,有可能造成事故,故不推荐。

ORM规范

【强制】在表查询中,禁止使用 * 作为查询的字段列表,需要哪些字段必须明确写明。

使用 * 的坏处:

  1. 增加查询分析器的解析成本;
  2. 增减字段容易与resultMap配置不一致;
  3. 多余字段增加网络消耗,尤其是text类型;

【强制】POJO类的布尔属性不能加is,而数据库字段必须加is_,要求在resultMap中进行字段与属性之间的映射。

【强制】sql.xml配置中参数注意:#{},#param#;但不要使用${},容易出现SQL注入。

【强制】iBatis自带的queryForList(String statementName, int start, int size)不推荐使用。

其原理类似offset查询机制,会读取所有记录后再取子集,可能会造成OOM。

【强制】不允许直接拿HashMap与HashTable作为查询结果集的输出。

可能会因为数据版本不同而出现转换类型的不同。

【强制】更新数据表记录时,必须同时更新记录的update_time为当前时间。

【推荐】不要写一个大而全的数据更新接口,传入为POJO类,更新所有字段。

不要更新无改动的字段:

  1. 易出错;
  2. 效率低;
  3. binlog增加存储;

【参考】@Transactional事务不要滥用。

  1. 事务会影响数据库的QPS;
  2. 使用事务的地方需要考虑各方面的回滚方案,如缓存回滚、消息补偿等;

工程规范

应用分层

【推荐】从上至下建议的层级如下:

终端显示层 / 开放API层
请求处理层(Web层)
业务逻辑层(Service层)
通用逻辑层(Manager层)
数据持久层(DAO层) / 第三方服务
数据存储系统 / 外部数据接口

详见前言的文档内容。

【推荐】分层异常处理规范如下:

DAO层:异常类型很多,无法用细粒度异常进行catch;直接使用Exception来catch并抛出异常到上层。不需要打印日志,因为在Manager/Service层一定会捕获抛出的日常并打印日志。
Service层:出现异常时,必须记录日志,并尽可能带上案发现场信息,如调用参数。
Manager层:如果与Service同机部署,日志方式与DAO层处理一致;如果是单独部署,则采用与Service层一致的处理方式。
Web层:将异常转化为用户可以理解的错误提示信息。
开放API层:将异常转化为错误码和错误信息方式返回。

【推荐】分层领域模型规范如下:

DO:数据对象,DAO层向上传输数据的对象。
DTO:数据传输对象,Service层或Manager层向外传输的对象。
BO:业务对象,Service层输出的封装业务逻辑的对象。
VO:视图对象,Web层向终端显示层传输数据的对象。
Query:数据查询对象,各层接收上层的查询请求。

二方库/三方库规范

【强制】二方库版本号命名方式:主版本号.次版本号.修订号。

主版本号:产品方向改变,或者大规模API不兼容,或者架构不兼容升级。
次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的API不兼容修改。
修订号:保持完全兼容性,修复BUG、增加次要功能特性等。

版本号从 1.0.0 开始。

【强制】线上应用不允许依赖SNAPSHOT版本包。

【强制】正式发布的二方库不允许版本号覆盖升级。

【强制】二方库里可以定义枚举类型,参数可以使用枚举类型;但是接口返回值不允许使用枚举类型或者包含枚举类型的POJO对象。

由于升级原因,导致双方的枚举类不尽相同,在接口解析时会出现反序列化异常。

【强制】依赖于一个二方库群/三方库群时,必须定义一个统一的版本变量,避免版本号不一致。

【强制】禁止在多个子项目的pom依赖中对同一个 GroupId/ArtifactId 出现不同Version。

【推荐】工具类在二方库/三方库已经提供的,不要在本应用中编程实现。

  • json操作:jackson
  • md5操作:commons-codec
  • 工具集合:Guava包
  • 数组操作:ArrayUtils
  • 集合操作:CollectionUtils
  • 还有NumberUtils,DateFormatUtils,DateUtils,StringUtils等,优先使用org.apache.commons.lang3这个包下的。原因是lang包下的是从JDK1.2开始支持,很多新特性不支持,例如泛型。

【推荐】pom文件中的所有依赖声明放在dependencies语句块中,所有版本仲裁放在dependencyManagement语句块中。

【参考】二方库发布遵循的原则如下:

  • 精简可控原则:移除一切不必要的API和依赖。
  • 稳定可追溯原则:每个版本的变化应该被记录,维护人和源码地址等,要能方便查到。

中间件规范

【强制】任何操作,都是先保存数据库成功后,再进行缓存的新增、更新、删除操作。

【强制】RPC调用必须有超时设置。

【推荐】了解每个服务大致的平均耗时,通过独立线程池配置,将较慢的服务与主线程池隔离开,防止各个服务线程同归于尽。

服务器规范

【推荐】高并发服务器调小TCP协议的time_wait超时时间。

操作系统默认数据为240秒,可以修改/etc/sysctl.conf文件去修改该缺省值:
net.ipv4.tcp_fin_timeout = 30

【推荐】调大服务器所支持的最大文件句柄数。

主流操作系统是将TCP/UDP连接采用与文件一样的方式去管理,即每一个连接对应一个文件句柄fd。
linux系统一般默认所支持最大fd数量为1024,可以根据内存大小进行调高。

【推荐】给JVM环境参数设置 -XX:+HeapDumpOnOutOfMemoryError 参数,让JVM碰到OOM场景时可以输出dump信息。

【推荐】在线上生产环境,JVM的Xms和Xmx设置一样大小的内存容量,避免在GC后调整堆大小带来的压力。

设计规范

【强制】存储方案和底层数据结构的设计也需要在设计评审中通过,并沉淀为文档。

【强制】在需求分析阶段,如果与系统交互的User超过一类并且相关的 Use Case 超过5个,使用用例图来表达更加清晰的结构化需求。

【强制】如果某个业务对象的状态超过3个,使用状态图来表达并且明确状态变化的各个触发条件。

【强制】如果系统中某个功能的调用链路上的涉及对象超过3个,使用时序图来表达并且明确各调用环节的输入与输出。

【强制】如果系统中模型类超过5个,并且存在复杂的依赖关系,使用类图来表达并且明确类之间的关系。

【强制】如果系统中超过2个对象之间存在协作关系,并且需要表示复杂的处理流程,使用活动图来表示。

【强制】系统设计时要准确识别出弱依赖,并针对性地设计降级和应急预案,务必保证核心系统正常可用。

系统依赖的第三方服务被降级或者屏蔽后,依然不会影响主干流程继续进行,仅影响信息展示、或消息通知等非关键功能,那么这些服务称为弱依赖。

【推荐】系统架构设计时明确以下目标:

  • 确定系统边界。确定系统在技术层面上的做与不做。
  • 确定系统内模块之间的关系。如模块之间的依赖关系、模块的宏观输入和输出。
  • 确定指导后续设计与演化的原则。使后续的子系统或模块设计可以在一个既定的框架内和技术方向上继续演化。
  • 确定非功能性需求。如:安全性、可用性、可扩展性等。

【推荐】需求分析与系统设计在考虑主干功能的同时,需要充分评估异常流程和业务边界。

【推荐】类在设计与实现时要符合单一原则。

【推荐】谨慎使用继承的方式来进行扩展,优先使用聚合/组合的方式来进行实现。

【推荐】系统设计阶段,根据依赖倒置原则,尽量依赖抽象类与接口,有利于扩展与维护。

【推荐】系统设计阶段,注意对扩展开放,对修改闭合。

【推荐】系统设计阶段,将共性业务或公共行为抽取出来公共模块/配置/类/方法等,避免重复代码情况。

【参考】设计文档的作用是明确需求、理顺逻辑、后期维护、次要目的用于指导编码。

【参考】设计的本质是识别和表达系统难点。

准确地表达系统难点需要具备如下能力:

  • 表达规则和表达工具的熟练性;
  • 抽象思维和总结能力的局限性;
  • 基础知识体系的完备性;
  • 深入浅出的生动表达力;

【参考】可扩展性的本质是找到系统的变化点,并隔离变化点。

【参考】敏捷开发不等于没有设计。

敏捷开发是快速交付迭代可用的系统,省略了多余的设计方案,摒弃了传统的审批流程,但核心关键点上的必要设计和文档沉淀是需要的。

【参考】代码即文档的观点是错误的,清晰的代码只是文档的某个片断,而不是全部。

代码的深度调用,模块层面上的依赖关系网,业务场景逻辑,非功能性需求等问题都是需要相应的文档来完整地呈现的。

【参考】在做无障碍产品设计时,需要考虑到:

  • 所有可交互的控件元素必须能被tab键聚焦,并且聚焦顺序符合自然操作逻辑。
  • 用于登陆校验和请求拦截的验证码均需提供图形验证以外的其他方式,如语音验证。
  • 自定义的控件类型需明确交互方式。

附录

一、名词参考

名称含义
POJODO/DTO/BO/VO的统称
DO数据对象,xxxDO中xxx为数据表名
DTO数据传输对象,xxxDTO中xxx为业务领域相关的名称
VO展示对象,xxxVO中xxx为网页名称
BO业务对象,封装了业务逻辑的对象
一方库本工程内部子项目模块库
二方库公司内部发布的库
三方库公司之外的开源库
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页