1、业务功能API设计
SPI,全称是Service Provider Interface,是服务提供者接口。我们常常希望功能模块的复用性高,又希望各功能模块间的耦合度低,例如当某个模块中有恰好能满足新需求的功能时能直接拿过来用且又不需要引入其所依赖的其他构件,就像是一个独立又可靠的零件拼哪都能独立良好运行。从这个角度来讲,我们需要将各个功能做得可组装起来,当成功组装则该功能就可用。为了这个目标,我们应该需要一个context来进行这样的组装的管理,或许我们可以在每个组装对接的接口处建立一个context上下文来进行动态拼接组装管理比如自定义一个,也可以使用一个更大的context来进行一个整体的装配控制比如Spring的ApplicationContext配合条件装配来实现。
2、SQL和数据结构和算法
现在我们基本上都是面向SQL开发,所以SQL优化是很重要的一方面,SQL查询结果的处理又是很重要的一个方面。我个人倾向的原则是,通过最少的连接去获取最必要的数据,然后剩余的处理都在应用中来完成。
建表的时候除了那三原则之外,经验人士都建议再加上创建者、创建时间、更新者、更新时间这四个技术字段,这四个字段大多数时候是没什么用的,主要是用于记录的统计分析用。但是很多时候就是需要统计分析的时候,没有这几个字段我们就很蛋疼。
oracle我们可以看看解释计划来分析我们的sql的执行顺序、是否走索引、开销花费等等。
拼接占位符超过1000的时候我们可以in or in or in。
我曾经听闻有一种建立规则主键以应对海量数据处理的思路,主键肯定是有索引的,这样就可以通过这个规则主键来进行逻辑范围的划分,以实现在物理上分区分表后的逻辑上的范围划分。不过这我也只是听说过而已,并没有实践过,目前还没遇到过海量数据的处理情况,真要有这种情况的话到时候可能会有其他的针对性的解决方案。
性能优化,我认为做到极致就是拿空间换时间,当然算法肯定是要使用最优的算法咯。
算法从某种角度来讲,就是解决某个问题的具体的有穷的步骤,有一个小细节我想提醒一下,就是算法中有循环体的时候需要仔细考虑循环的边界处情况,如果循环中的某个操作是依靠下一次循环中的条件发生变化来触发的,那就需要考虑最后一次循环结束后没有下一次循环发生条件变化来触发,需要在循环结束后补充这一次操作。
3、一些注释的建议
注释需要好好地写,复杂的if判断的地方需要写上详细的逻辑注释,,否则到时候再读这段代码的时候可能就不是那么轻松了。但是也有注释逻辑与代码逻辑对不上的情况,看了注释后忽视检查代码实际逻辑以致出现问题,如果时间充足的话就写尽可能多的单元测试覆盖逻辑组合的情况以确保注释中的逻辑与代码实现的逻辑是一致的。不过有时候注释过多也不是一件好事,我们一般也就类头、方法头注释的内容会多一些,方法体中的注释应该只加在关键代码前后。我觉得那种包围型的注释是不好的,日积月累会严重影响代码的阅读。
复制粘贴是有风险的,因为很可能复制之后变量名的变更没修改全,最后导致错误。但是确实我们常常会遇到相同的业务逻辑或相同的处理模板流程,而大量的参数不同或参数的行为略有不同或实现细节略有不同的情况,于是我们在编码的时候会非常倾向于复制粘贴之后稍微改一改,这是人之常情,我唯一想说的就是记得对这一块代码进行充分地测试。复制有风险,粘贴需谨慎。
如果时间充足的话,最好在方法头、类头上把业务逻辑写到注释中,最好也写一写为什么要设计成这样,这样后续的兄弟来接收的时候看代码也能一目了然,能有个索引以提高代码阅读理解的效率。
4、日志规范的自勉
我还记得小时候看的那种带有操作电脑场景的电影中,常常看到显示器里从上到下一直在输出文字,最后镜头一个特写finish或者一个done主角就迅速离开了。那应该就是日志,不过在那种场景下或许更应该称之为人机交互。打得好的日志可以很便捷地帮助查看日志者了解当前的进度,但是日志输出太多太快却非常不便还非常占空间。
现在Java应用的日志大多数用的是log4j,一般会根据配置的样式把时间、类信息、线程信息以及要记录的内容给打印出来,如果一个应用只是几个简单的功能不会产生歧义,那真的是想怎么打就怎么打。如果功能很多时,虽然会把类信息给打印出来,但还是不便于区分和阅读,特别是再加上多线程处理和多线程打印,日志会打在一起,所以若是在输出的日志内容中就能加以区分那就是极好的。
比如进程启动的时候会有一个注册的动作,如果很多个模块、构件都有注册的动作,要是各个都打印一个“注册成功!”的日志,那看起来就很蛋疼,但是如果打印的是“XXX模块注册成功!”那看起来就一目了然了。
所以打印日志的时候,个人推荐按照这种格式来打印:“[本身份][在做什么动作时][发生了什么][接下来将要做什么]”。一般来讲,正常日志主要把交互处的情况给记录在日志中即可,比如在流程开始处和流程结束处,打印入参或返回值,而且需要是info级别,其他内部处理的日志可以是debug这样在日志级别较高的时候能少占一些空间。
对于异常情况,那更应该要记录日志,如果能大致判断出可能是什么原因导致出现异常,那就把起因作为消息放在异常中一起抛出去,然后在外层捕获打印。如果是业务异常,那就warn级别,意料之外的异常那就error级别。
有时候日志非常多,担心肉眼难以在一堆输出找到目标日志,或者希望自己关注的日志能在茫茫多的日志中一眼就能看到,于是就加了一大堆的“+”或一大堆的“=”或一大堆的“-”或一大堆的“*”,基本上就是希望以分割线带消息的方式来彰显相应的日志内容。这种虽然看起来很醒目,但是非常浪费日志硬盘的空间,第一次看的时候会看的很爽,但是我个人是不推荐的。
我倒是有一个大胆的想法,入口处的日志以“STA)-”开头,也就是“STA)-Entering Method getCachedMap, params:[arg1, arg2, ...]”,然后出口处的日志以“END)-”开头,也就是“END)-Leaving Method getCachedMap, return:result”。STA代表start,END代表end,)圆括号看起来比较柔和,-用以作分隔并连接。
如果有上下文联系且存在逻辑深度的日志,我觉得可以使用“+”来体现逻辑深度,例如:
“STA)-Entering Method getCachedMap, params:[arg1, arg2, ...]”,
“+-Condition1 is setup, so try A”
“++-Condition2 is setup, so try B”
“END)-Leaving Method getCachedMap, return:result”
一般来说,代码的逻辑深度不要超过三层,最好逻辑深度是<3的,所以一般也不会出现下面这种
“++++++++-ConditionX is setup, so try X”的情况。
在写日志的时候,除了考虑要便于阅读之外,还要考虑要便于分析。日志分析不只是我们自己对日志分析,还有其他人或其他程序对日志的分析。日志日志,无非就是记录到底干了些啥,通过对日志进行分析我们有时能获得一些额外的有用的信息。讲道理,日志在输出的时候是应该要带上线程号的,那么我们可以很容易拿到某个时间段在这个线程上作出的操作,但是程序中可能是多线程的操作,所以我们还要考虑到流程链路的追踪。
我觉得从某种角度来讲,日志分析的主要手段应该就是一个Key-Value的分析,就是我打印了什么日志就代表我执行了什么样的代码,打印了多少次就代表执行了多少次,还有就是代表执行结果的日志表明对应的执行状态。
所以,日志打得快就好了,分析的事情,再不济我们可以人肉来做嘛。
在使用占位符来打印日志的时候,一个是要避免空指针的情况,还有一个就是虽然使用占位符的时候我们不需要再判断isDebugEnable这种,但是当你占位符中的参数本身就会有一个比较耗时的操作的时候,那该作判断的就还是要if isDebugEnable判断一下,比如你这个对象要打印出对应的json结果,或者你的这个参数需要Arrays.toString一下,那这种情况,我觉得还是加上一下判断会比较好,因为就是你不需要做日志内容的字符串拼接,但是你json序列化和数组toString的操作还是会执行的。
5、流程控制、校验以规避异常、控制异常的影响范围
流程控制就是选择执行哪些指令走那条线。我们常用的有if判断,这其中可以作很多比较,但是会顺序向下执行判断的代码;还有switch case的流程控制本质上是只支持对整型的判断与查找,特别是当条件语句特别多大于5的时候,综合效率是要比if ... else if ... else高的,因为在执行的时候会根据编码的时候的情况优化成偏移量查找或二分查找,而当条件情况的case数小于某个数的时候就还是会挨个比较跟多个if判断没什么区别。
对于异常,分为受检查的异常和非检查型异常两大类。检查型的异常如果在代码中不作处理则是无法通过编译的,也就是说这个异常是已知在某种情况下一定会出现的,你必须要对其作出相应的处理。如果这个异常是不允许存在的,那我将之改正确不就好了嘛,但是如果这个可能产生的异常需要由调用处来决定是否处理和怎样处理,那这就是检查型异常存在的意义了,就是这个方法明确通知你这里是可能出现异常的,需要你调用时作出处理。
从某种角度来讲,运行时异常就属于那种编码中不允许出现的异常也就是很严重的问题,检查型异常就是那种需要你判断该如何处理的异常。因为你不能说数组下标越界了你还能正常向下处理,这很严重;当你在通过SimpleDateFormat来parse一个String为日期,而对你这个String无法解析就会产生异常,这是很明确的已知的情况,产生的原因是无法匹配解析的规则,这个方法可能出现的问题也并不是运行时的严重问题,不合适直接在这里尝试终止流程,所以就需要通知方法调用处处理这个异常,于是如果方法调用处没有对这个异常进行处理就会编译报错。
finally代码块是无论如何都会执行的代码。我们最好不要在这里面进行return操作。不过,执行的原则仍然是从上到下执行。在这个过程中发现了一个小细节,如果return一个i++,只会return这个i的值,而如果return一个++i,则会返回i自增1之后的值。也就是说哪怕是return,是先return拿到了++之前的值,之后的这个i才进行了自增。
异常一定要处理,绝对不能就e.printStackTrace()就完了,最不济最不济也要用日志记录一下,要不然到时候真出了问题查都查不了。
总的来说,我们进行流程控制的时候要根据情况来选择是使用if还是使用switch,我们不要使用异常来进行流程控制,因为抛出一个异常往往意味着这个地方是出现了问题,我们如果能提前检查来避免这个异常才是正确的。不过如果出现了异常我们还是需要对异常进行处理,虽然从形式上看确实是走了另外一个流程,但从上下文逻辑和语义上看并不是进行流程控制。
我认为在return时最好不要return一个null,因为这样会容易因为流程太长或一时疏忽而产生空指针,有这样一个规则:无论何时方法都应避免返回null,null仅用来表示“未初始化”或“不存在”的语义。我们可能会遇到这样的场景,就是我们作出了校验并不通过,我们希望能终止某几个步骤但不影响另外几个步骤。这种场景我见过通过return一个努力了,然后判断,然后再return一个null,再向上判断,然后再结束流程。我认为这样是非常不妥的,主要是语义不明,然后还容易造成空指针。我认为这种情况就应该规划考虑一个异常链,异常产生处就抛出具体的异常类,上层catch的异常类就越往上越抽象,通过catch的异常范围来控制异常的影响范围。
6、工程的规划中那些命名定义的细节
工程在动工之前一般都是会有一个规划的,至少会有一个大体的框架。聚沙成塔,看你是拿水和的还是用泥拌的还是用水泥混的还是用502粘的,也就是我们准备用什么基础架构来整合零散的功能点。一般来说,只是提供功能的构件要么就直接new出来的对象,要么就是Factory,在要么就是一个Manager。如果是一个系统的应用工程,我们就常用Spring来进行构件,因为Spring带来的优势在于非侵入式的编程,会非常方便地进行工程的整合和功能的编排。
根据依赖注入这个理念,我们可以开发好自己的单元性的功能点,然后将这些单独的功能点通过Spring来整合到一起,这样我们在保证单元性的功能点没问题时,再调试整合之后的结果即可。若是需求有变,我们在确保输入输出不变的情况下,调整依赖的编排即可,也就是不用改代码了。单元性的功能我们就用502粘得牢固一些,功能编排的情况,我们就使用Spring来处理,使之牢靠又灵活。
那么在使用Spring的时候,我们常用的是依赖注入和面向切面的编程,依赖注入主要是要考虑究竟是要单例还是多实例,然后是否是必要的装配。AOP面向切面编程这个东西,现在我们似乎用得有些少,因为某些业务的原因,我们似乎没法对很多方法进行切面编程,就算是rest权限这种,我们也是通过统一的拦截器来完成的。不过,对于AOP我想着重说说命名风格与规范的事情。
如果命名没有一个原则,AOP的切点是很不好找的。还有包名的命名也是讲究一个规律。我们常常根据MVC的分层来看就是controller层或rest层,然后就是service层,最后就是dao层。现在我们的开发实践中,我们几乎已经没有dao层了,因为jdbcTemplate直接执行sql然后返回结果已经非常便捷,而且对于一些sql拼接的复杂逻辑来讲orm映射反倒来得不方便。然而这些命名都非常传统,我们现在要处理的不只是简单的页面和简单的数据交互了,我们现在很多业务的关联性很复杂了,业务和业务之间的相互依赖也越来越深,不仅包要分层,我认为命名也要分层。
这时我又有了一个大胆的想法。我们写出来的代码无非就是来提供服务的,从某个角度来讲要么是查询服务、要么是处理服务。
查询的就以query打头作为方法名,这个可以参考JPA的Repository的方法命名方式,因为查询也就那样了,传入参数并返回结果。新增的话,我们有这么几个词可以选择:create、add、insert、save,考虑到业界曾流行过saveOrUpdate的命名方法,我个人觉得就用save打头作为方法名。更新就update打头,删除有remove和delete,我选择delete打头的方法。
处理的服务,我们常用的有handle、process、dispose、deal with、treat with、operate、execute这些单词,当要形成类名的时候会使用其动词的形式。我们一些核心处理我们会使用core这个单词,一些内部的处理会使用internal来命名。
从语义上讲,handler偏向于事务的处理,因为handle的名词含义是手柄或操纵杆,或者称之为句柄,有一个由一端来操控另一端的内在概念。processor偏向于数据的加工处理,因为process的本意是“过程”,有加工、工序的意思.所以,如果是纯处理型的代码,命名就推荐使用Processor,如果是接收外部操作进行内部处理的代码,命名就推荐使用Handler。
而Dispose从语义上讲,属于那种销毁性质的处理,类似于“乃伊组特,把他给处理掉”、“把这坨给处理掉”、“清仓大甩卖降价处理”、“污水处理厂”的“处理”,但是很多时候我们并不是要对传入或接收的数据进行这种带有销毁色彩的处理,所以如果是数据分拣,我们常有assort、assemble这样的动词,我觉得这些会比较适合这种功能语境,或者也可以使用提炼、萃取这样的词比如extract。
再就是JavaBean的相关命名,我觉得就按照Spring的套路就叫bean,而且java自己的包中也叫beans,那这种类型的类就放到bean这层包下,个人不推荐entity、pojo、po、vo、domain这种名字。entity本就是实体的意思,确实是能准确表达意思的,只是我认为Java的风格就应该要叫做bean。有些人喜欢称之为Pojo,这些想必是从Ejb时代过来的,根据我的搜索分析,pojo是简单的老式的Java对象,既然这样称呼那就意味着历史上曾经存在过复杂的新式的Java对象,而Spring的一大亮点就是不使用Ejb那一套也能构建一个完整有效的web工程,所以我认为Pojo是相对于Ejb来讲的。至于po、vo甚至dto、dfo、bo这些,我就觉得这个简称非常不好理解,我也不太认同这些缩写是业界约定俗成的东西,谁能想到po是persist object是持久化对象,而我第一次看到就以为是parameter object理解成参数对象。domain时领域的含义,我查了一下,我认为这个包下的类的存在的意义表示的就是这个领域的内容,但是其实最后还是一些JavaBean。我个人推荐这种类型的类的命名就以Bean来作为后缀,例如XxxParamBean、XxxResultBean、XxxBean就好。
考虑到AOP,我们的命名需要有一定的共通性,要有一个一致的规律,这样会更有利于复用或者说能更方便地将切面切入到切点,但是我个人还是觉得在命名的时候尽可能地避免common这样的词出现,因为common本就是共通的意思,使用不当就很容易造成语义不明。你不能就只是因为这个包下的类或类中的方法是公共的大家都会用到的,你就给他命名为XXXcommonXXX,这样很草率,因为这个公共性或者说这个共通性是有局限,也就是说这个common也是可以在某个范围下进行明确定义的,所以我们宁愿稍微多花一点时间来考虑一下这个命名的问题,而不是草草地命名为common就了事。Apache的Jakarta项目下的commons系列,后面也跟了后缀,比如commons-io,所以我们的具体业务项目应该要命名地更加具体一些,只不过范围更大一些。比如公共数据处理工具,这个就出现了common,我们应该要尝试把范围再缩小一些,分析归纳这些公共数据有哪些共同的特征,将这个特征作为这个数据处理类的类型定语。比如图表数据处理工具,其中就可以包含加权平均价的处理方法、移动平均价的处理方法、精度控制的处理方法、涨跌点涨跌幅的计算方法等等,当然这个大的公共处理数据方法还可以进一步细分,这个就要看工程设计的结构和粒度了。
不过我们在设计处理相似度超过55%的几个业务时,我们应该考虑更高一层的抽象。代码中要考虑抽象父类的字段和方法,数据库表设计要考虑使用含义相同的数据库字段,这样代码上可以复用,在sql上也可以复用了。重要的是sql上的复用。我们常用的orm框架都是面向对象的,一查就是把整个对象的数据都查出来,但是很多时候我们的业务场景并用不到那些多余的数据,我们为什么不要什么就查什么呢?虽然可能多写了几条SQL,但是如果表结构有超过55%的相似,我们大多数时候就可以直接替换表名直接复用方法了。当我们在优先考虑代码的复用之后,若一旦有非常特别的情况出现,我们也可以专门为这些业务特例再去写特别的专门的方法。
再回过头说工程的命名和包的命名,当我们的工程名是由多个单词或缩写组成的,那我们会常用中横杠来连接,但是我们在包命名的时候一般会将中横杠换成点“.”作为包的一个层级。再往下层级的包命名就是根据自己的具体业务逻辑来定,只要能准确地表达意思即可,比如一个名为配置的包名,你定义为config也行,conf也行,configure也行,configuration也行,cfg也行,按照团队或公司的风格来,如果没有没有团队的风格约规这种,那我推荐config这种相对比较全的单词来作包名。
还有工程中的常量,原则上是可以定义在一个公共工程中,但是一旦String类型和基本类型的常量内容发生了变更,相关联的工程的依赖需要依赖到对应版本的公共工程,因为这些类型的常量是会静态编译的。常量中不要定义数组,因为这样会出现数组内的值被变更的情况,所以我们可以在常量类中定义枚举,通过枚举来拿到数组。当然,对于那些同属一个概念的常量最好是定义成枚举值,因为这样类型会更加凝练一些,不过若是有静态编译的需求,那多定义几个常量就多定义几个没问题,这个时候在常量名加上前缀以作分组会比较好。