再谈设计和编码
这篇文章谈下编码中的一些细节问题,在谈之前首先还是要继续推荐下《代码大全》这本书,该书在很多公司都会被推荐为新人入职后的必读书,即使在互联网和敏捷开发环境下,该书对于新人仍然有仔细阅读和体会的必要,要明白架构和设计思维的基础仍然是编码思维,面向对象思维的基础仍然是代码本身的逻辑和结构,如果不能写出高质量的代码,那么架构和面向对象思想很多内容往往难以真正落地。
对于这篇文章,结合一个最简单的业务功能场景来讲解,即采购订单新增这个业务功能和场景。
拿到这个功能需求后当然是首先要详细阅读具体的业务功能需求,在阅读完后第一需要思考的不是业务流程,不是数据库表,而是首先应该思考该功能对应的核心领域对象究竟是什么?以这个功能为例,可以很明细的看到核心对象是采购订单,这是一个有明确业务含义的对象,可以看到后续很多的业务操作和方法都将围绕采购订单这个对象展开。核心主对象明确后再分析该功能需要依赖的附属对象,比如:
- 供应商:基础主数据对象,依赖原因是需要在创建采购订单的时候选择供应商
- 采购类型:数据字典类对象,依赖原因同样需要在创建订单的时候选择
- 订购商品:基础主数据对象,一张订单在创建的时候都必须要选择购买的一个或多个商品和数量信息
该问题做了基本的思考后,再回答我们具体使用的语言和开发框架,比如基于标准的java ssh框架来开发该功能,要明白我们一般选择了某种框架后基本的分层思路就已经确定了。当我们拿到一个现成的框架后,在基于这种框架做完练习后,首先要思考的就是这种框架本身的运行机制是如何的?各层之间的调用逻辑和分工是如何的?各层有哪些约束和边界,在业务实现过程中涉及到的业务规则,逻辑和数据处理究竟应该放在哪层去实现。这些问题必须在编码中搞清楚,而不是依葫芦画瓢而不知其所以然。
对于我们使用的分层框架基本运行机制和逻辑了解清楚后,基于拿到业务功能需求要开始思考的就不是框架和分层的问题了,而是我们应该在每个分层中设计哪些类?每个类应该有哪些核心方法?类和类之间如何衔接和实现内部调用。单表功能做多了最容易犯错的地方就是完全的数据库表式思维模式,即任何一个数据库表都会有对应的展现页面,控制类,service类和dao类,完全一对一映射和调用。在这种思维模式下忽略了最前面思考的对象的本质,在领域模型里面一个核心就是我们关心的是有明确业务含义的对象,而不是数据库表。数据库表和dao层只是在最终持久化要做的事情而已。
基于上面的分析可以看到,对于订单创建的时候供应商信息的获取应该是供应商类完成的职责,对于采购类型可能则是一个数据字典的common类完成的职责。而对于核心的订单类可以看到,我们需要的领域服务或方法很简单,即:
订单应该做为一个核心的领域对象来处理,但是实际在后台操作则涉及到OrderHeader和OrderDetail两张数据库表,任何一张订单的保存都涉及到对两张数据库表的操作。可以看到如果对应的业务功能没有明确的对订单明细的操作规则和方法,那么在Service层没有必要根据头和明细定义两个Service类,只需要一个OrderService类即可。在该类中实现所有的订单保存前数据准备和逻辑校验。
在这一个步骤想清楚后,即对于订单新增功能可以看到,即在页面和展现层进行新增订单操作,在点击保存按钮的时候应该将页面上的订单数据信息传递到action层进行基本的数据完整性校验,然后再将订单数据以json或已经实例化好的订单实体类传递到逻辑层的savePurchaseOrder方法去处理。而savePurchaseOrder要做的事情就是进行数据准备和转换,再调用Dao层的订单头和订单明细保存方法进行数据保存,并控制好事务处理。
基于该逻辑思路我们就基本可以写出各个分层的核心方法和方法实现,实现最基本的订单保存功能。在整个过程中我们始终围绕订单这个核心对象展开。逻辑层的savePurchaseOrder这个方法是核心,即从展现层到此是准备好订单实例数据,从该层到Dao层是实现数据最终的持久化和事务控制。
对于业务规则可以分为两类规则,即一类规则是数据参考完整性规则,比如数据类型,数据的长度,2个数据属性间简单控制逻辑(比如订单类型为A时候,发运地址必须输入等);还有一类即较为复杂的数据处理规则或需要调用后台数据库实现的控制逻辑(订单总金额>1000的时候用户信用等级必须>B级,在订单保存前还需要再实时检查商品库存信息是否足够等)。对于两类规则的基本原则就是第一类可以在action层实现,而第二类数据则需要在业务逻辑层来实现。
对于逻辑层的savePurchaseOrder方法最终应该相当简单,即首先进行业务规则教育,在通过后再调用订单保存方法进行订单保存操作。类似如下:
{
if !validBusinessRule1(order) return;
if !validBusinessRule2(order) return;
//校验通过后调用订单保存方法
saveOrderInfo(order);
}
在该实现中可以看到首先进行了子方法的拆分,保持订单保存方法本身的简洁和代码可读性。其次需要考虑对于拆分的处理规则的方法是否需要拆分到单独的业务规则类里面。这里要看情况来处理,即对于业务规则本身有比较高的复用性时候最好拆分为单独的业务规则类来处理(比如在订单分发或订单拆分业务功能中仍然需要使用同样的业务规则,那么规则单独拆分到类是有必要的),如果功能确实够简单也可以考虑不拆分。
以上都思考完成后基本的框架和设计实现逻辑就都清楚了,下一步则过渡到具体的编码实现环节。基于任何语言的编码其核心都是算法和数据结构,而在这两者里面最基本的又是变量和数据类型,程序控制逻辑(判断和循环等),变量定义的规范性,数据类型选择的合理性,控制逻辑代码的清晰度都将直接影响到编码本身的健壮性和可读性,这些最基本的内容才是写出高质量代码的关键,也是类似《代码大全》书籍所一直强调的内容。
代码本身具备足够的自解释性,源代码的设计核心就是我们的命名,方法的拆分,控制逻辑要清晰,代码具备足够的可读性往往则不再需要过多的注释。子方法的拆分是另外一个重点,对于子方法的拆分不仅仅是考虑到方法的复用性问题,有时候一两行代码也需要拆分,其核心原因包括两个,一个是拆分后增加了代码的可读性,其次是增加代码的可扩展性,即后续拆分的子方法往往存在规则或逻辑变更和扩展的可能等。
任何一段代码本身的结构化和逻辑化是面向对象编程的基础,而结构化后最直观的体现就是代码的可读性和可维护性。软件质量的衡量不仅仅是简单功能的实现,而是在各种非常规场景下的边界和异常处理能力,因此代码本身的健壮性是另外一个重点。任何在编码阶段的工作都必须时刻关注这两个重点,才可能不断提升最基本的编程思维和编码能力。