概述
同事离职跑路,留下一大堆的坑等你填;简单需求,却需要大改代码;修改一处代码,却影响另一处代码;代码耦合度太高,牵一发而动全身;系统的维护成本越来越大。
作为一个开发人员,我相信大家或多或少都面临过这些比较普遍的困扰。但是受限于技术水平,面对这些困扰时往往一筹莫展。想要写出优秀的代码,好像唯有提升技术水平一途。然而,技术水平的提升绝非一日之功。因此,我最近一直在思考,试图为公司制定出一套基于面向对象思想的编码规范,只要遵守该规范,就可以相对写出稳定性好、可读性强,并且可扩展性高的代码,使得整个系统呈现出高内聚、低耦合的特性。
但是如何写出一款优秀的程序,这是一个比较大的话题,一篇文章很难面面俱到,所以,我打算用多篇文章从不同的角度来阐述。好了,废话不多说,本文的主题是围绕 封装
和 单一
两个角度,提炼开发规范。
封装的概念
前面已经讲过,整篇文章都是围绕 封装
和 单一
这两个核心展开分析的,因此,在正式分析之前,我们必须对这两个概念了如指掌。首先来讲 封装
的概念。
所谓 封装
,官方给出的定义是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。 而百度百科给出的解释是,封装是把过程和数据包围起来,对数据的访问只能通过已定义的接口。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。封装是一种信息隐藏技术。封装把对象的所有组成部分组合在一起,封装定义程序如何引用对象的数据,封装实际上使用方法将类的数据隐藏起来,控制用户对类的修改和访问数据的程度。 适当的封装可以让程序代码更容易理解和维护,也加强了程序代码的安全性。
上面对封装概念的解释,看似抽象,实则非常精炼,正如百度百科中所说,面相对象计算始于封装的概念,这句话足以说明封装在面相对象中的重要性了,说它是面相对象思想的灵魂也不为过。
单一的概念
首先说明,这里的单一,指的是 单一职责原则
或 单一功能原则
,说法不同,含义相同。
原则是指经过长期经验总结所得出的合理化的现象,是人类言行所依据的准则。在面向对象程序设计中,也同样存在原则,比如 单一职责原则
,它是我们开发中的一个最基础的、必须遵守的原则性指导思想。
深入的描述待补充 。。。
项目代码
下面,我用实际工作中的项目代码作为示例,先分析代码中存在的问题,然后给出解决方案,最后再从解决方案中提炼出开发规范。
项目背景
我们公司开发了一套自己的支付系统,为内部其他系统提供基础的支付服务。系统集成了支付宝支付、微信支付、中通支付、汇潮支付等
10
家第三方支付公司。每家第三方支付公司提供10
个支付方式,比如微信支付提供的二维码支付、H5支付、小程序支付、公众号支付等。这样算下来,支付系统一共对接了100
个支付方式。
需求描述
用户发起支付时,支付系统创建订单。订单创建完成后五分钟之内未完成支付,则更新订单状态为
过期未支付
,支付成功时,更新订单状态为支付成功
,支付失败时,更新订单状态为支付失败
过期未支付对应的状态码为
3
,支付失败对应的状态码为2
,支付成功对应的状态码为1
。
代码实现
{.tabset}
订单服务
订单服务接口:
public interface OrderService {
/**
* 通用更新方法
*/
boolean updateOrder(Order order);
}
订单服务实现类:
@Service("orderService")
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
/**
* 通用更新方法
*/
public boolean updateOrder(Order order) {
return orderDao.update(order);
}
}
支付服务
支付系统中共有 100
个支付方式,每个支付方式对应着一个支付服务。这里,我们用微信公众号支付方式对应的支付服务(如无特别说明,后面简称支付服务)作为示例:
/**
* 微信公众号支付服务
*/
@Slf4j
@Service
public class JsapiPayment {
@Resource
private OrderService orderService;
/**
* 微信支付结果回调通知,根据支付结果,调用订单服务更新订单状态
*/
public void paymentResultCallback(String orderNo, String paymentResult) {
// 处理微信公众号支付相关逻辑......
Order order = new Order();
order.setOrderNo(orderNo);
if ("支付成功".equals(paymentResult)) {
order.setOrderStatus("1");
} else if ("支付失败".equals(paymentResult)) {
order.setOrderStatus("2");
}
order.setUpdateTime(new Date());
orderService.updateOrder(order);
}
}
代码分析
认真读完代码后,大家的第一感觉,这样的代码是否有问题?如果单纯从当前功能实现的角度来看,代码简洁易读,逻辑清晰明了,好像完全没有问题。但是,通过个人感觉来衡量一个程序的好坏,这是远远不够的,我们需要从 稳定性
、可读性
、可扩展性
等多个角度综合判断。
如果不满足 单一功能原则
,一个类或方法负责多个功能,必然会导致各个功能之间相互存在耦合,如果某个功能需要修改时,不得不考虑是否会对其他功能带来影响,甚至,修改一个功能,会直接导致其他功能无法使用。这就不符合低耦合的要求,程序的稳定性、可读性和可扩展性都很差。
封装是一种信息隐藏技术,如果 隐藏
做的不够好,把自己的数据对外暴露,那么外部就可以直接操作数据,外部的错误可能会影响内部逻辑,如果内部的数据结构发生变化,同样可能会影响到所有的外部调用方,且这样的影响面更大。这就不合符高内聚的要求,我自己的数据自己内部维护,绝不让外人指手画脚,或者说,自己的数据高度聚集在自己类的内部,这就是高内聚。
至此,该做的铺垫做了,该说的概念也说完了,现在就正式进入代码的分析阶段吧。
分析一
订单服务的本质就是为了维护订单数据,包括订单的创建,订单状态的变更,历史订单的查询,垃圾订单的删除等。所以,Order
实体属于订单服务的数据。但是,上面的代码中,支付服务依赖了 Order
实体,这样的代码,高内聚、低耦合
方面做的是非常差的。
如果某天来了,让我把支付成功的订单状态码修改为 PAY-SUCCEED
,我们至少需要修改 100
个支付服务中的状态码,那如果订单新增或删除一个状态码呢,对支付服务的影响又有多大?要是在现实世界中,我想支付服务肯定要骂娘了。作为支付服务,我为什么要关心你订单中的破状态码?你订单服务把状态码封装到自己内部,你想怎么改就怎么改,对我们广大的支付服务没有任何影响。各行其道,互不影响,这样不香吗?
分析二
过了几天,某天又来了,让我把 Order
实体中的状态码字段的名称改掉,这样会出现什么结果?毫无疑问,广大的支付服务又要受伤了,上百个支付服务全部报错。还是那句话,作为支付服务,我为什么要关心你订单中某个数据的字段名?你订单服务把你的所有数据都封装到自己内部,你想怎么改就怎么改,对我们广大的支付服务没有任何影响。各行其道,互不影响,这样不香吗?
分析三
再过几天,某天再次来了,提出了新的要求,当订单支付成功时,给用户发送一封邮件,订单支付失败时,给用户发送一条短信。不出意外,上百个支付服务悲催了,再次躺枪,所有支付服务中,所有出现成功状态码的地方,均要调用邮件服务,所有出现失败状态码的地方,均要调用短信服务。站在邮件服务和短信服务角度,人家只需要和订单服务建立关联关系就行了,你非要让他们和上百个支付服务都要建立关联关系。有些小聪明可能会想到,在订单服务提供的通用更新方法 updateOrder(Order order)
中,使用 if...else...
判断订单状态,然后根据判断结果选择调用短信服务或邮件服务,这样一来,邮件服务和短信服务只和订单有关联关系,且只需要修改订单服务的通用更新方法即可。这样真的可行吗?我只能说,你太年轻了。原因是订单服务提供的通用更新方法 updateOrder(Order order)
违背了 单一职责原则
,它的功能很强大,可以更新订单中的所有字段。假如一个已经处于支付失败状态的订单,调用方调用此方法希望把订单的付款金额改小一点,让穷酸用户也可以支付得起,此时,更新方法的操作仅仅是更新金额,你却通过判断订单状态为支付失败,再次给用户发送了一条支付失败的短信,这是不是就出BUG了?人家本来就有点穷,第一次收到支付失败短信就感觉挺尴尬的,你非要再发一条给人家,往人家伤口上撒盐。记住,在修改违背 单一职责原则
的方法时,一定要考虑到所有业务场景的方方面面,否则,一不小心就会捡到BUFF
分析四
随着公司发展越来越好,系统中各服务的数据量越来越大,传统应用已经无法满足业务需要。现在需要把系统中的订单服务单独抽离出来,作为一个独立的微服务。上面的代码又该如何应对这样的变化?大牛哥写的代码,可以轻松应对业务的千变万化,而上面的代码呢,如果业务经常变化,你的头发还能保住几根?
对于上面的代码,缺点说完了,我们再来聊聊优点。但是,好像实在是找不到什么优点,如果硬要找一个,可能是订单服务的代码量稍微少一点吧。对于喜欢看优点的人,请移步下面的优化方案。对比上方的四个分析来阅读下方的优化方案效果最好。
优化方案
{.tabset}
订单服务
订单服务接口:
public interface OrderService {
/**
* 订单支付成功
*/
boolean orderPaySucceed(String orderNo);
/**
* 订单支付失败
*/
boolean orderPayFail(String orderNo);
}
订单服务实现类:
@Service("orderService")
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
/**
* 订单支付成功
*/
public boolean orderPaySucceed(String orderNo) {
// 这里扩展邮件相关逻辑......
Order order = new Order();
order.setOrderNo(orderNo);
order.setOrderStatus("1");
order.setUpdateTime(new Date());
return orderDao.update(order);
}
/**
* 订单支付失败
*/
public boolean orderPayFail(String orderNo) {
// 这里扩展短信相关逻辑......
Order order = new Order();
order.setOrderNo(orderNo);
order.setOrderStatus("2");
order.setUpdateTime(new Date());
return orderDao.update(order);
}
}
支付服务
支付系统中共有 100
个支付方式,每个支付方式对应着一个支付服务。这里,我们用微信公众号支付方式对应的支付服务(如无特别说明,后面简称支付服务)作为示例:
/**
* 微信公众号支付服务
*/
@Slf4j
@Service
public class JsapiPayment {
@Resource
private OrderService orderService;
/**
* 微信支付结果回调通知,根据支付结果,调用订单服务更新订单状态
*/
public void paymentResultCallback(String orderNo, String paymentResult) {
// 处理微信公众号支付相关逻辑......
if ("支付成功".equals(paymentResult)) {
orderService.orderPaySucceed("ORDER267652C4490AI03");
} else if ("支付失败".equals(paymentResult)) {
orderService.orderPayFail("ORDER267652C4490AI03");
}
}
}
规范总结
待补充。。。