最近看了一些项目代码,清一色的就3个模板:
(代码示例省去开启事务的部分)
public Response createOrder(Request req) {
checkParam(req);
Order order = convertTo(req);
saveData(order);
return buildResponse(order);
}
public Response updateOrder(Request req) {
checkParam(req);
Order order = findOrder(req.getOrderId());
Assert.notNull(order, "cannot find order");
updateOrder(order, req);
saveData(order);
return buildResponse(order);
}
public Response queryOrder(Request req) {
checkParam(req);
Order order = findOrder(req.getOrderId());
return buildResponse(order);
}
细心的你一定看出来了,这不就是增改查吗?(为啥没有delete 因为现在大厂对数据管得严,基本上不允许进行delete操作)
出现这样的代码也是一种必然性,因为这种模式容易复制,对程序员的要求低,不管是谁,只要看懂了需求,直接就能上手干。
我之前还在菊厂打工的时候,还有人把这样的代码封装成模板,很贴心有没有,可以省掉一次copy!!(菊厂躺枪,但其他人也别笑,90%的人都写过这样的代码)
写这样的代码,工作是完成了,但长此以往,编程能力是没长进的,凭心而论,这样的代码写出来有成就感吗?或许刚入行的同学会觉得这种代码看起来很“干净”,事实上这样的代码完全没有结构可言,长期一定是难以维护。
怎样让代码有“结构性”,看看这一篇《为什么说用例设计在软件开发中很重要》,或许对你有些帮助
我认为程序员应该是最富有创造力的一类人,千万别把自己变成一个只会ctrl+c、ctrl+v的机器,工作8年、10年还只会CRUD,还谈什么提升?今天教大家三招,只需在代码中融入一些架构思维,瞬间让你的代码提升一个档次。
上面提供的范例都称为“面条式代码”,为什么这种面条式代码会难以维护?
checkParam可能带有业务逻辑(校验业务合法性、校验余额、校验状态等)、convertTo也有业务逻辑(核心模型order的构造就是业务逻辑)、甚至有些saveData里也有逻辑(save的时候不放心前面的检查结果,有时还要再检查一遍),业务规则是零散的。
另外,如果业务上有多种不同的createOrder呢?例如自营渠道下单、合作渠道下单、自营还分成线上/线下模式,这些看起来很类似又有些许不同的逻辑就会先copy一份,再微调一下,导致规则难以维护。或者干脆不copy,直接在原来的代码上写if/else,屎山代码不就是这么来的?
举一个订单状态校验的例子,原来的状态流转:订单创建->待支付->已支付->已发货->已签收,后面业务规则变了,要支持先用后付,这要改多少处代码?至少要把各种不同的createOrder入口都检查一遍,checkParam要改,convertTo要改,saveData可能也要改。
一个好的做法是,给订单建立一个领域模型,而且是充血模型,业务规则都放在模型内部:
// 不提供setter方法,保证逻辑内聚
@Getter
@Builder
public class Order {
private OrderId orderId;
// 订单状态,通过状态机组件来管理
private OrderStatus status;
private List<OrderItem> items;
private Timestamp createTime;
private UserId buyerId;
private PayOrderId payOrderId;
//...
// 订单支付
public void pay(Timestamp payTime, long payAmount, PayOrderId payOrderId) {
// 1.校验订单状态
// 2.校验支付金额是否正确
// 3.修改payOrderId
// 4.修改订单状态
}
}
通过这样封装以后,如果订单状态的逻辑发生变更,就只需修改Order即可。
还可以进行一些其他调整:
@Service
public class OrderAppService {
// 库存领域服务
InventoryService inventoryService;
// 用户领域服务
UserService userService;
// 订单领域服务
OrderService orderService;
public OrderAppService(...) {// 依赖注入,省略...}
// xx渠道下单
// AppService层需要开启事务
@Transactional
public Response createOrderByXxxChannel(@Valid CreateOrderRequest req) {
// 参数检查由框架层做了,业务代码里就不需要重复做
// 构建订单的逻辑,包装在factory里
// 创建订单的同时进行订单规则校验,生成订单号,判断是否幂等
Order order = OrderFactory.newOrder(...);
// 校验用户的状态
// 具体要不要引入缓存,由用户模块来决定
bool userAvailable = userService.available(order.getBuyerId);
Assert.true(userAvailable, "用户状态异常");
// 锁定库存
// 具体是通过数据库实现还是缓存实现,由库存模块来决定,这里不需要关心
inventoryService.lock(order.getItemType(), order.getQuantity());
// 订单领域服务,负责处理订单
// 包括订单信息入库,入库前的必要检查,发送订单创建的领域事件等
// 入库前的必要检查也不是直接在service里面写,可能是调用order.readyToPay()来更新订单状态,核心的逻辑是在order里。往下翻有代码示例
orderService.createOrder(order);
return buildResponse(order);
}
}
为了让大家更容易看懂,我添加了很多注释,但实际上把中文注释都去掉,是不是基本上也能看懂?这里去掉了所有技术的术语,而是替换为有业务语义的函数名称。
或许会有人质疑,你这样改完以后不还是变成了另一种代码模板吗?非要这么说的话也可以,这是一种无法copy的模板,因为每个业务的逻辑都不相同,你的思维方式已经转变为不再是为了把数据存进数据库,而是把业务逻辑搞明白,使其更加内聚,这是一个非常大的转变。其实,做好“高内聚、低耦合”不就是走向架构师的第一步吗?
如果你要问我作为一名架构师最需要的思维方式是什么? 我会告诉你:识别,并隔离变化。
1.把容易变的和不变的隔离开 2.把业务规则和技术实现隔离开 3.把业务主流程中的强依赖和弱依赖隔离开
短短三句话,其实很考验架构师的基本功,很多代码的性能、可维护性、可扩展性有问题,追到根上就是隔离没做好。而隔离变化常见的方式有:
举几个例子:
// 领域事件的例子
// 订单领域服务
@Service
public class OrderService {
OrderRepository orderRepository;
EventPublisher eventPublisher;
public OrderService(...) {// 依赖注入,省略...}
// 领域服务层的入参通常是Entity或ValueObject
public void createOrder(Order order) {
// 调用实体的readyToPay,做一些事前校验,变更订单状态等
order.readyToPay();
// app service已经开了事务,这里直接调用repository的save
// 数据存储的细节由repositoryImpl负责,领域层不需要关心
orderRepository.save(order);
// 发布领域事件,这个事件由订阅器消费,至于后面是发通知还是其他,就不关心了
// 事件并不一定都是异步的,更多是为了解耦和隔离
// 具体是同步还是异步,在事件组件里去配置,领域服务中不需要关心
Event e = Event.createEvent(EventCode.ORDER_CREATED, order);
eventPublisher.publish(e);
}
// 订单支付,也同样以Entity作为入参
public void pay(Order order, PayOrder payOrder) {
// ...
}
}
到这里我介绍了一些方法,但也别忘了我们最终的目的:隔离变化。好些同学学会了这些招式以后进入另一个极端,逮住一个地方就开始做拆分,加各种AOP,导致代码反而变得越来越复杂。切记时刻思考隔离的本质(上面说的三句话),才能让架构思维得到进一步提升。
抽象能力也是衡量一个架构师水平的尺子。
之前听过一个段子:把大象放进冰箱需要分几步?答案是三步:1. 打开冰箱,2. 把大象放进去,3. 关上冰箱。 这从一定程度上说也是一种抽象,但这种抽象就显得很生硬,没法落地。
抽象分为对过程的抽象和对结构的抽象。
前者多数人是熟悉的,上面提到的CRUD模板还有把大象装进冰箱,都是对过程的抽象,但需要注意抽象不能脱离了业务流程,否则就会像CRUD模板那样生搬硬套,不解决业务实际问题。
这里所说的“业务流程”是泛指,如果你恰好在做一个跑批框架,可以认为你现在面对的“批处理”这件事就是业务流程,虽然它看起来是个技术的东西。这里就提供一个对批处理进行抽象的例子:
// 抽象的批处理任务
@Slf4j
public abstract class AbstractTask {
private String taskId;
private TaskType type;
private TaskStatus status;
private String param;
private int retryTimes;
public AbstractTask(...) {// ...}
// 任务的执行入口,支持传入参数
public void ExecuteTask(String param) {
// 前置条件判断
bool canRun = preCheck();
if (!canRun) {
return;
}
log.info("task started")
try {
doExecute();
status = TaskStatus.SUCCESS;
} catch (BusinessException e) {
ErrorCode c = e.getErrorCode();
// 根据错误码判断是否可以重试,更新定时任务状态
if (c == ErrorCode.XXX) {
status = TaskStatus.WAITING_TO_RETRY;
retryTimes++;
} else {
status = TaskStatus.FAILED;
log.error("task run failed!", e);
}
} catch (Throwable e) {
status = TaskStatus.FAILED;
log.error("task run failed!", e);
}
// 后置处理,不影响任务执行结果,例如发送通知等可以放这里
try {
afterSuccess();
} catch (...) {
// ...
}
}
protected abstract void preCheck();
protected abstract void doExecute();
protected abstract void afterSuccess();
}
经过这样抽象之后,对“批处理”这个业务流程就可以进行统一,而不需要所有的批处理任务都实现一遍。具体的任务可以继承AbstractTask,实现3个抽象方法即可。
另一类对结构的抽象,举一个例子说吧,这个例子可能不一定恰当。例如现在有多种不同的订单类型:实物订单、虚拟物品订单,这两种订单的库存判断方式不一样,发货方式也不一样,虚拟物品不涉及物流。之前锁定库存的接口是:InventoryService.lock(ItemType type, int quantity)
,因为增加了订单类型,接口就要改为InventoryService.lock(ItemType type, int quantity, OrderType t)
,将来有没有可能再增加别的判断要素?这样库存的接口要改,订单模块作为调用方也要改。这时候可以考虑对订单做一个抽象:
public interface Order {
ItemType getItemType();
int getQuantity();
OrderType getOrderType();
}
// 抽象之后库存接口就可以改为
public class InventoryService {
public void lock(Order order) {
OrderType orderType = order.getOrderType();
if (orderType == OrderType.VIRTUAL) {
// 虚拟订单
}
}
}
这样订单模块就不需要再感知库存模块的变化,订单实体加了一个getOrderType()方法也并没有破坏订单这个实体的内聚性。(不过这个例子确实有些不太好,我再考虑一下给一个更好的例子)
架构能力非一朝一夕之功,需要刻意练习,不要抱怨说“我都没有做大项目的机会,没机会锻炼架构能力”。其实我们的日常工作就是锻炼架构能力最好的机会,努力写好每一行代码,自然就能成为优秀的架构师。
本文发表于公众号:支付进阶之路。
作者louis,腾讯架构师,有多年大型支付架构设计经验,DDD和支付领域专家
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。