转载自:https://www.zhihu.com/question/60761181/answer/1737592739
分享一套使用Spring特性优雅书写业务代码的方法。
大家在日常业务开发工作中相信多多少少遇到过下面这样的几个场景:
- 当某一个特定事件或动作发生以后,需要执行很多联动动作,如果串行去执行的话太耗时,如果引入消息中间件的话又太重了;
- 想要针对不同的传参执行不同的策略,也就是我们常说的策略模式,但10个人可能有10种不同的写法,夹杂在一起总感觉不那么优雅;
- 自己的系统想要调用其他系统提供的能力,但其他系统总是偶尔给你一点“小惊喜”,可能因网络问题报超时异常或被调用的某一台分布式应用机器突然宕机,我们想要优雅无侵入式地引入重试机制。
其实上面提到的几个典型业务开发场景Spring都为我们提供了很好的特性支持,我们只需要引入Spring相关依赖就可以方便快速的在业务代码当中使用啦,而不用引入过多的三方依赖包或自己重复造轮子。下面我们就来看看Spring提供的强大魔力吧。
【点击头像关注我们知乎号,别错过更多阿里工程师一线技术干货。】
————————————————————————————————————
使用Spring优雅实现观察者模式
观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,其主要解决一个对象状态改变给其他关联对象通知的问题,保证易用和低耦合。一个典型的应用场景是:当用户注册以后,需要给用户发送邮件,发送优惠券等操作,如下图所示。
使用观察者模式后:
UserService 在完成自身的用户注册逻辑之后,仅仅只需要发布一个 UserRegisterEvent 事件,而无需关注其它拓展逻辑。其它 Service 可以自己订阅 UserRegisterEvent 事件,实现自定义的拓展逻辑。Spring的事件机制主要由3个部分组成。
- **ApplicationEvent:**通过继承它,实现自定义事件。另外,通过它的 source 属性可以获取事件源,timestamp 属性可以获得发生时间。
- **ApplicationEventPublisher:**通过实现它,来发布变更事件。
- **ApplicationEventListener:**通过实现它,来监听指定类型事件并响应动作。这里就以上面的用户注册为例,来看看代码示例。首先定义用户注册事件 UserRegisterEvent。
publicclass UserRegisterEvent extends ApplicationEvent {
/**
* 用户名
*/
private String username;
public UserRegisterEvent(Object source) {
super(source);
}
public UserRegisterEvent(Object source, String username) {
super(source);
this.username = username;
}
public String getUsername() {
return username;
}
}
然后定义用户注册服务类,实现 ApplicationEventPublisherAware 接口,从而将 ApplicationEventPublisher 注入进来。从下面代码可以看到,在执行完注册逻辑后,调用了 ApplicationEventPublisher的 publishEvent(ApplicationEvent event) 方法,发布了 UserRegisterEvent 事件。
@Service
publicclass UserService implements ApplicationEventPublisherAware { // <1>
private Logger logger = LoggerFactory.getLogger(getClass());
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
public void register(String username) {
// ... 执行注册逻辑
logger.info("[register][执行用户({}) 的注册逻辑]", username);
// <2> ... 发布
applicationEventPublisher.publishEvent(new UserRegisterEvent(this, username));
}
}
创建邮箱Service,实现 ApplicationListener 接口,通过 E 泛型设置感兴趣的事件,实现 onApplicationEvent(E event) 方法,针对监听的 UserRegisterEvent 事件,进行自定义处理。
@Service
publicclass EmailService implements ApplicationListener<UserRegisterEvent> { // <1>
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
@Async// <3>
public void onApplicationEvent(UserRegisterEvent event) { // <2>
logger.info("[onApplicationEvent][给用户({}) 发送邮件]", event.getUsername());
}
}
创建优惠券Service,不同于上面的实现 ApplicationListener 接口方式,在方法上,添加 @EventListener 注解,并设置监听的事件为 UserRegisterEvent。这是另一种使用方式。
@Service
publicclass CouponService {
private Logger logger = LoggerFactory.getLogger(getClass());
@EventListener// <1>
public void addCoupon(UserRegisterEvent event) {
logger.info("[addCoupon][给用户({}) 发放优惠劵]", event.getUsername());
}
}
看到这里,细心的同学可能想到了发布订阅模式,其实观察者模式于发布订阅还是有区别的,简单来说,发布订阅模式属于广义上的观察者模式,在观察者模式的 Subject 和 Observer 的基础上,引入 Event Channel 这个中介,进一步解耦。图示如下,可以看出,观察者模式更加轻量,通常用于单机,而发布订阅模式相对而言更重一些,通常用于分布式环境下的消息通知场景。
使用Spring Retry优雅引入重试机制
如今,Spring Retry是一个独立的包了(早期是Spring Batch的一部分),下面是使用Spring Retry框架进行重试的几个重要步骤。第一步:加入Spring Retry依赖包
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.1.2.RELEASE</version>
</dependency>
第二步:在应用中包含main()方法的类或者在包含@Configuration的类上加上@EnableRetry注解 第三步:在想要进行重试的方法(可能发生异常)上加上@Retryable注解
@Retryable(maxAttempts=5,backoff = @Backoff(delay = 3000))
public void retrySomething() throws Exception{
logger.info("printSomething{} is called");
thrownew SQLException();
}
在上面这个案例当中的重试策略就是重试5次,每次延时3秒。详细的使用文档看这里,它的主要配置参数有下面这样几个。其中exclude、include、maxAttempts、value几个属性很容易理解,比较看不懂的是backoff属性,它也是个注解,包含delay、maxDelay、multiplier、random四个属性。
- **delay:**如果不设置的话默认是1秒
- **maxDelay:**最大重试等待时间
- **multiplier:**用于计算下一个延迟时间的乘数(大于0生效)
- **random:**随机重试等待时间(一般不用)
Spring Retry的优点很明显,第一,属于Spring大生态,使用起来不会太生硬;第二,只需要在需要重试的方法上加上注解并配置重试策略属性就好,不需要太多侵入代码。
但同时也存在两个主要不足,第一,由于Spring Retry用到了Aspect增强,所以就会有使用Aspect不可避免的坑——方法内部调用,如果被 @Retryable 注解的方法的调用方和被调用方处于同一个类中,那么重试将会失效;第二,Spring的重试机制只支持对异常进行捕获,而无法对返回值进行校验判断重试。如果想要更灵活的重试策略可以考虑使用Guava Retry,也是一个不错的选择。
优雅使用Spring特性完成业务策略模式
策略模式相信大家都应该比较熟悉,它定义了一系列的算法,并将每一个算法封装起来,使每个算法可以相互替代,使算法本身和使用算法的客户端分割开来,相互独立。
其适用的场景是这样的:一个大功能,它有许多不同类型的实现(策略类),具体根据客户端来决定采用哪一个策略类。比如下单优惠策略、物流对接策略等,应用场景还是非常多的。
举一个简单的例子,业务背景是这样的:平台需要根据不同的业务进行鉴权,每个业务的鉴权逻辑不一样,都有自己的一套独立的判断逻辑,因此需要根据传入的 bizType 进行鉴权操作,首先我们定义一个权限校验处理器接口如下。
/**
* 业务权限校验处理器
*/
publicinterface PermissionCheckHandler {
/**
* 判断是否是自己能够处理的权限校验类型
*/
boolean isMatched(BizType bizType);
/**
* 权限校验逻辑
*/
PermissionCheckResultDTO permissionCheck(Long userId, String bizCode);
}
业务1的鉴权逻辑我们假设是这样的:
/**
* 冷启动权限校验处理器
*/
@Component
publicclass ColdStartPermissionCheckHandlerImpl implements PermissionCheckHandler {
@Override
public boolean isMatched(BizType bizType) {
return BizType.COLD_START.equals(bizType);
}
@Override
public PermissionCheckResultDTO permissionCheck(Long userId, String bizCode) {
//业务特有鉴权逻辑
}
}
业务2的鉴权逻辑我们假设是这样的:
/**
* 趋势业务权限校验处理器
*/
@Component
publicclass TrendPermissionCheckHandlerImpl implements PermissionCheckHandler {
@Override
public boolean isMatched(BizType bizType) {
return BizType.TREND.equals(bizType);
}
@Override
public PermissionCheckResultDTO permissionCheck(Long userId, String bizCode){
//业务特有鉴权逻辑
}
}
可能还有很多其他的业务鉴权逻辑,这里就不一一列举了,实现逻辑像上面这样组织就好了。接着就到了关键的地方了,上面我们定义了这么多策略,应该怎么优雅的组织起来呢,这就需要用到Spring提供的一些扩展特性了,Spring主要为我们提供了三类扩展点,分别对应不同Bean生命周期阶段:
- Aware接口
- BeanPostProcessor
- InitializingBean 和 init-method
我们这里用到的主要是 Aware 接口和 InitializingBean 两个扩展点,其主要用法如下代码所示,关键点就在于实现 ApplicationContextAware 接口的 setApplicationContext 方法和 InitializingBean 接口的 afterPropertiesSet 方法。
实现 ApplicationContextAware 接口的目的就是要拿到 Spring 容器的资源,从而方便的使用它提供的 getBeansOfType 方法(该方法返回的是 map 类型,key 对应 beanName, value 对应 bean);而实现 InitializingBean 接口的目的则是方便为 Service 类的 handlers 属性执行定制初始化逻辑。
可以很明显的看出,如果以后还有一些其他的业务需要制定相应的鉴权逻辑,我们只需要编写对应的策略类就好了,无需再破坏当前 Service 类的逻辑,很好的保证了开闭原则。
/**
* 权限校验服务类
*/
@Slf4j
@Service
publicclass PermissionServiceImpl
implements PermissionService, ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
//注:这里可以使用Map,偷个懒
private List<PermissionCheckHandler> handlers = new ArrayList<>();
@Override
public PermissionCheckResultDTO permissionCheck(ArtemisSellerBizType artemisSellerBizType, Long userId,
String bizCode) {
//省略一些前置逻辑
PermissionCheckHandler handler = getHandler(artemisSellerBizType);
return handler.permissionCheck(userId, bizCode);
}
private PermissionCheckHandler getHandler(ArtemisSellerBizType artemisSellerBizType) {
for (PermissionCheckHandler handler : handlers) {
if (handler.isMatched(artemisSellerBizType)) {
return handler;
}
}
returnnull;
}
@Override
public void afterPropertiesSet() throws Exception {
for (PermissionCheckHandler handler : applicationContext.getBeansOfType(PermissionCheckHandler.class)
.values()) {
handlers.add(handler);
log.warn("load permission check handler [{}]", handler.getClass().getName());
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
当然在这里相信不少同学会有疑问,那就是这里在获取 handler 处理器 bean 的时候,所有的 bean 是不是已经初始化好了?会不会存在有的 handler 还没有初始化好的情况?
答案是不会的,Spring Bean 的声明周期保证了这一点(当然前提是 handler 自身不会有特殊的初始化逻辑)。经过实际验证,所有的 handler 会在 Service 初始化操作前 ready,感兴趣的同学可以编写代码验证,可以先在相应钩子处打上日志直接输出结果验证,然后在 Spring 源码关键处打上断点 debug,相信会有不少收获。
总结&思考
公司里的有些代码有点年龄,有些类写的又臭又长,很多地方充斥着代码坏味道,如重复的代码,过长的参数列,散弹式修改,基本型偏执等等,不一一展开。每天要面对这些代码进行开发,不仅消磨了我们对技术的热情也让人变得毫无斗志,很多同学会想——反正都已经这样了,那我也就这么来吧,相信不少小伙伴都有这样的遭遇与困惑。
但唯一不能停下来的就是进步,即使面对恶龙还是不能放弃抵抗。当然,在做需求的时候,很多时候也不能去修改那些代码,太耗时太费劲,风险太大。那自己起码也要思考一下如何设计代码才能去避免以后出现同样的情况,让自己下次不要犯同样的错误。
当我们在实际编写代码的时候,需要留意探索一下Spring有没有为我们提供一些已有的工具类和扩展点。一方面,使用Spring提供的这些特性可以让我们少造轮子,避免引入其他比较重的类库;另一方面,Spring对JDK等库提供的一些类和规范进行了抽象封装,易用性更好,更贴合开发者需求。
作者:阿里云网站
链接:https://www.zhihu.com/question/60761181/answer/874296743
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
写好代码,阿里专家沉淀了一套“如何写复杂业务代码”的方法论,在此分享给大家,相信同样的方法论可以复制到大部分复杂业务场景。
一文教会你如何写复杂业务代码
了解我的人都知道,我一直在致力于应用架构和代码复杂度的治理。
这两天在看零售通商品域的代码。面对零售通如此复杂的业务场景,如何在架构和代码层面进行应对,是一个新课题。针对该命题,我进行了比较细致的思考和研究。结合实际的业务场景,我沉淀了一套“如何写复杂业务代码”的方法论,在此分享给大家。
我相信,同样的方法论可以复制到大部分复杂业务场景。
一个复杂业务的处理过程
业务背景
简单的介绍下业务背景,零售通是给线下小店供货的B2B模式,我们希望通过数字化重构传统供应链渠道,提升供应链效率,为新零售助力。阿里在中间是一个平台角色,提供的是Bsbc中的service的功能。
在商品域,运营会操作一个“上架”动作,上架之后,商品就能在零售通上面对小店进行销售了。是零售通业务非常关键的业务操作之一,因此涉及很多的数据校验和关联操作。
针对上架,一个简化的业务流程如下所示:
过程分解
像这么复杂的业务,我想应该没有人会写在一个service方法中吧。一个类解决不了,那就分治吧。
说实话,能想到分而治之的工程师,已经做的不错了,至少比没有分治思维要好很多。我也见过复杂程度相当的业务,连分解都没有,就是一堆方法和类的堆砌。
不过,这里存在一个问题:即很多同学过度的依赖工具或是辅助手段来实现分解。比如在我们的商品域中,类似的分解手段至少有3套以上,有自制的流程引擎,有依赖于数据库配置的流程处理:
本质上来讲,这些辅助手段做的都是一个pipeline的处理流程,没有其它。因此,我建议此处最好保持KISS(Keep It Simple and Stupid),即最好是什么工具都不要用,次之是用一个极简的Pipeline模式,最差是使用像流程引擎这样的重方法。
除非你的应用有极强的流程可视化和编排的诉求,否则我非常不推荐使用流程引擎等工具。第一,它会引入额外的复杂度,特别是那些需要持久化状态的流程引擎;第二,它会割裂代码,导致阅读代码的不顺畅。大胆断言一下,全天下估计80%对流程引擎的使用都是得不偿失的。
回到商品上架的问题,这里问题核心是工具吗?是设计模式带来的代码灵活性吗?显然不是,问题的核心应该是如何分解问题和抽象问题,知道金字塔原理的应该知道,此处,我们可以使用结构化分解将问题解构成一个有层级的金字塔结构:
按照这种分解写的代码,就像一本书,目录和内容清晰明了。
以商品上架为例,程序的入口是一个上架命令(OnSaleCommand), 它由三个阶段(Phase)组成。
@Command
public class OnSaleNormalItemCmdExe {
@Resource
private OnSaleContextInitPhase onSaleContextInitPhase;
@Resource
private OnSaleDataCheckPhase onSaleDataCheckPhase;
@Resource
private OnSaleProcessPhase onSaleProcessPhase;
@Override
public Response execute(OnSaleNormalItemCmd cmd) {
OnSaleContext onSaleContext = init(cmd);
checkData(onSaleContext);
process(onSaleContext);
return Response.buildSuccess();
}
private OnSaleContext init(OnSaleNormalItemCmd cmd) {
return onSaleContextInitPhase.init(cmd);
}
private void checkData(OnSaleContext onSaleContext) {
onSaleDataCheckPhase.check(onSaleContext);
}
private void process(OnSaleContext onSaleContext) {
onSaleProcessPhase.process(onSaleContext);
}
}
每个Phase又可以拆解成多个步骤(Step),以OnSaleProcessPhase
为例,它是由一系列Step组成的:
@Phase
public class OnSaleProcessPhase {
@Resource
private PublishOfferStep publishOfferStep;
@Resource
private BackOfferBindStep backOfferBindStep;
//省略其它step
public void process(OnSaleContext onSaleContext){
SupplierItem supplierItem = onSaleContext.getSupplierItem();
// 生成OfferGroupNo
generateOfferGroupNo(supplierItem);
// 发布商品
publishOffer(supplierItem);
// 前后端库存绑定 backoffer域
bindBackOfferStock(supplierItem);
// 同步库存路由 backoffer域
syncStockRoute(supplierItem);
// 设置虚拟商品拓展字段
setVirtualProductExtension(supplierItem);
// 发货保障打标 offer域
markSendProtection(supplierItem);
// 记录变更内容ChangeDetail
recordChangeDetail(supplierItem);
// 同步供货价到BackOffer
syncSupplyPriceToBackOffer(supplierItem);
// 如果是组合商品打标,写扩展信息
setCombineProductExtension(supplierItem);
// 去售罄标
removeSellOutTag(offerId);
// 发送领域事件
fireDomainEvent(supplierItem);
// 关闭关联的待办事项
closeIssues(supplierItem);
}
}
看到了吗,这就是商品上架这个复杂业务的业务流程。需要流程引擎吗?不需要,需要设计模式支撑吗?也不需要。对于这种业务流程的表达,简单朴素的组合方法模式(Composed Method)是再合适不过的了。
因此,在做过程分解的时候,我建议工程师不要把太多精力放在工具上,放在设计模式带来的灵活性上。而是应该多花时间在对问题分析,结构化分解,最后通过合理的抽象,形成合适的阶段(Phase)和步骤(Step)上。
过程分解后的两个问题
的确,使用过程分解之后的代码,已经比以前的代码更清晰、更容易维护了。不过,还有两个问题值得我们去关注一下:
1、领域知识被割裂肢解
什么叫被肢解?因为我们到目前为止做的都是过程化拆解,导致没有一个聚合领域知识的地方。每个Use Case的代码只关心自己的处理流程,知识没有沉淀。
相同的业务逻辑会在多个Use Case中被重复实现,导致代码重复度高,即使有复用,最多也就是抽取一个util,代码对业务语义的表达能力很弱,从而影响代码的可读性和可理解性。
2、代码的业务表达能力缺失
试想下,在过程式的代码中,所做的事情无外乎就是取数据–做计算–存数据,在这种情况下,要如何通过代码显性化的表达我们的业务呢? 说实话,很难做到,因为我们缺失了模型,以及模型之间的关系。脱离模型的业务表达,是缺少韵律和灵魂的。
举个例子,在上架过程中,有一个校验是检查库存的,其中对于组合品(CombineBackOffer)其库存的处理会和普通品不一样。原来的代码是这么写的:
boolean isCombineProduct = supplierItem.getSign().isCombProductQuote();
// supplier.usc warehouse needn't check
if (WarehouseTypeEnum.isAliWarehouse(supplierItem.getWarehouseType())) {
// quote warehosue check
if (CollectionUtil.isEmpty(supplierItem.getWarehouseIdList()) && !isCombineProduct) {
throw ExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR, "亲,不能发布Offer,请联系仓配运营人员,建立品仓关系!");
}
// inventory amount check
Long sellableAmount = 0L;
if (!isCombineProduct) {
sellableAmount = normalBiz.acquireSellableAmount(supplierItem.getBackOfferId(), supplierItem.getWarehouseIdList());
} else {
//组套商品
OfferModel backOffer = backOfferQueryService.getBackOffer(supplierItem.getBackOfferId());
if (backOffer != null) {
sellableAmount = backOffer.getOffer().getTradeModel().getTradeCondition().getAmountOnSale();
}
}
if (sellableAmount < 1) {
throw ExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR, "亲,实仓库存必须大于0才能发布,请确认已补货.\r[id:" + supplierItem.getId() + "]");
}
}
然而,如果我们在系统中引入领域模型之后,其代码会简化为如下:
if(backOffer.isCloudWarehouse()){
return;
}
if (backOffer.isNonInWarehouse()){
throw new BizException("亲,不能发布Offer,请联系仓配运营人员,建立品仓关系!");
}
if (backOffer.getStockAmount() < 1){
throw new BizException("亲,实仓库存必须大于0才能发布,请确认已补货.\r[id:" + backOffer.getSupplierItem().getCspuCode() + "]");
}
有没有发现,使用模型的表达要清晰易懂很多,而且也不需要做关于组合品的判断了,因为我们在系统中引入了更加贴近现实的对象模型(CombineBackOffer继承BackOffer),通过对象的多态可以消除我们代码中的大部分的if-else。
过程分解+对象模型
通过上面的案例,我们可以看到有过程分解要好于没有分解,过程分解+对象模型要好于仅仅是过程分解。对于商品上架这个case,如果采用过程分解+对象模型的方式,最终我们会得到一个如下的系统结构:
写复杂业务的方法论
通过上面案例的讲解,我想说,我已经交代了复杂业务代码要怎么写:即自上而下的结构化分解+自下而上的面向对象分析。
接下来,让我们把上面的案例进行进一步的提炼,形成一个可落地的方法论,从而可以泛化到更多的复杂业务场景。
上下结合
所谓上下结合,是指我们要结合自上而下的过程分解和自下而上的对象建模,螺旋式的构建我们的应用系统。这是一个动态的过程,两个步骤可以交替进行、也可以同时进行。
这两个步骤是相辅相成的,上面的分析可以帮助我们更好的理清模型之间的关系,而下面的模型表达可以提升我们代码的复用度和业务语义表达能力。
其过程如下图所示:
使用这种上下结合的方式,我们就有可能在面对任何复杂的业务场景,都能写出干净整洁、易维护的代码。
能力下沉
一般来说实践DDD有两个过程:
1. 套概念阶段
了解了一些DDD的概念,然后在代码中“使用”Aggregation Root,Bonded Context,Repository等等这些概念。更进一步,也会使用一定的分层策略。然而这种做法一般对复杂度的治理并没有多大作用。
2. 融会贯通阶段
术语已经不再重要,理解DDD的本质是统一语言、边界划分和面向对象分析的方法。
大体上而言,我大概是在1.7的阶段,因为有一个问题一直在困扰我,就是哪些能力应该放在Domain层,是不是按照传统的做法,将所有的业务都收拢到Domain上,这样做合理吗?说实话,这个问题我一直没有想清楚。
因为在现实业务中,很多的功能都是用例特有的(Use case specific)的,如果“盲目”的使用Domain收拢业务并不见得能带来多大的益处。相反,这种收拢会导致Domain层的膨胀过厚,不够纯粹,反而会影响复用性和表达能力。
鉴于此,我最近的思考是我们应该采用能力下沉的策略。
所谓的能力下沉,是指我们不强求一次就能设计出Domain的能力,也不需要强制要求把所有的业务功能都放到Domain层,而是采用实用主义的态度,即只对那些需要在多个场景中需要被复用的能力进行抽象下沉,而不需要复用的,就暂时放在App层的Use Case里就好了。
注:Use Case是《架构整洁之道》里面的术语,简单理解就是响应一个Request的处理过程
通过实践,我发现这种循序渐进的能力下沉策略,应该是一种更符合实际、更敏捷的方法。因为我们承认模型不是一次性设计出来的,而是迭代演化出来的。
下沉的过程如下图所示,假设两个use case中,我们发现uc1的step3和uc2的step1有类似的功能,我们就可以考虑让其下沉到Domain层,从而增加代码的复用性。
指导下沉有两个关键指标:代码的复用性和内聚性。
复用性是告诉我们When(什么时候该下沉了),即有重复代码的时候。内聚性是告诉我们How(要下沉到哪里),功能有没有内聚到恰当的实体上,有没有放到合适的层次上(因为Domain层的能力也是有两个层次的,一个是Domain Service这是相对比较粗的粒度,另一个是Domain的Model这个是最细粒度的复用)。
比如,在我们的商品域,经常需要判断一个商品是不是最小单位,是不是中包商品。像这种能力就非常有必要直接挂载在Model上。
public class CSPU {
private String code;
private String baseCode;
//省略其它属性
/**
* 单品是否为最小单位。
*
*/
public boolean isMinimumUnit(){
return StringUtils.equals(code, baseCode);
}
/**
* 针对中包的特殊处理
*
*/
public boolean isMidPackage(){
return StringUtils.equals(code, midPackageCode);
}
}
之前,因为老系统中没有领域模型,没有CSPU这个实体。你会发现像判断单品是否为最小单位的逻辑是以StringUtils.equals(code, baseCode)
的形式散落在代码的各个角落。这种代码的可理解性是可想而知的,至少我在第一眼看到这个代码的时候,是完全不知道什么意思。
业务技术要怎么做
写到这里,我想顺便回答一下很多业务技术同学的困惑,也是我之前的困惑:即业务技术到底是在做业务,还是做技术?业务技术的技术性体现在哪里?
通过上面的案例,我们可以看到业务所面临的复杂性并不亚于底层技术,要想写好业务代码也不是一件容易的事情。业务技术和底层技术人员唯一的区别是他们所面临的问题域不一样。
业务技术面对的问题域变化更多、面对的人更加庞杂。而底层技术面对的问题域更加稳定、但对技术的要求更加深。比如,如果你需要去开发Pandora,你就要对Classloader有更加深入的了解才行。
但是,不管是业务技术还是底层技术人员,有一些思维和能力都是共通的。比如,分解问题的能力,抽象思维,结构化思维等等。
用我的话说就是:“做不好业务开发的,也做不好技术底层开发,反之亦然。业务开发一点都不简单,只是我们很多人把它做“简单”了
因此,如果从变化的角度来看,业务技术的难度一点不逊色于底层技术,其面临的挑战甚至更大。因此,我想对广大的从事业务技术开发的同学说:沉下心来,夯实自己的基础技术能力、OO能力、建模能力… 不断提升抽象思维、结构化思维、思辨思维… 持续学习精进,写好代码。我们可以在业务技术岗做的很”技术“!。
后记
这篇文章是我最近思考的一些总结,可能需要一些DDD和应用架构的知识做铺垫。否则的话,有些地方可能会显得有些唐突,或是没有那么有体感。
有时间的话,可以去看看《领域驱动设计》和《架构整洁之道》去了解一些前序知识。如果没有那么多时间,也可以快速浏览下我之前的文章领域建模去知晓一下我之前的思想脉络。