为什么要写好业务代码?
直接分享一段痛苦的项目维护经历吧,看大家有没有类似的经历。当时,我接手了一个维护项目,刚上班就接到新增一个显示字段的任务。我以为这应该是一个分分钟就能够搞定的小需求,没有想到这就开始了我的痛苦之旅。我梳理了关联的api后,发现每个api都是从controller控制层-》service-》服务层-dao数据层,甚至每个api都对应一个sql查询。
但是,所有的api之间又有很大类似的代码。我开始阅读代码的时候,发现一个特殊的controller,在该controller里包括身份校验,参数校验,各种业务代码,各种if else,for循环语句,甚至dao层的逻辑都融到了一块。
更让人悲痛欲绝的是项目没有文档,代码也几乎没注释,没有测试用例,我还是直接撸代码梳理业务,很多属性字段无法理解到底代表什么,例如,ajAmount,gjjAmount;在sql语句中写status in(1,2,4,6),case when,等很多魔法数条件判断。
我最后直接抓包调用了一下api,然后,通过与页面的展示端字段匹配我才知道ajAmount,gjjAmount分别表示按揭贷款,公积金代码,status的部分字段是什么意思。这样的项目维护经历,你有没有类似的经历?
个人认为,只要我们做到api拒绝烟囱式开发,业务代码拒绝All in one,项目做好代码注释,就可以写出易阅读,好扩展的代码。
api如何拒绝烟囱式开发
上述的api开发开发过程就是典型的烟囱式开发模式,所有的api服务与相似业务,但是每个api都是完全独立的开发,其开发流程如图:
如上的开发流程有几个弊端,如下:
业务代码重复,在不同的service实现中,业务相似的话会有大量重复代码。
数据库表结构的改动需要修改所有涉及到的dao层,维护成本比较高。
此类相似业务,api层定义各自显示对象,dao层负责获取全量数据(例如,用户查询,就获取整个用户表字段的数据),service层定义业务对象,根据不同api不同业务类型的判断,根据dao查询的数据组转业务对象,以及业务对象向api显示对象的转换。
开发流程如图:
这样的开发模式有如下优势:
业务代码集中在service层,专注业务对象bo的封装,以及业务对象向给类显示层vo的转换;封装复用逻辑,可以大量减少重复代码。如果,设计模式从一开始就设计得易扩展,后期维护就快捷的多。
数据库的改动只涉及到db层,能够快速的在各个业务响应。
业务代码如何拒绝All in one?
以上的controller代码最突出的缺点就是代码完全无法复用,完全没有使用到面向对象封装,集成,多态的特性。业务开发中,一般都是权限校验,参数校验,业务判断,业务对象转换数据库操作。
我的做法是业务抽象,把公共代码进行抽取,通过配置的形式的方式调用,使业务代码可以以可插拔的方式选择指定的权限校验,参数校验。简单来说,就是善用AOP面向切面编程的思想,示例如下:
权限校验:
使用aop对权限校验逻辑进行抽取,能够通过注解的方式指定哪些controller需要进行权限校验。对用户进行数据过滤时,使用controller的拦截器获取该用户拥有的各类权限,并把用户数据保存在上下文threadloal中,并且通过配置对指定url进行拦截。在业务层,从上下文拿到用户权限数据做各类数据业务过滤,通过aop实现各类拦截业务的指定调用。
参数校验:
使用java validtion对通用的字段,例如电话号码,身份证,进行扩展,详细可以参考,如何使用validation校验参数?,在项目中其他类似校验进行复用。
业务判断:使用设计模式对不同类型的业务开发进行封装,集成,多态扩展;这样在后期的扩展中可以基于开发封闭原则,针对新的业务扩展子类即可。
业务对象转换数:
业务开发过程中,依照阿里巴巴研发规范的要求,存在DO(数据库表结构一致的对象),BO(业务对象),DTO(数据传输对象),VO(显示层对象),Query(查询对象)。
使用MapStruct,可以灵活的控制的不同属性值之间的转换规格,比org.springframework.beans.BeanUtils.copyProperties()方法更加灵活。
参考这篇文章:
https://www.javastack.cn/article/2021/maptruct-advanced-useages/
示例:
public interface CategoryConverter { CategoryConverter INSTANCE = Mappers.getMapper(CategoryConverter.class); @Mappings({ @Mapping(target = "ext", expression = "java(getCategoryExt(updateCategoryDto.getStyle(),updateCategoryDto.getGoodsPageSize()))")}) Category update2Category(UpdateCategoryDto updateCategoryDto); @Mappings({ @Mapping(target = "ext", expression = "java(getCategoryExt(addCategoryDto.getStyle(),addCategoryDto.getGoodsPageSize()))")}) Category add2Category(AddCategoryDto addCategoryDto); }
DB数据库公共字段填充:
例如,公共字段,生成日期,创建人,修改时间,修改人使用插件的形式进行封装,在mybatis-plus中使用MetaObjectHandler,在执行sql之前完成统一字段值的填充。
业务平台字段查询过滤:
在中台的开发中,数据采用不同平台code的列实现不同平台业务数据的隔离。基于mybatis插件机制的多租户过滤机制实现可以参考如何使用MyBatis的plugin插件实现多租户的数据过滤?。
在dao层的方法或者接口上加上自定义过滤条件即可,示例如下:
@Mapper @Repository @MultiTenancy(multiTenancyQueryValueFactory = CustomerQueryValueFactory.class) public interface ProductDao extends BaseMapper<Product> { }
缓存的使用:
Spring开发中通常集成spring cache使用以注解的形式使用缓存。整合redis并且自定义默认时间设置可以参考(Spring Cache+redis自定义缓存过期时间)。
示例如下:
/** * 使用CacheEvict注解更新指定key的缓存 */ @Override @CacheEvict(value = {ALL_PRODUCT_KEY,ONLINE_PRODUCT_KEY}, allEntries = true) public Boolean add(ProductAddDto dto) { // TODO 添加商品更新cache } @Override @Cacheable(value = {ALL_PRODUCT_KEY}) public List<ProductVo> findAllProductVo() { return this.baseMapper.selectList(null); } @Override @Cacheable(value = {ONLINE_PRODUCT_KEY}) public ProductVo getOnlineProductVo() { // TODO 设置查询条件 return this.baseMapper.selectList(query); }
项目如何做好代码注释?
枚举类的使用:
在业务中特别是状态的值,在对外发布api的vo对象中,加上状态枚举值的注释,并且使用@link 注解,可以直接连接到枚举类,让开发者一目了然。
示例如下:
public class ProductVo implements Serializable { /** * 审核状态 * {@link ProductStatus} */ @ApiModelProperty("状态") private Integer status; }
迁移sql查询条件:
避免在sql层写固定的通用的过滤条件,迁移到服务层做处理。
示例如下:
// sql查询条件 SELECT * from product where status != -1 and shop_status != 6 // 在业务层把各类状态值进行条件设置 public PageData<ProductVo> findCustPage(Query query ){ // 产品上线,显示状态 query.setStatus(ProductStatus.ONSHELF); // 产品显示状态 query.setHideState(HideState.VISIBAL); // 店铺未下线 query.setNotStatus(ShopStatus.OFFLINE); return productService.findProductVoPage(query); }

