面向过程OR面向对象
面向过程的代码
在说面向对象前我们来说说什么是面向过程。什么是面向过程呢?
“面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了; ”
举个栗子,比如之前项目组做的付款业务,这里面包含了A付款,B付款,C付款,D付款等模块。
拿A付款模块来说,我们在提交付款时,要求:
代码语言:javascript复制1.提交前的业务校验(如判断付款金额,预留额度)
2.单据信息填充(金额信息,银行信息,用户信息)
3.付款信息推送第三方系统(如结算系统)
4.信息推送后更新单据信息(单据状态,更新占用额度)
5.消息通知责任人处理付款信息(邮件通知,OA通知,短信通知,微信通知)
看到这个需求我们会觉得很简单嘛,功能已经很明确,按着这个说明一行一行写代码就行了,于是我们开写:
代码语言:javascript复制public void submitPayInfo(PayInfoyCmd createParam) {
validateBeforeSubmit(createParam);
updatePayInfo(createParam);
push2PaySettle(createParam);
updateStatusAndPayApply(createParam);
notifyExecutor(createParam);
}
我们将每个功能抽象成一个方法,相应功能写在相应的方法中。这样看上没问题,我们满足了设计原则中的单一职责原则,方法尽可能的做到了短小精悍。但我们仔细读面向过程的解释:
“面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了; ”
发现这不就是我们平时代码写的么,按照常规思路,我们写成了面向过程的代码。
在我们在刚接触面向对象时就听说过面向对象了。那时书本或者网上是这么解释的
““面向对象”是专指在程序设计中采用封装、继承、多态和抽象等设计方法。 ”
那么上面的案例代码也有抽象,也有封装,为什么还是算面向过程思维呢。
那么什么是面向对象呢?
我们从哲学上来说:面向对象的基本哲学是认为世界是由各种各样具有自己的运动规律和内部状态的对象所组成的;不同对象之间的相互作用和通讯构成了完整的现实世界。因此,人们应当按照现实世界这个本来面貌来理解世界,直接通过对象及其相互关系来反映世界。这样建立起来的系统才能符合现实世界的本来面目。
这里我理解的是:
“
- 万物皆可为对象
- 对象包含了自身属性与行为
- 功能的交付其实是对象与对象之间的交互
”
那么按照这样的想法,上述代码中的校验,三方系统推送,消息通知应该属于各自对象的行为。我们需要创建各自的对象去装载各自的行为。
代码语言:javascript复制public void submitPayInfo(PayInfoCmd createParam) {
payInfoSubmitValidator.validateBeforeSubmit(createParam);
updatePayInfo(createParam);
settleApplicationService.push2PaySettle(createParam);
updateStatusAndPayApply(createParam);
notifyHandler.notifyExecutor(createParam);
}
低耦合,关注功能本身
要想写出面向对象思维的代码,我们需要遵循面向对象的原则:高内聚,低耦合。在面向对象思想中,
功能交付是对象与对象之间的交付,每个对象承担自己的工作,对象与对象之间应该尽量减少耦合。因此我们需要降低对象之间的耦合,关注对象功能本身。
我们将上述案例代码继续抽象。发现主要做了几件事情:
“
- 前置处理-校验
- 本身功能处理
- 后置处理
”
其实大部分函数方法都可以抽象成三步。
如果我们不考虑第3点的后置处理。1,2点就是我们常见的模型。对于一般方法,我们可以抽象为:
代码语言:javascript复制1.非业务
2.业务
校验
我们在复用代码时发现有的情况下是不需要有校验存在的。
例如在提交时,我们需要业务校验,但是在保存时,又不需要。为此常见做法是在入参添加一个字段:
代码语言:javascript复制private boolean needSubmit;
在写代码时,使用if
判断,如果needSubmit
传true
,调用校验方法,传false
反之。所以每次写类似代码时,我们都要为是不是一定需要校验操心。
程序员无法专注与本身业务处理,对于软件质量来说。未必是件好事。
那么这里我们需要一个低耦合,可插拔的设计。
注解
这里我决定使用注解。
“tips:善用注解,但别滥用 ”
注解虽然降低了代码耦合度,简化了开发过程。但内部使用了反射,在一定程度上牺牲了性能。
注解大家应该不陌生,我们使用Spring
系列框架开发,就一定会用到注解,但是我相信大家很少自己开发注解。
说回正题,我们如何使用注解开发校验功能呢?
我们来看直接使用示例:
上述A付款提交功能中,提交功能代码里面就是单纯的提交功能,对于校验功能,我们写在了注解中:
代码语言:javascript复制@ValidatorHandler(validators = PayInfoSubmitValidator.class)
上面的案例大家可能只会觉得:这个跟代码写在校验类里面直接调用有什么区别呢?
那我们再来看一个例子,大家都有用过导出功能,比如EasyExcel
导出。那么使用这个EasyExcel
导出时,我们可以抽象为:
1.查询数据结果集
2.调用EasyExcel方法进行导出
事实上第2点我们可以交给注解去做,程序员只需要将结果集返回即可。
那么使用注解后:
使用注解除了简化业务代码,还有一个重要功能:
例如在上面的注解校验案列中,我们使用注解校验。后续维护的时候,程序员就不需要进入主体代码,只需要在对应的校验类里面维护即可,保证功能的安全性。
利用面向对象思维简化代码
我们在编写代码时,需要思考,
“
- 这段代码是否可以重复利用
- 这段代码是否可以不写
”
关于重复利用,我们经常会做,比如抽取成公共的方法。关于代码是否可以不写,我们可能会思考的比较少,一般判断代码是否可以省略,需要看这段代码是不是通用功能。比如我们可以使用拦截器,注解,Spring
框架的AOP
来减少不必要的代码。
同样举个例子:
之前业务开发时,有一个字段接收的数据是Json
格式的,并且需要以Json
形式入库:
如上图,数据库有个字段survey_conclusion_options
数据是以Json
形式储,这里我们要求:
1.create创建时,前端准入Json形式的字符串存储;
2.查询展示时,以List对象形式展示
那么常规情况下我们会在入库时直接传Json
格式的数据,示例如下:
实体类:
代码语言:javascript复制public class ConclusionTemplate extends BaseEntity implements Serializable {
...
@ApiModelProperty(value = "结论选项(json形式存储)")
@Column(name = "conclusion_options")
private String conclusionOptionsJsonArrays;
...
}
入库:
代码语言:javascript复制public int createConclusionTemplate(ConclusionTemplate conclusionTemplate) {
BusinessExceptionAssert.checkNotNull(conclusionTemplate, "参数不能为空!!!");
conclusionTemplate.setConclusionOptionsJsonArrays("前端传入")
return this.save(conclusionTemplate);
}
展示:
代码语言:javascript复制@Data
public class ConclusionTemplateVo {
private List<ConclusionTemplateOptionVo> conclusionOptions;
}
代码语言:javascript复制public ConclusionTemplateVo getVoById(String id) {
ConclusionTemplateVo vo = new ConclusionTemplateVo();
ConclusionTemplate template = getById(id);
vo.setConclusionOptions(JsonUtils.str2list(template.getConclusionOptionsJsonArrays,ConclusionTemplateOptionVo.class));
}
这转换我们抽象为:
代码语言:javascript复制JsonArray -> JsonUtils转换 -> List实体
插入的时候我们抽象为:
代码语言:javascript复制JsonArray -> 数据库
或 List实体 -> JsonUtils转换 -> JsonArray
那么我们每次存取时都需要在List
与Json
之间转换。并且程序员需要写这段代码。
但是在面向对象的思想中,这个Json
数组中的每个元素就是一个对象,我们可不可以在代码层中以List
的形式存,然后以List
的形式取出,中间的Json
转换有程序自动去做,不需要开发去手动转。
我们可以在Entity
类中这样写:
我们在实体类中的这个字段写成List
形式的,在上面添加@ColumnType
注解。这个注解用来实现List
与Json
之间的自动互转。然后需要在Mapper.xml
文件中配置:
<result column="conclusion_options" property="conclusionOptions" typeHandler="com.xx.xx.xx.conclusion.application.ConclusionTemplateTypeHandler" />
我们在这个字段添加typeHandler
。
这样我们下代码存数据的时候就是添加List
而不是Json
了。
我们来看ConclusionTemplateTypeHandler
中的写法
通过ConclusionTemplateTypeHandler
,我们实现了Json
与List
的自动互转。
使用设计模式实现面向对象设计
设计模式相信大家很熟悉,在面试的时候也会被问道:设计模式了解么?
常见的设计模式如:单列,工厂,代理,策略等等。今天我来分享我常用的几种这几模式:
策略模式
策略模式是一种比较简单的设计模式,生活中做成一件事有几种不同的策略选择供你达成。比如你上班可以选择坐公交上班,可以选择坐地铁上班,也可以选择自驾上班,甚至还可以步行上班。
如果抽象程代码:
代码语言:javascript复制if("1".equals(status)) {
return "公交";
} else if("2".equals(status)) {
return "地铁";
} else if("3".equals(status)) {
return "自驾";
} else if("4".equals(status)) {
return "步行";
}
if语句
上面的代码有什么问题呢,就是耦合第很高,如果需要增加、移除、修改算法,需要在这里反复修改,违反了开闭原则。这个问题可以使用策略模式解决。
如何换成策略模式是什么样的呢?
策略模式
上图我们可以知道:策略模式就是将算法调用方与算法实现方分割开来,实现两者之间的解耦。
我认为策略模式优点是:
“
- 算法可自由切换
- 避免多重if-else语句
- 更好的扩展性
”
我们来举个列子:
文章开头的案列中,在提交付款最后,我们需要发消息通知,例如发OA通知:
代码语言:javascript复制notifyHandler.notifyExecutor(createParam);
这里付款提交需要发OA通知,同样的采购付款,预付款,紧急预付款也需要发OA通知。这里我们不就可以使用策略模式来做么:
通知策略
在主体方法我们只需要调用:
代码语言:javascript复制notifyStrategy.notifyExecutor(createParam);
各自模块的功能:
代码语言:javascript复制NotifyStrategy
|__ PayXXANotifyStrategy
|__ PayXXBNotifyStrategy
|__ PayXXCNotifyStrategy
|__ PayXXDNotifyStrategy
代码语言:javascript复制public interface NotifyStrategy {
void notifyExecutor(PayInfoCmd createParam);
}
各自策略类实现NotifyStrategy
。那么以后我们修改消息通知功能就只需要在策略类中修改,不需要去主体功能方法中。
观察者模式
什么是观察者模式呢?
在现实生活中,许多对象都不是独立存在的,其中一个对象的改变往往会导致其它对象的改变。比如:到了下班时间你会下班回家,路上遇到红灯你会停下来,股市行情好了你会追加投资。
观察者模式
因此我们可以抽象为:
代码语言:javascript复制功能A运行,触发了功能B的运行。
你可能不知道观察者模式这个名词,但你一定用过:例如消息队列的发布-订阅模型(生产-消费),我们拿kafka
举例:
kafka
发布消息:
ListenableFuture future = kafkaTemplate.send(topic, jsonString);
消费者订阅消息:
代码语言:javascript复制@KafkaListener(topics = "${spring.kafka.topic}")
public void listen(ConsumerRecord<?, ?> record) {
log.info("topic={}, offset={}, message={}", record.topic(), record.offset(), record.value());
}
除了消息中间件的发布-订阅模型属于观察者模式,Zookeeper
的watch
机制也使用了观察者模式。
public void watcherNode(String path) {
ZkClient connection = zookeeperConfig.getConnection();
connection.subscribeDataChanges(path, new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
log.info("路径:{}收到了节点变化:{}", dataPath, data);
}
@Override
public void handleDataDeleted(String s) throws Exception {
log.info("路径:{}收到节点删除");
}
});
}
利用Zookeeper
的watch
机制,我们可以设计出很多功能,例如设计可靠性更优良(相对于Redis
)的分布式锁,服务发现(注册中心)等。
我们又回到开头的案列,在提交付款后需要发消息通知:
代码语言:javascript复制5.消息通知责任人处理付款信息(邮件通知,OA通知,短信通知,微信通知)
这里的代码我们可能会这样写:
代码语言:javascript复制public void notifyExecutor(PayInfoCmd createParam) {
emailNotifyApplicationService.sendEmail(createParam);
oaNotifyApplicationService.sendOaMsg(createParam);
noteNotifyApplicationService.sendNote(createParam);
wechatNotifyApplicationService.pushMsg(createParam);
}
这里我们只是处理消息通知,如果付款之后还有别的操作呢?例如打印付款记录,创建订单,创建物流的等等。如果我们都写在主体代码中,后面万一撤销功能(如撤消邮件通知,微信通知),这样肯定违反了设计原则中的避开原则。
观察者模式-付款
如果我们的系统是单体系统,我们可以使用Spring
的事件机制:
public class PayInfoEvent extends ApplicationEvent {
private PayInfo payInfo;
public PayInfoEvent(Object source, PayInfo payInfo) {
super(source);
this.payInfo = payInfo;
}
public PayInfo getpayInfo() {
return payInfo;
}
public void setpayInfo(PayInfo payInfo) {
this.payInfo = payInfo;
}
}
代码语言:javascript复制@Component
@Slf4j
public class PayInfoEventPublisher {
@Autowired
public ApplicationEventPublisher applicationEventPublisher;
public void publishPayInfoEvent(PayInfo PayInfo) {
log.info("付款提交后消息推送.....");
if (PayInfo == null) {
log.info("无需推送至sap");
return;
}
applicationEventPublisher.publishEvent(new PayInfoEvent(this, PayInfo));
}
}
我们在提交付款后调用消息推送事件:
代码语言:javascript复制public void submitPayInfo(PayInfoCmd createParam) {
PayInfoSubmitValidator.validateBeforeSubmit(createParam);
updatePayInfo(createParam);
PayInfo PayInfo = BeanUtils.copy(createParam, PayInfo.class);
PayInfoEventPublisher.publishPayInfoEvent(PayInfo);
}
发送之后,需要监听并订阅:
PayInfoSapEventReceiver
@Component
@Slf4j
public class PayInfoSapEventReceiver {
@Autowired
public PayInfoService PayInfoService;
@EventListener
public void eventHandler(PayInfoEvent PayInfoEvent) {
PayInfo payInfo = PayInfoEvent.getpayInfo();
PayInfoService.putPayInfo2Sap(payInfo);
log.info("付款推送sap事件已处理完毕,单号【{}】", payInfo.getPayCode());
}
}
PayInfoOaEventReceiver
@Component
@Slf4j
public class PayInfoOaEventReceiver {
@Autowired
public PayInfoService PayInfoService;
@EventListener
public void eventHandler(PayInfoEvent PayInfoEvent) {
PayInfo payInfo = PayInfoEvent.getpayInfo();
PayInfoService.putPayInfo2Oa(payInfo);
log.info("付款推送OA事件已处理完毕,单号【{}】", payInfo.getPayCode());
}
}
依此类推,如果我们的系统是分布式的,例如消息推送,订单推送,物流推送都有各自的系统,这里需要远程调用。我们同样使用发布-订阅机制,例如使用kafka
等消息队列中间件在业务系统中发布领域事件,在各自业务系统中消费这些领域事件。
使用观察者模式需要注意:
代码语言:javascript复制消息发送成功,接收消息也成功了,但是处理消息时失败了应该怎么办。
这个问题留给大家思考。
当我们要取消某个消息推送时,我们只要将对应类中的@EventListener
注释掉即可,不需要修改主体代码。
上面介绍的策略模式和观察者模式都面向对象语言中的设计模式。
使用策略模式,我们能在面对不同的需求情况更加灵活的做出不同的策略回应,同时策略模式也提升了代码的扩展性。
使用观察者模式,我们独立了目标与观察者,降低了两者的依赖性,也是面向对象设计中低耦合的体现。
面向对象思维模式代表——基于领域驱动的设计
文章上面我们谈到领域事件。你或许会好奇:什么是领域事件?
在这之前我们需要弄明白什么是领域驱动设计。
领域驱动设计(Domain Driven Design
)是一种从系统分析到软件建模的一套方法论。以领域为核心驱动力的设计体系。
领域驱动设计适合产品化,可持续迭代,业务逻辑足够复杂的业务系统,对于系统初期业务逻辑相对比较简单的应用,传统MVC
架构更具有优势,可以减少一部分认知成本与开发成本。
限界上下文
这里我们可以简单认为一个领域即为一个服务。
我认为基于领域驱动的设计更符合面向对象设计的原则,当我们接触到需求的第一步就需要考虑领域模型,而不是将其切割成数据和行为,然后数据用数据库实现,行为使用服务实现,最后造成需求的首肢分离。
总的来说我们需要先考虑业务语言,而不是数据。
领域驱动设计将业务语义显性化,更准确的传达业务规则,因此我们可以更清晰的实现代码。
今天我们简单介绍下在代码中如何运用DDD
领域驱动设计模型
说到DDD
,人们首先会讨论充血模型与贫血模型。
贫血模型
“贫血领域对象 贫血领域对象(Anemic Domain Object)是指仅用作数据载体,而没有行为和动作的领域对象。 ”
简单来说,就是只有Getter/Setter
方法的实体。既然有贫血领域对象,那也就有充血领域对象。
充血模型
“充血领域对象 实体除了Getter/Setter方法,还有描述实体行为和动作的方法 ”
充血模型与贫血模型
在充血模型中我们的对象不只有本身的属性,还有相关的行为。来看下面代码:
上面代码是一个提交进入审批流程的方法,提交后我们需要在后台数据库记录一条提交记录,这个时候需要对数据做一些初始化,例如:初始化审批层级为第一层,初始化节点类型为提交节点,初始化删除标志为未删除。
如果按照贫血模型来写:
代码语言:javascript复制public Long submitProcess(ProcessCreateParam createParam) {
...
process.setDeleted(SystemConstant.ZERO_BYTE);
process.setProcess(SystemConstant.ONE_BYTE);
process.setType(SystemConstant.ONE_BYTE);
...
}
贫血模型中,我们将对象初始化的具体细节交给了submitProcess()
方法来做。然后从面向对象的角度来说,这些是属于对象本身需要做的事情,如果在其他方法中,我们又需要给对象标上非删除标记,初始化层级,设置提交节点。那么在这些方法又要同样的事情。我们为何不将这件事情交给对象本身来做呢?
在充血模型中我们可以这样写:
需要时我们直接通过对象调用即可:
代码语言:javascript复制public Long submitProcess(ProcessCreateParam createParam) {
...
Process process = BeanUtils.copy(createParam, Process.class);
process.unDeleted();
//初始化层级:1
process.initLevel();
//初始化节点类型:提交节点
process.submitType();
...
}
回到文章开头所说的面向对象设计,充血模型无疑是对面向对象一个很好的诠释。
关于DDD领域驱动设计,推荐书籍:
“《领域驱动设计:软件核心复杂性应对之道》 《实现领域驱动设计》 ”
为什么我们在使用贫血模型
看了上面的代码,我们可能会疑问:我使用贫血模型开发挺好的啊?为什么还要使用充血模型?也没看出什么不一样啊?
传统开发模式的贫血模型,将数据与业务彻底隔离。对象通过Getter/Setter方法修改属性,这样对象属性可能会被随意修改,从而违反了面向对象中的封装特性。
其实这样的编程方式是传统的面向过程思维编程方式,面向过程的思维方式是符合我们人类的大脑思维逻辑的,不需要太多的设计模式和过多的设计思考。其实我们在开发中存在这样的思维惯性:怎么方便怎么来,代码编写很简单,别人也容易接受。
因此我总结为什么人们更愿意使用贫血模型呢:
“
- 充血模型相对贫血模型存在一定的设计难度,你需要多花时间思考哪些是对象本身的行为
- 面向过程的编程思想根深蒂固,很难改变
- 对代码没有太大负责态度,认为怎么简单怎么来
”
我们需要使用充血模型么
DDD
领域驱动设计是应对复杂的软件设计而产出的一种思维模式,遵循面向对象的设计思想。在复杂的系统中,我们使用贫血模型(面向过程思维)开发,那最后的结果是
点连成线,线交织成网,密密麻麻不可维护
然而我们大部分负责的系统并不复杂,我的建议是:
代码语言:javascript复制朝充血模型思维方式靠齐
我的思考
如果你还在抱怨自己的工作只是简单,重复,机械的增删改查。那么建议你多做一些的思考:
代码语言:javascript复制1.我的代码是不是面向对象的代码
2.我的代码设计是否遵循 高内聚,低耦合的设计标准
3.我的代码是否遵循设计原则,如单一职责原则,开闭原则等
4. ...
从细节处提升自己。