关于api层接口定义的思考

一般接口的功能无非就是增删改查,定义一个接口看似简单,其实不然。接口的好坏无论对开发前期还是后期的功能迭代与维护影响都很大,接口一旦上线了,特别被其他服务使用了,后续修改的成本是很高的,接口上线容易下线难。要么保持接口的向后兼容性,缺点是随着项目的迭代,接口会变得越来越臃肿,后面可能自己都不想维护了,只能新增一个接口逐渐替换掉旧的接口。要么让接口调用方与接口提供方同步修改,这样成本还是比较高的,还需要占用额外的测试资源。

好的接口具有以下特征:

  1. 接口要保持简洁精练,符合单一职责原则
  2. 不同接口的参数模型不要混用
  3. 接口类名、方法名、字段名、参数名要做到见名知意。

1、接口要保持简洁精练,符合单一职责原则

好的接口不追求大而全,要尽可能保持精炼。大而全往往意味着高耦合,低扩展性,后续可维护性差。试想一下,当某个接口出了个小bug,仅仅对某个服务造成了影响,但当你要去修复的时候,动到的代码却影响到很多服务,这样风险就变大了,测试的时候是不是要花更多的时间呢。大而全的接口对接口调用方来说也是不友好的,这样无疑增加了对接口理解的成本

2、不同接口的参数模型不要混用

一般来说,不同接口之间的参数是不建议混用的,很多同学为了少定义几个类,不同接口的出入参出现混用的情况,这样做导致的结果就是扩展性差,接口变得难以理解。后续需要增加功能,需要增加对参数添加字段等,这样也会影响到其他接口的出入参。就算是对于同一种数据操作的创建类和更新类入参也是不建议混用的,如新增商品和编辑商品。有些人为了贪图一时之快,只定义一个入参模型,如新增和编辑都用ProductEditDTO

//商品新增和编辑共用的输入参数
public class ProductEditDTO {
    //商品id
    private Long id;
    //商品名称
    private String productName;
    //商品状态
    private Integer status;
    //商品分类
    private Integer categoryId;
}
复制代码

这样做导致的结果是有些字段不能用validation-api相关注解进行接口参数的提前校验,因为两个操作对参数的要求是不一致的。因为对于新增操作操作来说,id是非必填的,而productNamestatuscategoryId则是必填的。对于编辑操作来说,id是必填的,其他字段可以非必填。这样做的结果是对于一些基本的参数非空校验和格式校验只能在方法里面做校验了,这就导致了方法里面充斥很多参数校验性的事务脚本,代码看起来没那么美观。而新增操作单独用一个入参类ProductCreateDTO,编辑操作用ProductEditDTO,这样两个接口的参数看起来就清爽多了。

//商品新增参数
public class ProductCreateDTO {
    private Long id;
    @NotBlank(message = "商品名称不能为空")
    private String productName;
    @NotNull(message = "商品状态不能为空")
    private Integer status;
    @NotNull(message = "商品分类不能为空")
    private Integer categoryId;
}
复制代码
//商品编辑参数
public class ProductEditDTO {
    @NotNull(message = "商品id不能为空")
    private Long id;
    private String productName;
    private Integer status;
    private Integer categoryId;
}
复制代码

有些接口比较复杂,参数会有比较多的层次结构,而且不同接口也会存在一些字段意义相似或相同的参数,定义类的时候容易造成命名困难,就算解决了了命名问题,也容易造成包路径下类数目过多,看得眼花缭乱。如创建新商品的入参为ProductCreateDTO,编辑商品的入参为ProductEditDTO,查询商品详情的出参为ProductDetailDTO,这三个对象都需要有一个叫商品属性的字段attributes,那么就需要定义一个叫商品属性的类型,而且他们并不能共用同一个类型,因为他们的职责不一样,所需要的字段也不完全一样,约束条件也不一样,那么就要定义三个属性DTO对象了。同一个目录下,他们的名字不能相同,ProductCreateDTO的属性类可以命名为ProductCreateAttributeDTOProductEditDTO的属性类可以命名为ProductEditAttributeDTOProductDetailDTO的属性类可以命名为ProductAttributeDTO。实际上商品对象拥有的字段会更多,会有更多的子对象,如商品图片、商品标签、物流方式、包装方式等等,对于商品简单的增删改查操作就需要定义很多参数类了。我的做法是,不同接口的出参和入参各自定义一个外部类,内部涉及的数据结构都使用内部类,这样做的结果是每增加一个接口,包路径下最多只会增加两个类文件,一个入参,一个出参,出入参各自相关的数据结构都内聚都同一个文件里,符合接口设计低耦合、高内聚的思想,同时在一定程度上降低了不同接口的参数被混用的概率。内部类的命名也可以更宽松,不需要加过多的修饰,因为它所处的位置就足以说明他的作用,同时也会出现命名冲突的情形,ProductCreateDTOProductEditDTOProductDetailDTO里面的商品属性统一命名为AttributeDTO即可,编码的时候更加简单明了。复杂的对象逻辑上会存在子类还会嵌套其他子类的情况,子类嵌套越深,代码里面定义变量、写lambda表达式的代码就会越长,可读性不高。内部类可以定义外部类的第一层,不要嵌套多层,一般不会出现命名冲突的,虽然嵌套多层逻辑上是严谨一点,但是代价是读写代码要花更多的时间。

//商品创建输入参数
public class ProductCreateDTO {
    //商品id
    private Long id;
    //商品名称
    @NotBlank(message = "商品名称不能为空")
    private String productName;
    //商品状态
    @NotNull(message = "商品状态不能为空")
    private Integer status;
    //商品分类
    @NotNull(message = "商品分类不能为空")
    private Integer categoryId;
    //商品属性
    @NotEmpty(message = "商品属性不能为空")
    private List<@Valid AttributeDTO> attributes;
    //商品属性
    public static class AttributeDTO {
        //基础属性id
        @NotNull(message = "基础属性id不能为空")
        private Long attrId;
        //属性名称
        @NotEmpty(message = "属性名称不能为空")
        private String name;
        //属性值
        @NotBlank(message = "属性值不能为空")
        private String value;
    }
}
复制代码
//商品编辑输入参数
public class ProductEditDTO {
    //商品id
    @NotNull(message = "商品id不能为空")
    private Long id;
    //商品名称
    private String productName;
    //商品状态
    private Integer status;
    //商品分类
    private Integer categoryId;
    //商品属性
    @NotEmpty(message = "商品属性不能为空")
    private List<@Valid AttributeDTO> attributes;
    /**
     *商品属性,商品可以存在多个属性,编辑的时候可以新增属性,也可以更新某个属性,
     *所以id,name都是非必填,支持输入自定义属性的话,attrId也可以为空
     */
    public static class AttributeDTO {
        //商品属性id
        private Long id;
        //基础属性id
        private Long attrId;
        //属性名称
        private String name;
        //属性值
        @NotBlank(message = "属性值不能为空")
        private String value;
    }
}
复制代码
/**
 *属性详情,查询结果不需要做参数校验,故不用validation-api注解,
 *因为主要用作展示,所以字段会多一点
 */
public class ProductDetailDTO {
    //商品id
    private Long id;
    //商品名称
    private String productName;
    //商品状态
    private Integer status;
    //商品分类
    private Integer categoryId;
    //商品分类名称
    private String categoryName;
    //商品属性
    private List<AttributeDTO> attributes;
    //商品属性
    public static class AttributeDTO {
        //商品属性id
        private Long id;
        //属性id
        private Long attrId;
        //属性类型
        private Integer type;
        //属性名称
        private String name;
        //属性值
        private String value;
    }
}
复制代码

当然,有些公共的参数模型是可以共用的,如接口最外层响应对象,一般命名为RemoteResponseCommonResult等,还有分页查询出入参PageQueryPageResponse。如果开放给外部系统使用的接口,一般会定义共同的请求参数和响应参数模板,请求参数会带一些公共的认证信息等,响应参数返回公共的状态码和错误信息。

3、接口类名、方法名、字段名、参数名要做到见名知意。

好的的命名是可以做到见名知意的,只需要简单的注释即可。相反,如果发现一个方法需要写很多注释,我们是否应该思考一下这个方法是不是承担了太多业务功能,没有做到单一职责性,是否需要拆分成多个方法?一般来说,一个模块至少有两个成员在维护,两个人可以互相替补的,有一套共同的命名规则是有利于团队成员之间的高效协作。

一般来说,代码里面的命名可以遵循这样的规则:

  1. 创建类操作方法名可以命名为createXXX,输入参数可以命名为XXXCreateDTO
  2. 编辑类操作方法可以命名为editXXX,输入参数可以命名为XXXEditDTO
  3. 查询类操作方法可以命名为getByXXXlistByXXXfindByXXX,如getByIdlistByIdsfindByPage等。一般来说,列表分页查询返回的对象只需要少量字段,参数可以命名为XXXDTO,详情查询接口返回参数一般需要返回更完整的数据,可以命名为XXXDetailDTO,分页查询不要共用详情查询的出参。

小结

软件开发就如同建造一栋大厦,万丈高楼平地起,定义接口是第一步,所以定义接口时候要保持敬畏之心,不能随意,这对后续功能迭代影响是很大的。

猜你喜欢

转载自juejin.im/post/7041816053671788581
今日推荐