最近学到的一些技巧
功能设计的技巧(方法),如何设计一个类,如何设计一个表。代码层面一些常见的优化。spring断言。一些工具。
一、功能设计的技巧(方法)
1.必须先理解需求
个人认为,设计一个功能,永远是从读懂需求开始的。如果编码时没有彻底理解需求,就会遇到很多不必要的麻烦(漏掉一些细节/写出来的东西和需求有出入),导致返工。
2.“自上而下”和“自下而上”
理解完需求后,可以开始设计功能。设计流程主要有“自上而下”/“自下而上”。
“自上而下”就是先设计好入口类(controller),往下设计调用的接口(service),写实现类(impl),设计返回的封装类(dtos),设计需要使用的实体类(pojo),在这个过程中搞清楚自己需要些什么信息,总结起来设计表结构,写获取信息的方法(dao)。当然,用一些类似generator的工具可以同时把设计表、实体类、dao的事情全干了。
“自下而上”就是先设计表结构,再把dao、pojo都弄好。之后设计service,写impl,设计dtos,最后补充controller,就把整个流程补充完整了。
各人有各人的习惯,个人认为这两种比较靠谱。
3.我的习惯
(1)设计入口方法
我一般会先设计入口方法(用spring boot来举例,就是设计controller中的入口方法)。设计入口方法有三个关键点:我要通过什么路径来调用这个方法(调用方法)、我要提供什么参数(提供参数)、我收到的返回值是什么(返回值)。
(2)设计接口
写完controller,就要设计service层的接口,供controller调用。在设计接口时,一定要注释每个接口方法,写清楚需要的参数类型、返回类型、这个方法的使用说明(比如适用于什么情况、参数的限制,等等),方便后人调用时,能直接看到接口方法的详细信息。
(3)测试驱动(见仁见智)
写完了接口,我们应该马上写实现类吗?如果项目要求写单元测试,最好还是用测试驱动开发(TDD)的模式,先把单元测试写了。
写单元测试的时候,可以把边界值、特殊值、特殊情况全都搞清楚,写逻辑的时候就能避开雷区。如果不要求写单元测试,我们还是要假设所有的特殊情况,并且记录下来(手写、txt都行,不要太相信自己的记性)
基本上,我接触过的所有程序员都是讨厌写单元测试的,理由也很简单:“我怎么可能写错代码?让我再写一堆代码验证1 + 1 = 2,不是浪费我时间吗?(更何况有测试同学把关)”
个人认为,程序员的工作就是交出健壮的代码。只要你能保证代码没问题,谁管你写不写单元测试呢?这里要强调一个很严肃的问题:测试也是人。我们写完代码肯定要自己先测,把你想到的情况都测了,才交给测试,帮你找剩下的疏漏的错误,切忌一写完简单测测就扔给测试(相当于把代码质量交给测试把关了),结果测试找出来一大堆显式错误,浪费大家时间。如果这种事经常发生,别人肯定会觉得你的工作态度有问题。
(4)实现类
接下来写实现类。实现类是由多个实现方法组成的,每个实现方法包括了多个模块,每个模块实现了一小部分的逻辑(功能),等待我们逐个实现。在实现整个方法之前,最好先写上注释,这个模块的作用是什么,这个过程有点像写伪代码。如果要写一个购买商品的方法,我可能会这样准备:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public ReturnPojo buy(int goodsId, int num) { // 通过goodsId获取商品信息 // 如果商品不存在,报错 // 检查num参数是否合规 // 如果存在,检查剩余库存 // 库存不足以购买,报错 // 计算购买的总额 // 通过token获取用户信息,获取用户金额 // 金额不足以购买,报错 // 扣款,入库 // 记录购买信息,入库 // 正确返回 return null; } |
这些注释最好对照着单元测试的注意事项来写。写完注释之后,把这些逻辑编码“翻译”出来即可。
在编码的过程中,我们要有“组件化”开发的思想,每个模块都是一块组件。比如“扣款,入库”这一步操作,一看就是经常使用的方法,我们可以找找相似的代码,直接拿过来用即可。
如果没有相似的代码,就要单独写成一个组件,方便后人调用。
如果某个组件非常复杂(让整个方法臃肿)/重复出现(出现冗余),我们就要考虑把这个组件抽出来写成一个私有方法,优化代码结构。
(5)设计dto、pojo、dao
随着每个模块的编写,我们逐渐清楚了自己需要什么样的类。这时候,我们先设计展示用的dto(根据前端需要的东西,我们就可以设计dto),再根据dto和逻辑,设计pojo,最后根据pojo,设计dao。这就把整个代码逻辑补完了。
(个人理解,dto和pojo的区别是,dto是展示用的数据,是提供给前端的结果,是从pojo处理(封装)得来的。pojo是满足业务需求的基本数据,只是单纯的数据记录。如果我展示的东西就是一个pojo,那么这个pojo也可以是dto。顺带一提,什么domain、bean之类的东西,其实都是pojo(我不太确定,请指正))
(6)养成写完就review的好习惯
编码完成后,我们应该马上实现下一个功能吗?个人认为,最好先review一次,趁着思路最清楚的时候找bug/看看实现是否合理,可以显著提高代码质量。
二、如何设计一个类
我们如何按照面向对象的思想,设计一个类?个人认为,一个类包括属性和行为。
User.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package com.xie.pojo; public class Test { private Integer id; private String username; private String password; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } |
id,username,password就是这个类的属性,是这个类需要用到的基本数据,下面的get、set方法,是围绕着属性的操作,是类的行为,我们可以用这个类做一些什么操作。这个东西大家都应该懂。
那么问题来了,我现在想封装一个dto,需要写一个getDto方法,那么这个方法,应该写在Service层中,还是写在这个dto类中?
个人认为有两种解释:
- “获取dto”是一种服务,所以写在Service层中,dto对象本身只是作为一个“承载信息的载体”而已。
- “获取dto”是dto类提供的一个方法,是dto类的自发行为。因为getDto方法和dto的属性有关,所以应该写在dto类中。
反正都解释得通,所以没必要纠结太多,我一般是写在dto类中的:
HuoLangRewardDto.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package xn.sftx.dto.huolang; import lombok.Data; import xn.sftx.domain.Goods; import xn.sftx.domain.huolang.HuoLangPlayerRecord.Node; import xn.sftx.domain.huolang.HuoLangReward; import xn.sftx.dto.PropDto; @Data public class HuoLangRewardDto implements Comparable<HuoLangRewardDto> { private int id; private String name; private int num; private int type; private int copperOrGold; private int price; private int currentBuyNums; private int maxBuyNums; private long comeOutTime; private PropDto goods; public static HuoLangRewardDto createRewardDto(Node node, Goods goods, HuoLangReward huoLangReward) { HuoLangRewardDto dto = new HuoLangRewardDto(); dto.setId(huoLangReward.getId()); dto.setName(goods.getName()); dto.setType(huoLangReward.getType()); dto.setCopperOrGold(huoLangReward.getCopperOrGold()); dto.setComeOutTime(node.getComeOutTime()); int currentBuyNum = node.getCurrentBuyNum(); String[] prices = huoLangReward.getPrice().split(","); dto.setPrice(new Integer(prices[currentBuyNum])); dto.setCurrentBuyNums(currentBuyNum); dto.setMaxBuyNums(huoLangReward.getMaxBuyNum()); PropDto propDto = new PropDto(goods); propDto.setCount(huoLangReward.getNum()); dto.setGoods(propDto); return dto; } @Override public int compareTo(HuoLangRewardDto o) { // TODO Auto-generated method stub return (int) (o.getComeOutTime() - this.getComeOutTime()); } } |
如何设计一个类,只要遵循属性+行为的规范,其他的就见仁见智了。不过一个项目中最好还是有一个统一的规范。
三、如何设计一个表
我以前经常是这么干的:拿到需求后,理解需求,根据需求大致设计一个表,通过工具拿到dao、pojo、mapper,写其他部分。
后来我发现,把设计表作为起手式,问题是比较大的。举几个例子:设计一个表总有疏漏,如果漏掉某个字段,就要更改表结构,上层的dap、pojo、mapper全都要改写,非常麻烦。写完逻辑发现表过度设计了,根本不需要这么多字段,又要更改表结构…
如果先设计接口、理清楚大概的逻辑,就会对整体的功能更为了解,设计表时越能够一不到位,省掉了很多修改的功夫。
至于设计表时的规范,什么表名大写开头、字段小写开头、boolean类型不用is开头,参考《阿里巴巴Java开发手册》就行了,创表之前可以先过一下。如果不怕麻烦,数据库的字段描述要写上说明。如果有功能上的变动,必须修改描述,避免误导他人(我经常懒得写,这是不对的)。
最后一点,我们无法预计某个功能在将来是否需要拓展,最好“按照感觉”留下一些空字段给后人使用,避免变动表结构。
四、代码层面一些常见的优化
包括但不限于:
- 简明易懂的命名方式,看到方法名就清楚大概作用。
- 分解过于臃肿的类,细分成更小的模块。
- 常用方法写成工具类,方便别人调用。
- 常用的常数写成静态属性,方便修改。
- 关键逻辑加上注释,说明工作原理,编码思路。
- 接口方法加上注释,调用时更加直观。
- 关键逻辑多加日志,日志尽可能详细易懂,能更快定位问题所在。
- 类开头加上作者、使用说明(使用了哪些表、怎么去修改,等等),方便后人工作。
- 拒绝硬编码。
- 善用断言。
可以参考:
http://www.xie4ever.com/2018/01/11/%E6%88%91%E5%AF%B9%E4%BA%8E%E5%90%88%E9%80%82%E7%9A%84%E4%BB%A3%E7%A0%81%E7%9A%84%E4%B8%80%E4%BA%9B%E6%83%B3%E6%B3%95/
五、spring断言
直接看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Override public void init() { // TODO Auto-generated method stub List<HuoLangReward> zaHuoList = huoLangRewardDao.selectRewardListByType(0); // spring断言 Assert.isTrue(zaHuoList.size() >= 7, "杂货的配置数量必须大于等于7"); for (HuoLangReward huoLangReward : zaHuoList) { String[] prices = huoLangReward.getPrice().split(","); Assert.isTrue(prices.length >= 2, "HuoLangReward表中,id为" + huoLangReward.getId() + "的物品价钱配置错误,必须存在至少2个价钱,否则获取价格时(打开界面时)会报数组溢出错误"); // 缓存杂货奖励 zaHuoRewardCache.put(huoLangReward.getId(), huoLangReward); } } |
这里的Assert.isTrue()就是spring提供的断言方法了。在这段代码中,prices.length >= 2这个条件必须得到满足,否则将会中断程序的启动, 并且打印出后面的描述。
经常有这样的场景:系统启动时,从数据库中读取配置。如果配置有误(为null之类的),那么系统的运行肯定会出问题,应该阻止其启动。这时候断言就能派上用场了。
六、一些工具
1.lombok
本质上是个辅助编码的工具。官网:https://projectlombok.org/
用这个东西,可以通过注解取代java类中冗长的get、set方法、toString方法、构造方法,通过“注解换方法”的方式让java类变得清爽。
如果需要使用lombok,将其jar包加入pom.xml文件即可:
1 2 3 4 5 6 |
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.20</version> <scope>provided</scope> </dependency> |
<scope>provided</scope>时表示:该包只在编译和测试的时候使用。可以猜测lombok的原理:通过识别java类中的注释,编译时直接在class文件(字节码)中加上相应的方法。
那么问题来了:只有编译后,class文件中才会有get、set方法(等等),编码时的java文件中是没有的。我们写代码的时候岂不是无法调用了?
为了解决这个问题,需要在Eclipse装上lombok插件:
1 |
java -jar lombok.jar // jar包所在的路径 |
使用指令后就能打开lombok界面,选中Eclipse所在的路径安装插件,重启Eclipse即可。其他高级用法请自行掌握。
2.JsonView.exe
查看json的工具,解析json更方便,免得每次都上www.json.com去解析。
JsonView.exe就是个小程序,不是百度搜JsonView出来的那个火狐插件…我没找到这个东西的官网,不过百度之后也有很多结果,可以自行下载。
七、总结
小小的工作总结。