介绍
编程 = 算法 + 数据结构
数据结构:一个提供一系列(接口)输入得到指定输出(数据格式)的一个数据容器,可以通过选择特定数据结构更好的解决业务逻辑。数据结构都已经有一系列现成的实现,开发过程中我们只要根据情况选择合适的实现就可以了。
算法:依赖数据结构解决实际业务,开发中我们需要自定义实现的。开发过程中,对于简单问题我们也需要满足算法的以下特性:
-
正确性
-
可读性
-
高效性
-
健壮性
还需关注:开发效率。
这五个方面就是我们编程中需要重点关注的环节。
而可靠性,易用性,可扩展性、可复用性、兼容性、可移植性等特性个人认为是健壮性的延伸,大多是在框架层面会考虑,属于业务框架选型范畴。
正确性
我们都是通过业务完成度来确定正确性。业务是一切的根基,如果业务理解出现偏差导致南辕北辙,做的都是白用功。
我们需要抓住业务线的两端(是什么,为什么),通过UML来解决需求到代码的映射问题。开发就是通过类,实例变量,方法,方法参数,方法返回值来实现业务逻辑。其中的组合关系涉及设计模式,不进行展开。
-
我们可以通过TDD(Test-Driven Development)测试驱动开发,来确保正确性。
通过测试案例使用断言预设返回类型,判断数据是否正确
-
TDD编程过于繁琐复杂,通过编写测试来确保正确,但是业务要求变更或测试案例写错而重新编写测试会严重影响开发效率。如果使用postman手动测试接口也不方便,我们可以使用idea插件RestFul提供的脚本来进行测试。
可读性
使用快捷键 ctrl + alt+ l 对代码格式化
1. 类名,变量名,方法名是代码的直接体现,名称要体现业务关系,还要保证符合代码规范。
- 类名要首字母大写,兼顾驼峰命名,通过后缀区分不同业务。
- 方法名首字母小写,动词在前,注意动词的各种意涵。
- 静态常量和枚举全大写用下划线分割,其他变量首字母小写,骆驼法则。
-
注释一定要写,接口中的方法添加注释,实现类方法上就不需要在添加了。
-
注意使用todo,确保不会遗留待开发内容。
-
减少代码行数和每行长度,可以增强可读性
-
运行各种工具类简化代码,如
if(null != list && list.size > 0){}
//可以写成
if (CollectionUtils.isNotEmpty(list)) {}2. 如果if while for等语句只有一行,**可以**省略大括号{} ```java if (CollectionUtils.isNotEmpty(list)) list.addAll(checkedNotBindingAmount.get(checked)); else list = checkedNotBindingAmount.get(checked); ``` 3. return 中包含业务逻辑,而不用单独处理完再返回 ```java //判断是否是奇数 public String oddOrEven(int a) { if ((a & 1) == 1) {//位运算,考虑到为负数的情况 return "奇数"; } else { return "偶数"; } } //可以写为 public String oddOrEven(int a) { return (a&1) == 1?"奇数":"偶数"; } ``` 4. 对变量的有效性判断可以统一提取到代码块最前方,这样后面就不用再判断 ```java public Result bindingInvoice(BindingInvoiceReq bindingInvoiceReq) { if (CollectionUtils.isEmpty(bindingInvoiceReq)){ return new Result("bindingInvoiceReq 不能为空"); } } ``` 5. 使用断言和枚举通过自定义异常返回业务失败提示,对于异常,要进行统一处理,dubbo调用可以直接抛出异常,对外的http接口则需要对异常封装成Result对象传输。 ```java public interface IResultEnum{//返回枚举的接口,枚举中有哪些field /** * 获取返回码 * * @return 返回码 */ String getCode(); /** * 获取返回信息 * * @return 返回信息 */ String getMessage(); } public interface Assert extends IResultEnum{//继承IResultEnum /** * 创建异常 * * @param args * @return */ default BizException newException(Object... args) { return new BizException(this.getCode(), MessageFormat.format(this.getMessage(), args)); } //obj为空时,抛出异常 default void assertNotNull(Object obj, Object... args) { if (obj == null) throw newException(args); } //do something } @Getter @AllArgsConstructor public enum ResultEnum implements Assert {//枚举类 PARAMETERS_IS_NULL("6000", "参数{0}不能为空!"), //do something /** * 返回码 */ private String code; /** * 返回消息 */ private String message; } ``` ```java public Result bindingInvoice(BindingInvoiceReq bindingInvoiceReq) { //不再需要写判断 ResultEnum.PARAMETERS_IS_NULL.assertNotNull(bindingInvoiceReq, "bindingInvoiceReq"); } ``` 6. 对于boolean类型变量不要再写condition == true ```java public void method(){ boolean condition = false; //do something if(condition == false) return; //写成 if(!condition) return; } ``` 7. 减少if,for嵌套,可以巧妙使用if+标签+break优雅跳出循环 ```java public void method(){ boolean condition1 = false; //do something if(condition1){ boolean condition2 = false; //do something if(condition2){ //do something } } //可以写成 if(condition1) flag1:{ boolean condition2 = false; //do something if(!condition2)//不满足条件,提前返回到上层或者结束代码,我自己成为否定前移 break flag1; //do something } } ``` 8. 尽量不要用参数来带回方法运算结果(方法的值传参和引用传参),多使用返回值 ```java List<String> tableNos = new LinkedList<>(); //解析bindingOrderDetailReqs对象,返回tableNos parseBindingOrderDetailReqs(bindingOrderDetailReqs, tableNos); //可以写成 List<String> tableNos = parseBindingOrderDetailReqs(bindingOrderDetailReqs); ``` 9. 使用java新特性 ```java List<String> orderDetailReceiveNos = new ArrayList(); //判断orderDetailReceiveNos集合中是否有"RE"开头的字符串 boolean hasChecked = false; for(String orderDetailReceiveNo:orderDetailReceiveNos){ if(orderDetailReceiveNo.startsWith("RE")){ hasChecked = true; break; } } //可以写成 boolean hasChecked = orderDetailReceiveNos.stream().anyMatch(string -> string.startsWith("RE")); List<String> agreementNos = new ArrayList(); //打印合同号 for(String agreementNo:agreementNos){ System.out.print("'"+no+"',") } //可以写成 agreementNos.forEach(no -> System.out.print("'"+no+"',")); agreementNos.forEach(System.out::print); ``` 10. 使用重构快捷键,将重复代码,过长方法进行重构。  idea对应快捷键 `Refactor` 重构 `Signarture` 签名 `Extract` 提取 `Field` 属性 `Constant` 常量 `Method` 方法 `Parameter` 参数 `Surround` 环绕 重命名文件、方法、属性等(Rename):Shift+F6,同时被修改处在其他被调用的地方也会随之更改,修改文件名,如类名,右键当前类文件 -> Refactor -> Rename,便会看到快捷键提示 Shift+F6 重构类、方法(Change Signarture):Ctrl+F6 提取当前选择为变量(Extract Variable):Ctrl+Alt+V 提取当前选择为类的私有属性(Extract Field):Ctrl+Alt+F 提取当前选择为常量(Extract Constant):Ctrl+Alt+C 提取当前选择为方法(Extract Method):Ctrl+Alt+M 提取当前选择为方法参数(Extract Parameter):Ctrl+Alt+P 提取代码块至 if、try 等结构中(Surround With):Ctrl+Alt+T 创建模块文件等(New):Ctrl+Alt+N 创建测试用例(Test):Ctrl+Shift+T 重构菜单(Refactor for this):Ctrl+T 重构一切:Ctrl+Shift+Alt+T 11. 选择合理的设计模式,提高类关联层次。 12. 日志对http请求方法必须记录其请求和返回数据,对异常必须打印。 ```java @GetMapping("/getDefaultAgreements") public Result getDefaultAgreements(String supplyName, String personCode){ logger.info("用户 {} 请求 url: {} 根据供应商名称返默认的回合同列表,查询参数 is {}", personCode, "/getDefaultAgreements", supplyName); //do something logger.info("用户 {} 请求 url: {} 根据供应商名称返默认的回合同列表结束,返回 is {}", personCode, "/getDefaultAgreements", JSON.toJSONString(defaultAgreements)); return defaultAgreements; } ```
-
高效性
提高代码运行效率,主要分析代码的时间复杂度。
代码方面对于嵌套循环,若能减少一层嵌套,其内循环执行的次数将显著减少,效率将提高。
数据库方面的优化可以解决我们70%的效率问题,需要重点关注。
数据库设计层面:
-
相关模块的表名与表名之间尽量提现join的关系,如user表和user_login表。
-
多对一关系,在多方添加字段,维护一方的数据。
srm_pur_order_agreement表对应多个srm_pur_order,应该在srm_pur_order上维护gareement上的信息。 -
建表时关于主键:(1)强制要求主键为id,类型为bigint,且为auto_increment (2)标识表里字段不要设为主键,建议设为其他字段如user_id,rder_id等,并建立唯一索引。
srm_pur_order_agreement中的agreement_no合同编号应该是唯一索引。
-
反范式设计:把经常需要join查询的字段,在其他表里冗余一份。
如usrm_pur_order_detail表中可以添加srm_pur_order_agreement中的agreement_no合同编号字段,减少join查询。
-
在建立索引时,多考虑建立联合索引,并把主要列放在前面。
比如我们合同有分组,agreement_no可以拆成agreement_group+agreement_number两个字段,建立联合唯一索引。
-
表中所有字段必须都是NOT NULL属性,业务可以根据需要定义DEFAULT值。因为使用NULL值会存在每一行都会占用额外存储空间、数据迁移容易出错、聚合函数计算结果偏差等问题。
-
时间类型选取timestamp。因为datetime占用8字节,timestamp仅占用4字节,但是范围为1970-01-01 00:00:01到2038-01-01 00:00:00。需要超过2038年的,选用int来存储时间,使用SQL函数unix_timestamp()和from_unixtime()来进行转换。
-
存储金钱的字段,用int,程序端乘以100和除以100进行存取。因为int占用4字节,而double占用8字节,空间浪费。
我们存储金额都是用decimal,float占4个字节,double占8个字节,decimail(M,D)占M+2个字节。decimal(8,2)就占用10个字节,而其表示金额10的7次方(直到百万金额),使用int可以表示两千万的金额只用4个字节,而且方便计算。
-
业务中选择性很少的状态status、类型type等字段推荐使用tinytint或者smallint类型节省存储空间。
mysql中int、bigint、smallint 和 tinyint的占用大小
-
表和列的名称必须控制在32个字符以内,表名只能使用字母、数字和下划线,一律小写。
-
创建表时必须显式指定字符集为utf8或utf8mb4(最好)。存储引擎一律使用InnoDB。
-
建议对表里的blob、text等大字段,垂直拆分到其他表里,仅在需要读这些对象的时候才去select。
-
业务中IP地址字段推荐使用int类型,不推荐用char(15)。因为int只占4字节,可以用如下函数相互转换,而char(15)占用至少15字节。select inet_aton(‘192.168.2.12’)返回3232236044 select inet_ntoa(3232236044)返回192.168.2.12,而且使用int还能快速查找同一ip段的数据,如查询"192.168.2"这个ip段的数据
select ip from table where ip between inet_aton(‘192.168.2.0’) and inet_aton(‘192.168.2.255’)
-
主键的名称以“pk_”开头,唯一键以“uk_”或“uq_”开头,普通索引以“idx_”开头,一律使用小写格式,以表名/字段的名称或缩写作为后缀。
-
索引类型必须为BTREE
sql编写层面:
-
连接数据库的次数要进行控制,能够批量查询的,不要用for+单数据查询。
-
对sql查询数据进行简单封装处理的,可以使用sql完成。
-
sql不要使用嵌套查询,多使用连接查询。
-
不要使用select *
-
SELECT语句不要使用UNION,推荐使用UNION ALL,并且UNION子句个数限制在5个以内。因为union all不需要去重,节省数据库资源,提高性能。
-
索引列不要使用函数或表达式,否则无法利用索引。如where length(name)='admin’或where user_id+2=10023。
-
减少使用or语句,可将or语句优化为union all
-
事务里更新语句尽量基于主键或unique key,如update … where id=XX; 否则会产生间隙锁,内部扩大锁定范围,导致系统性能下降,产生死锁。
-
禁止联表更新语句。
健壮性
对于整个项目来说,更能多的是对类间关系的管理。
开发时注意符合六大设计原则
- 开闭原则:注意业务扩展时,多扩展源码,而不是修改源代码
- 依赖倒置原则:抽象不应该依赖细节;细节应该依赖抽象。针对接口编程,不要针对实现编程。
- 单一职责:一个类/接口/方法只负责一项职责。
- 迪米特原则:一个对象对自己需要耦合关联调用的类应该知道的少;这会导致类之间的耦合度降低,每个类都尽量减少对其他类的依赖。出现在成员变量、方法输入、输出参数中的类称为朋友关系类,而出现在方法体内部的类不属于朋友类,所以这类实体类也是我们要避免的。
- 接口隔离原则:一个类对一个类的依赖应该建立在最小的接口上。建立单一的接口,不要建立庞大臃肿的接口。
- 里式替换法则:子类可以扩展父类的功能,但不能改变父类原有的功能。
开发效率
确定需求,划分好模块边界。
对于通用模块,进行统一处理。
统一团队风格,多沟通协作。
总结
有道无术,术尚可求也,有术无道,止于术。
编程是一个沉淀的过程,技术可以学习,s希望大家对技术精益求精,同进退共成长。