文档章节

代码结构及一些代码规范建议

Mr---D
 Mr---D
发布于 01/23 16:52
字数 2726
阅读 2957
收藏 93

一些感悟


  • 代码结构和规范关系到项目的可持续维护以及维护的周期,非常重要,但真正重视并落地的很少
  • 经典的MVC模式一般都能说出来,但真正落地到项目代码结构的时候,却缺少思考
  • 当写代码和找代码让人感觉别扭的时候,就该考虑如何去优化了
  • 一切皆对象,在规划代码结构的时候也需要有面向对象的思维方式
  • 很多张口就是高并发、大数据、高流量等之类高大上词汇的人,缺很少注重代码的基础结构,写出的代码很难让人轻易上手
  • 如果代码结构和规范做得好一点,一般项目有一两个顶梁柱再加一些新手就完全可以搞定。这样既可以节省人力成本,也可以快速培养新人,新加入的成员也能快速融入

以下是整理的一般类型的项目代码结构,仅供参考。部分模块是使用spring boot开发项目的命名,但总体结构思路是一样的,如果不使用spring boot开发项目,只是修改一下名字即可

建议的包结构及简单说明

  • itopener-parent:顶级maven parent,配置统一的maven插件、依赖包版本管理等
    • itopener-utils:全局公用的工具类,如:加密操作、集合处理、字符串处理等等
    • itopener-framework:基于框架的统一的封装,比如:拦截器、controller返回对象、BaseController等
    • itopener-spring-boot-starters-parent:自定义封装spring boot starter的parent,方便统一管理。此目录即公共组件库,存放各种封装好的组件,以便使用
      • itopener-lock-spring-boot-starter-parent:单个starter的parent,方便单个starter的管理,如:单独对此starter进行优化升级
        • itopener-lock-spring-boot-starter:spring boot 的starter,使用的时候依赖此模块
        • itopener-lock-spring-boot-autoconfigure:spring boot starter的自动配置模块
      • itopener-druid-spring-boot-starter-parent
        • itopener-druid-spring-boot-starter
        • itopener-druid-spring-boot-autoconfigure
    • itopener-demo-parent:应用的parent,方便项目的统一管理
      • itopener-demo-model:maven模块,存放与数据库表对应的域对象及其枚举定义、查询条件、mapper及操作等
        • com.itopener.demo.model:po对象,与数据库表一一对应,可以增加关联对象的属性,方便返回数据,如订单表关联订单明细表的多条数据,则订单类里可以增加一个订单明细的List属性,但严禁增加查询条件,所有查询均使用conditions包对应的对象作为查询条件(只有一个查询条件的可以直接以条件作为参数,如select(long id)、select(UserCondition condition)
        • com.itopener.demo.dao:mybatis对mapper的操作,建议使用sqlid的调用方式,更灵活,公用性更强
        • com.itopener.demo.mapper:mybatis的映射文件,与数据库表一一对应
        • com.itopener.demo.conditions:查询条件,建议每个表都对应一个查询条件,包含分页、排序、like条件、between条件等属性
        • com.itopener.demo.enums:model对象中属性对应的枚举,属于model的属性,而并非全局公用,很多人会放到整个项目的公共模块,我认为有点过度设计
      • itopener-demo-common:应用的公共模块,主要存放公共的service操作,方便web、work、api等模块公用
        • com.itopener.demo.common.service:公共的service
      • itopener-demo-web:web模块,提供web服务(针对有页面的web服务端)
        • com.itopener.demo.Application:应用启动类
        • com.itopener.demo.config:存放应用的配置
        • com.itopener.demo.controller:控制器
        • com.itopener.demo.service:应用的service,通过调用dao或公共的service组合成controller需要的业务
        • com.itopener.demo.vo:请求参数和返回值对象
        • com.itopener.demo.vo.converter:vo与model之间的转换器
        • com.itopener.demo.enums:返回值相关的枚举定义(根据实际需要定义,非必要)
      • itopener-demo-api:api模块,对外提供http协议的服务(供其他系统调用,无页面)
        • com.itopener.demo.Application:应用启动类
        • com.itopener.demo.config:存放应用的配置
        • com.itopener.demo.controller:控制器
        • com.itopener.demo.service:应用的service,通过调用dao或公共的service组合成controller需要的业务
        • com.itopener.demo.vo:请求参数和返回值对象
        • com.itopener.demo.vo.converter:vo与model之间的转换器
        • com.itopener.demo.enums:返回值相关的枚举定义(根据实际需要定义,非必要)
      • itopener-demo-sdk:sdk模块,主要存放对外提供的接口的辅助类,如入参出参、调用方式等。接口调用方和提供方公用的模块
      • itopener-demo-work:work模块,应用所需的自动任务
        • com.itopener.demo.Application:应用启动类
        • com.itopener.demo.config:应用的配置
        • com.itopener.demo.service:应用的service,通过调用dao或公共的service组合成work所需的业务
        • com.itopener.demo.work:调度任务
      • itopener-demo-views:views模块,纯前端静态资源,方便前后端完全分离
        • static:静态资源,如:js、css、jpg、html等
        • templates:前端模板页面,如:html、ftl等(如果使用纯html则不需要此包)
      • itopener-demo-gateway:应用网关,用于鉴权、配置路由、负载均衡、重试等
        • com.itopener.demo.Application:应用启动类
    • itopener-demo2-parent:另一个系统,与itopener-demo-parent一致
      • itopener-demo2-model
      • itopener-demo2-common
      • itopener-demo2-web
      • itopener-demo2-api
      • itopener-demo2-work
      • itopener-demo2-views

说明

  • 此代码结构是使用spring boot、spring cloud作为主要开发技术。如果不使用spring boot + spring cloud,大体结构类似,比如:maven模块的划分、model/view/controller对应包的划分

  • model模块可以使用工具根据数据库表结构自动生成对应的文件

  • 代码结构中的接口是http形式的接口,如果是rpc方式,api模块做对应调整即可

代码规范和结构的几点建议

  • service不需要接口,直接写java对象,区别在于spring会使用cglib作为代理的生成方式(如果是写接口和实现类,会使用jdk代理)。理由:在应用内部的各层次(controller、service、dao)之间进行调用,每次修改都是在应用内修改,不存在接口的调用方和实现方单独升级的情况,如果同时去修改接口和实现类,显得很多余;如果是应用之间的调用,必须定义接口。(个人认为原因可能是所谓的面向接口编程被过度理解和使用。应用内如果有一些复杂的业务逻辑,可适当考虑使用接口+多实现的方式,如遇这些情况,多参考设计模式的一些思想)

  • 如果service需要写接口,接口以I开头,实现类以接口名去掉I命名。有的实现类会以impl结尾,个人认为没有必要。理由:impl表示的是类的层次结构,这个在包名上已经体现出来,并且类名应该主要体现实现的业务(见名知义),再有service的类名已经以service结尾,体现出了分层的意义,没有必要在命名上重复体现

  • mybatis的mapper建议使用sqlid的方式调用,这样能提高mapper.xml内代码的重用性;如果用定义接口与mapper.xml的id对应的方式,只能一一对应,不方便公用。比如User对象的查询功能:

<sql id="Where">
    <where>
        <if test="id > 0">
            and id = #{id,jdbcType=BIGINT}
        </if>
        <if test="status > 0">
            and status = #{status,jdbcType=TINYINT}
        </if>
        <if test="username != null and username != ''">
            and username = #{username,jdbcType=VARCHAR}
        </if>
        <if test="password != null and password != ''">
            and password = #{password,jdbcType=VARCHAR}
        </if>
    </where>
</sql>
     
<select id="select" parameterType="com.itopener.demo.conditions.UserCondition" resultMap="BaseResultMap">
    select <include refid="Column_List" />
    from t_user
    <include refid="Where" />
</select>

使用sqlid调用方式的话,dao中的查询可以公用同一个sqlid:

public List<User> selectList(UserCondition condition) {
    return baseDao.selectList(NAMESPACE + "select", condition);
}
 
public List<User> selectPage(UserCondition condition) {
    return baseDao.selectPage(NAMESPACE + "select", condition);
}

public User selectOne(UserCondition condition) {
    return baseDao.selectOne(NAMESPACE + "select", condition);
}

如果使用写接口与mapper对应的方式,mapper.xml则需要写三个select,或者通过传入参数进行判断处理,返回值也根据使用情况需要处理

  • 数据库中整数字段的值,对应java类中建议尽量使用基础数据类型,这样在使用过程中可以减少为空的判断,直接判断数字即可。

  • 数据库中枚举类型的字段也对应基础数据类型,并且建议不要使用0来表示某种意义。这样可以方便的在mybatis的xml中使用>0来判断是否有传值,达到编写公共性更强的动态SQL的目的

  • 关于spring cloud网关的使用,个人认为只是在原web应用(微服务)之前加了一层控制,两者是独立存在的,各司其职。如果网关上有服务聚合或某些业务需要调用服务时,应该使用http通过服务名调用(RestTemplate支持),尽量少用Feign调用,因为Feign需要网关和服务都依赖相同的接口。如果非要使用Feign调用,也建议不要将服务中的类名写成service,因为应用中本身是有service层来调用不同dao组合业务提供给controller的,两者都叫service容易混淆。建议两种方式:

    • 服务中以正常的controller命名,但通过包名区分开
    • 以Feign或其他特定字符命名,并通过包名区分开

但总的来说,我认为网关本身的作用是用来做服务路由、安全控制、流控等,应该尽量少与服务有依赖,而Feign这种方式就需要和服务有共同的依赖,所以还是建议使用RestTemplate通过服务名来调用,达到聚合服务的目的,如果只是服务路由,可以使用zuul来实现

  • 减少事务开启时间,建议尽量将接收到的参数的判断、对象的初始化、vo与po的转换等放到事务开启之前,因为service只是调用不同dao或公用service对数据的操作来组合成controller需要的业务,并做事务控制,所以service应该只接收自己需要的并且正确的数据对象

  • 方法名去掉冗余的部分,java定义方法签名是方法名+参数,只要根据方法签名能够知道其意思即可,如:

User selectById(long id);
User selectByName(String name);

可以改为:

User select(long id);
User select(String name);
  • 写工具类或封装的时候考虑级联操作,即对对象属性的操作方法返回当前对象,如下是自己封装的controller统一使用的返回对象:
import org.springframework.ui.ModelMap;
 
/** * Created by fuwei.deng on 2017年5月5日. */
public class ResultMap extends ModelMap {
 
    /** */
    private static final long serialVersionUID = 5898506914945717989L;
     
    public static final String CODE_SUCCESS = "ok";
     
    public static final String CODE_FIALED = "failed";
     
    private final String FLAG_KEY = "code";
     
    private final String MSG_KEY = "msg";
     
    public ResultMap(){
        put(FLAG_KEY, CODE_SUCCESS);
    }
     
    public ResultMap(String msg) {
        put(FLAG_KEY, CODE_FIALED);
        put(MSG_KEY, msg);
    }
     
    public ResultMap(String code, String msg){
        put(FLAG_KEY, code);
        put(MSG_KEY, msg);
    }
     
    public ResultMap setCode(String code) {
        put(FLAG_KEY, code);
        return this;
    }
     
    public ResultMap setMsg(String msg) {
        put(MSG_KEY, msg);
        return this;
    }
     
    public ResultMap put(String key, Object value){
        super.put(key, value);
        return this;
    }
     
    public static ResultMap buildSuccess(){
        return new ResultMap();
    }
     
    public static ResultMap buildFailed(String msg){
        return new ResultMap(msg);
    }
}

使用的时候可以这样:

public ResultMap query(Condition condition){
    long count = ...
    List<User> list = ...
    return ResultMap.buildSuccess().put("count", count).put("list", list);
}

© 著作权归作者所有

共有 人打赏支持
Mr---D
粉丝 82
博文 17
码字总数 37748
作品 0
成都
程序员
加载中

评论(20)

之渊
之渊
就是很多情况下 service,dao 不需要建立接口的,很麻烦而且浪费时间。又不是写什么框架插件有多种实现方式的。
无著方知尘亦珍
无著方知尘亦珍

引用来自“J猿”的评论

引用来自“无著方知尘亦珍”的评论

还有很多其他的什么建议,说是为了什么通用,性能?但在我看来是牺牲了,清晰,准确,类型安全,隔离性,可重构,等等换来的

@无著方知尘亦珍 看了你的评论,我只能说呵呵,你根本就没仔细看,完全就是喷子一个

必须喷,因为到了如今再回头看看,当初刚初学的时候,很多人很多所谓的“经验”,都只是表面功夫,所谓的教条主义,等你自己去看那些顶级的开源项目,去理解就明白了。
翠翠
翠翠

引用来自“J猿”的评论

引用来自“翠翠”的评论

接口还是不要使用 I 开头了。

引用来自“J猿”的评论

原因?

引用来自“翠翠”的评论

接口以 I 开头是 C++ 的遗留习惯而已,Java 规范中没有也并不推荐这么做,而且 JDK 源码的接口基本没有用 I 开头的。
1. 接口以 I 开头就算是从C++而来的遗留习惯,如果是好习惯,为啥不能用?
2. Java规范中不推荐这么做是有官方文档说明吗?如果有请贴出来,感谢
3. JDK源码的接口和实现类在命名上本身就没有太多重合度,所以没有用 I 开头的是很正常的
4. 文中所说的以 I 开头并不是指所有的接口。是因为很多项目中写service的时候会写一个接口对应一个实现类,是指这种情况的service接口建议以 I 开头,实现类与其对应的名称去掉 I 命名
5. 当然如果存在一些复杂逻辑需要定义接口对应有多个实现类的时候完全可以不用 I 开头,而以接口或类的实际作用命名

回复@J猿 : C++ 里这样做有他的原因,Java 不这样做也有 Java 的原因(其实说白了就是 IDE 越来越智能了而已)。说实话 Java 也二十来年了,代码规范这块儿早已定型,Google 公开了自己的规范,Ali 也公开了自己的规范,两者只有微小区别而已,更别提还有 checkstyle 这个强大的代码规范检查工具,建议还是直接拿过来用,不要自己造轮子了。
Mr---D
Mr---D

引用来自“无著方知尘亦珍”的评论

还有很多其他的什么建议,说是为了什么通用,性能?但在我看来是牺牲了,清晰,准确,类型安全,隔离性,可重构,等等换来的

@无著方知尘亦珍 看了你的评论,我只能说呵呵,你根本就没仔细看,完全就是喷子一个
无著方知尘亦珍
无著方知尘亦珍
还有很多其他的什么建议,说是为了什么通用,性能?但在我看来是牺牲了,清晰,准确,类型安全,隔离性,可重构,等等换来的
无著方知尘亦珍
无著方知尘亦珍
I开头命名接口是干嘛?表示我是接口么?为什么非得强调这个类是接口?"接口"本身是一个通用概念,谁说必须是java接口类才是接口?那是不是所有对外的什么类,资源都做特殊命名?所谓的封装,透明,难道就靠命名?匈牙利命名为什么被废弃了?因为现在的开发工具已经可以明示类型了,是不是接口同样可以。假如我有个类是I开头,但他就不是接口类,怎么办?
无著方知尘亦珍
无著方知尘亦珍
看了前面几条,立马坚决反对这样做。开玩笑,少在这乱扯,除了那个service可以不写接口之外能接受。那个I开头命名接口,mybatis用id搞什么通用之类的。你是在逗我呢?
Mr---D
Mr---D

引用来自“吃饼青年”的评论

代码规范是缩小维护成本的一大利害。
并且也是新成员快速融入的好处之一,在人员流动方面的影响会更小
Mr---D
Mr---D

引用来自“翠翠”的评论

接口还是不要使用 I 开头了。

引用来自“J猿”的评论

原因?

引用来自“翠翠”的评论

接口以 I 开头是 C++ 的遗留习惯而已,Java 规范中没有也并不推荐这么做,而且 JDK 源码的接口基本没有用 I 开头的。
1. 接口以 I 开头就算是从C++而来的遗留习惯,如果是好习惯,为啥不能用?
2. Java规范中不推荐这么做是有官方文档说明吗?如果有请贴出来,感谢
3. JDK源码的接口和实现类在命名上本身就没有太多重合度,所以没有用 I 开头的是很正常的
4. 文中所说的以 I 开头并不是指所有的接口。是因为很多项目中写service的时候会写一个接口对应一个实现类,是指这种情况的service接口建议以 I 开头,实现类与其对应的名称去掉 I 命名
5. 当然如果存在一些复杂逻辑需要定义接口对应有多个实现类的时候完全可以不用 I 开头,而以接口或类的实际作用命名
翠翠
翠翠

引用来自“翠翠”的评论

接口还是不要使用 I 开头了。

引用来自“J猿”的评论

原因?
接口以 I 开头是 C++ 的遗留习惯而已,Java 规范中没有也并不推荐这么做,而且 JDK 源码的接口基本没有用 I 开头的。
iOS 移动端面向文档开发

之前的解耦架构生成器在实际项目中已经顺利测试通过了, 现在要做的是将文档规范出来, 并扩展到Android, HTML5端的共用, 实现面向文档开发. 参考链接: iOS 执行.py脚本生成解耦架构 iOS 执行....

Castie1
2017/11/25
0
0
JavascriptCodingStandard Javascript 代码规范

规范理念 Any violation to this guide is allowed if it enhances readability. 所有的代码都要变成可供他人容易阅读的。 --引用自Dojo Javascript 语法规范 规范详解 命名规范 必须使用 Ta...

big军
2011/06/24
0
0
【腾讯前端代码规范】DevelopmentCodes

Web技术中心代码规范 前言 软件的长期价值直接源于其编码质量。在它的整个生命周期里,一个程序可能会被许多人阅读或修改。如果一个程序可以清晰的展现出它的结构和特征,那就能减少在以后对...

undefine
2014/01/20
0
1
【iOS】iOS开发编码规范小结

规范编码可以提高代码的可读性,降低维护成本。作为一个程序员,要对自己写的代码负责,虽然bug无可避免,但是写代码时最基本的编码规则还是应该遵守的,否则不是坑自己就是坑别人,因为代码...

浅浅青丘
01/24
0
0
php代码规范

1 编写目的 为了更好的提高技术部的工作效率,保证开发的有效性和合理性,并可最大程度的提高程序代码的可读性和可重复利用性,指定此规范。开发团队根据自己的实际情况,可以对本规范进行补...

出门右拐是食堂
2016/01/09
209
1

没有更多内容

加载失败,请刷新页面

加载更多

八种排序算法的时间复杂度复杂度

1、稳定性 归并排序、冒泡排序、插入排序。基数排序是稳定的 选择排序、快速排序、希尔排序、堆排序是不稳定的 2、时间复杂度 最基础的四个算法:冒泡、选择、插入、快排中,快排的时间复杂度...

陈刚生
22分钟前
1
0
大数据学习系列 Hadoop+Spark+Zookeeper+HBase+Hive集群搭建 图文详解

目录 引言 目录 一、环境选择 1,集群机器安装图 2,配置说明 3,下载地址 二、集群的相关配置 1,主机名更改以及主机和IP做相关映射 2,ssh免登录 3,防火墙关闭 4,时间配置 5,快捷键设置...

董黎明
35分钟前
1
1
六元一个的私有博客系统,了解一下?

神说要有光,于是便有了光 写代码的,偶尔都想装点逼,想要自己写点博客。刚开始还能在各大社区写,比如说CSDN,开源中国,博客园什么的。但是越写就会越觉得,那些博客平台都不是自己想要的...

耒耒耒耒耒
40分钟前
1
0
maven环境隔离

一.maven项目环境根据实际情况进行隔离: 开发环境 dev 测试环境 beta 线上环境 prod 二.pom 配置: build节点 <build> <resources> <resource> <directory>src/......

imbiao
41分钟前
1
0
webrtc收包流程源码分析

版本: webrtc M59 收包流程: AsyncUDPSocket::OnReadEvent AllocationSequence::OnReadPacket HandleIncomingPacket UDPPort::OnReadPacket Connection::OnReadPacket P2PTransportChannel......

bill_shen
43分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部