码农戏码

新生代农民工的自我修养


  • 首页

  • 归档

  • 标签

  • 关于

  • 在线工具

  • 搜索

接口隔离原则带来的复杂性

发表于 2022-12-18
字数统计: 1.9k 字数 | 阅读时长 ≈ 7 分钟

ISP

什么是ISP,之前总结过,详细内容可回顾《SOLID之ISP》

简单总结:多餐少吃,不要大接口,使用职责单一的小接口。

just so easy!

不就是把大接口拆成小接口嘛!

然而,最近在review之前的代码时,发现了点问题。

简单介绍下背景业务知识,项目是处理发票业务,在公司报销过的人都了解,我们团建、出差,公办支出都会让商家开具一张发票,作为报销凭证。

那么一张发票在被上传到报销软件,行为分为几个部分:

1、上传识别:从一张发票图片,被OCR,识别出一份结构化数据

2、修改:修改发票信息,包括删除、编辑识别出的发票内容,甚至手工填写一张发票信息

3、验真:会调用国税接口,验证一下发票的真伪

4、查询:查看发票详情

每一部分都会有几个方法,为了避免胖接口,自然会拆分成职责更专注的小接口

使用IDEA绘制出类结构:

InvoiceVerifyService:表示发票验真职责

InvoiceDiscernService:表示发票识别职责

InoviceService:表示发票查询、编辑等职责

思路清晰,结构中正。

可在项目中却出现了一段这样的代码:

1
2
3
if(invoiceService instanceof InvoiceVerifyService){
InvoiceVerifyService verifyService = (InvoiceVerifyService)invoiceService;
}

看着instanceof关键字,就倍感别扭。要么抽象得不对,要么结构不对。

如果没有拆分成三个接口,肯定不需要这样的判断。

所以还得重新审视一下ISP。

ISP:接口隔离原则,里面两个关键词:“接口”和“隔离”;“隔离”相对比较简单,从单一职责角度,把职责不相关的行为拆分开。而“接口”则需要重新审视一下。

接口

其实每个人对接口的理解是不一样的,从分类上讲,大该两类,一是狭义:常被理解为像Java语言中的interface,或者模块内部的使用;二是广义:系统间交互契约。

Martin Fowler给了两种类型接口:RoleInterface和HeaderInterface

A role interface is defined by looking at a specific interaction between suppliers and consumers. A supplier component will usually implement several role interfaces, one for each of these patterns of interaction. This contrasts to a HeaderInterface, where the supplier will only have a single interface

大致也是这个意思。

广义

主要是系统间交互的契约。类似于一个系统的facade对外提供的交互方式。

就算你不设计接口,并不代表没有接口。不局限于语言层面的interface,而是一种契约。

最重要的原则是KISS原则,最小依赖原则或者叫最少知识原则,让人望文知义。

追求简单自然,符合惯例。

比如一个微服务用户系统提供了一组跟用户相关的 API 给其他系统使用,比如:注册、登录、获取用户信息等。

还包含了后台管理系统需要的删除用户功能,如果接口不作隔离,具体代码如下所示:

1
2
3
4
5
6
7
8
9
10

public interface UserService {
boolean register(String cellphone, String password);
boolean login(String cellphone, String password);
UserInfo getUserInfoById(long id);
UserInfo getUserInfoByCellphone(String cellphone);

boolean deleteUserByCellphone(String cellphone);
boolean deleteUserById(long id);
}

然而,删除操作只限于管理后台操作,对其他系统来讲,不仅是多余功能,还有危险性。

通过使用接口隔离原则,我们可以将一个实现类的不同方法包装在不同的接口中对外暴露。应用程序只需要依赖它们需要的方法,而不会看到不需要的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

public interface UserService {
boolean register(String cellphone, String password);
boolean login(String cellphone, String password);
UserInfo getUserInfoById(long id);
UserInfo getUserInfoByCellphone(String cellphone);
}

public interface RestrictedUserService {
boolean deleteUserByCellphone(String cellphone);
boolean deleteUserById(long id);
}

public class UserServiceImpl implements UserService, RestrictedUserService {
// ...省略实现代码...
}

狭义

狭义常被理解为像Java语言中的interface,或者模块内部的使用。

单纯某一个接口,与单一职责一样,希望接口的职责单一,不要是胖接口、万能接口。

模块内部设计时,不管是模块调用模块,还是模块调用第三方组件。

我们一般有两种选择:

一、是直接依赖所基于的模块或组件;

二、是将所依赖的组件所有方法抽象成一个接口,让模块依赖于接口而不是实现。

其实这在之前对面向对象反思的文章中,提到过,打开我们90%的项目,所有的service都有对应的service接口和serivceImpl实现,整齐划一,美其名曰,面向接口编程。

然而,到项目生命周期结束,一个service都不会有两种实现。

所以,建议还是直接依赖实现,不要去抽象。如无必要,勿增实体。

如果我们大量抽象依赖的组件,意味着我们系统的可配置性更好,但复杂性也激增。

什么时候考虑抽象呢?

1、在需要提供多种选择的时候。比如经典的Logger组件。把选择权交给使用方。

这儿也有过度设计的情况,比如数据库访问,抽象对数据库的依赖,以便在MySQL和MongoDB之间切换,在绝大数情况下,这种属于过度设计。毕竟切换数据库本身就是件小概率事件。

2、需要解除一个庞大的外部依赖。有时我们并不是需要多个选择,而是某个依赖过重。我们在测试或其它场景会选择mock一个,以便降低测试系统的依赖

3、在依赖的外部系统为可选组件时。这个时候可以实现一个mock的组件,并在系统初始化时,设置为mock组件。这样的好处,除非用户关心,否则就当不存在一样,降低学习门槛。



回到文章篇头的问题,每个接口职责都是单一明确的,为什么还需要instanceof来判别类型?其实是更上层混合使用了

类似于:

1
Map<String,InvoiceService> invoiceServiceMap = SpringUtils.getBeans();

客户端使用时,得拆分开:

1
2
3
Map<String,InvoiceVerifyService> invoiceServiceMap = SpringUtils.getBeans();
Map<String,InvoiceService> invoiceServiceMap = SpringUtils.getBeans();
Map<String,InvoiceDiscernService> discernServiceMap = SpringUtils.getBeans();

当需要具体能力时,可以从对应的集合中获取对应的Service。而不是通过instanceof去判断。通过空间的换取逻辑的明确性。

VS SRP

接口隔离原则跟单一职责原则有点类似,不过稍微还是有点区别。

单一职责原则针对的是模块、类、接口的设计。

而接口隔离原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考的角度不同。

它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

总结

表达原则的文字都很简单,但在实践时又会陷入落地时的困境。

这些原则的背后,也体现了架构之道,虚实结合之道。从实悟虚,从虚就实。

DDD工程代码模型的几种包风格

发表于 2022-10-22
字数统计: 2k 字数 | 阅读时长 ≈ 7 分钟

在团队中,一直在灌输DDD的理念,最近review一些新开发的项目时,发现工程包结构每个人的理解都是不一样的,命名也是各有特色。

因此,觉得有必要把之前整理的工程结构重新梳理下。

而在梳理的过程中,恍惚间,有种看山是山、看山不是山、看山还是山的体会。特别有意思。

传统风格

之前的总结DDD分层,每一层都是明确的。

整个工程的包结构就是这样的:

  • interface
  • application
  • domain
  • infrastraction

但是在落地时遇到了很多的问题,在DDD系列文章中也提到过:

1、循环依赖:

domain是依赖于infrastraction,但如repository接口是在domain层的,DDD也是这么定义的,但具体的ORM实现是在infrastraction。因此infrastraction又需要依赖domain。形成循环依赖。

2、domain的厚度

以前都是MVC,贫血模型。所以刚开始时,domain是很薄的,以致于没有存在感。很多service都被application干完了。常有application service与domain service区别的讨论。落地时也常搞混。

依赖倒置

不知道是不是整洁架构,还是洋葱架构之后或之前吧,依赖倒置成了程序员认知的共识。

为了脱离大泥球,人们注意到整体中各个部分的需求变化速率不同,进而通过关注点分离来降低系统复杂度。这是分层架构的起源。

倒置的原因,是因为领域层被赋于最稳定层。

1、展现层

逻辑是最容易改变的,新的交互模式以及不同视觉模板。

2、应用层

随着业务流程以及功能点的变化而改变。如流程重组和优化、新功能点引入,都会改变应用层逻辑。

3、领域层

核心领域概念的提取,只要领域概念和核心逻辑不变,基本是不变的。一旦领域层出现重大改变,就意味着重大业务调整,整个系统都被推倒重来。

4、基础设施层

逻辑由所选择的技术栈决定,更改技术组件、替换所使用的框架,都会改变基础设施层的逻辑。因而基础设施层的变化频率跟所用的技术组件有很大关系。越是核心的组件,变化就越缓慢,比如待定数据库系统后,不太可能频繁更换它,不太可能频繁地更换它。而如果是缓存系统,那么变化的频率会快很多。

但基础设施层可能存在不可预知的突变。历数过往诸多思潮,NoSQL、大数据、云计算等等,都为基础设计层带来过未曾预期的突变。

此外,周围系统生态的演化与变更,也会给基础设施层带来不可预知的突变的可能。比如,所依赖的消息通知系统从短信变成微信,支付方式从网银支付变成移动支付,等等。

整个工程的包结构就是这样的:

  • infrastraction
  • interface
  • application
  • domain

整体包结构是没有变化的,虽然理论是美好的,落地时问题依旧存在。尤其infrastraction与其它三层的不可调和的关系更浓烈了。

从以往感观,其他三层是必须要依赖infrastraction的,结果现在却在最顶层。

其实在之前文章中就提到,controller是在interface还是infrastraction,角度不同,在哪一层都可以。

而像一些基础的,如mq,应用层要发消息,怎么办呢?依赖结构决定了无法使用。

因此有人提出,基础设施层不是层的结论。每一层都是要依赖基础设施的。

菱形架构

经过了一番学习,发现了菱形架构,解决了之前的很多问题。

OHS:

对外主机服务,提供一切入口服务,分为remote和local.

remote:

提供一切对外服务,来源有传统的web,还是MQ的订阅等等。

local:

本地服务,是application的演变,如果远程服务要访问domain,必须通过local才能到达。

domain:

意义不变,就是domain

acl:

是原先infrastraction,但把范围给扩大了。把所有对外部的依赖都纳入其中,甚至repository。

port是表示接口,而adapter表示具体实现。

在《DDD实践指南》中有对菱形架构更详细的介绍。

这样解决了上述两种方案的缺点,理解起来也简单。

但后来还是不太喜欢,为啥,因为传统,传统的DDD理论中,repository是领域层,这儿却在acl中,所以一直在寻找别的方式来解决。

六边形风格

  • inputadapter
  • application
  • domain
  • outputadapter

这也是有相当数量受众的架构风格,类似于菱形风格,从外形理解也简单。

facade风格

  • facade
    • query
    • entity
    • appliation
    • adapter

这是在实践中,演变来的一种风格,对外一切都是facade,受CQRS影响

分为query查询与entity单对象的创建、更新操作;

application刚是业务原语的操作,简单理解为一个业务行为,会操作多个单entity;

adapter刚是封装的infrastraction或第三方接口,提供给外部使用。

混合格斗风格

经过一系列的学习,输出一个融合风格。

  • ohs
    • controller
      • pl
    • openapi
      • pl
    • xxljob
    • subscriber
      • mq
      • event
  • application
    • service
  • domain
    • entity
    • vo
    • aggregate
    • repository
  • acl
    • port
    • adapter
  • persistent
  • foundation
  • infrastraction
    • configuration

依赖关系:

ohs -> application

ohs -> infrastraction

请求入口都在ohs,不然是api,还是队列监听。

像队列底层属于infrastraction,但只面向接口编程,由ohs层实现。

application -> domain

domain -> foundation

application是domain的facade

domain -> acl

虽然可以通过供应商模式,其他层都依赖domain,但还有是会出来一些domain的依赖。放在acl中,供所有层使用。

这样也可以把需要主动调用的内容从infrastraction中剥离开,解决掉了以往提到的循环依赖。

回归传统风格

经过以上一系列的变化,可以说是由简到繁的过程。

再回头看经历过的项目现状,想想每次项目初始化,自己内心的纠结,在团队中也需要宣贯,需要解释,需要深化。

不如来得简单明了些,就使用最经典的DDD风格,只要有一点DDD理论知识,大家都看得明白。不会去问ohs是啥。

interface:有api、dto、assembler三个包,api接受外部请求,有传统的controller,还有rpc,callback,listener来的消息。dto就是传输对象。assembler则是interface->application时,把dto转换成application的command、query。

application: 还是CQRS的思路,分成query、command;还有event,由内部及domain抛出的event。

domain:还是核心概念,entity、vo、aggregate。但没有service,为啥,当有service时,经常会与application service相互干扰,并且会慢慢回到贫血模型。通过强制没有service,可以更加OO。

infrastraction:被拆成不同部分。

基础设施层,不单单是基础设施。得分成两种,一种像是acl,封装第三方接口;另一种像是mq,email等基础设施。

1、我们常见的mq,cache,io,email等等都是基础设施层,domain不是直接依赖他们,而是通过DIP来倒置。表现形式是domain都是接口,而基础设施变成了能力供应商。

2、而依赖的第三方接口,则是直接被domain,application调用。

因此infrastraction被分成两部分,同时解除了循环依赖的困境。

在之前文章中,提到过COLA的持久操作在application,当时很反感,后来感觉好像也对,也是供应商模式的一种体现。

总结

当然,最核心的还是domain的设计,专注修炼OO,没有丰满的domain,一切都是花架子,形似无神。

springboot2.6.x与swagger3兼容问题追踪

发表于 2022-10-12
字数统计: 1.3k 字数 | 阅读时长 ≈ 6 分钟

最近项目中使用了高版本的springboot-2.6.4,以及swagger3

1
2
3
4
5
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>

结果启动应用程序失败,报错:

1
Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException

这个问题,网上资料不少,主要原因是因为springboot2.6.x后,把pathMatcher默认值修改了,springfox年久失修,与springboot出现了兼容性问题。

找到一个Spring Boot下的Issue:https://github.com/spring-projects/spring-boot/issues/28794,但这个issue已经关闭了,目前这个问题的主要讨论在springfox,具体issue是这个:https://github.com/springfox/springfox/issues/3462

主要项目中还需要使用springboot-actuator,所以简单的修改一下配置spring.mvc.pathmatch.matching-strategy=ant-path-matcher还不行。可参考:Spring Boot 2.6.x 集成swagger3.0.0报错解决方案,Swagger is not working with Spring Boot 2.6.X

在此问题追踪过程中,第一个就是原先的Ant方式与当前的PathPattern有什么区别:

AntPathMatcher vs PathPattern

诞生时间

AntPathMatcher是一个早在2003年(Spring的第一个版本)就已存在的路径匹配器,

而PathPattern是Spring 5新增的,旨在用于替换掉较为“古老”的AntPathMatcher。

性能

PathPattern性能比AntPathMatcher优秀。

理论上pattern越复杂,PathPattern的优势越明显

功能

1、PathPattern只适用于web环境,AntPathMatcher可用于非web环境。

2、PathPattern去掉了Ant字样,但保持了很好的向下兼容性。

3、除了不支持将 ** 写在path中间之外(以消除歧义),其它的匹配规则从行为上均保持和AntPathMatcher一致

4、并且还新增了强大的{*pathVariable}的支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test() {
System.out.println("======={*pathVariable}语法======");
PathPattern pattern = PathPatternParser.defaultInstance.parse("/api/com/zhuxingsheng/{*pathVariable}");

// 提取匹配到的的变量值
System.out.println("是否匹配:" + pattern.matches(PathContainer.parsePath("/api/com/zhuxingsheng/a/b/c")));
PathPattern.PathMatchInfo pathMatchInfo = pattern.matchAndExtract(PathContainer.parsePath("/api/com/zhuxingsheng/a/b/c"));
System.out.println("匹配到的值情况:" + pathMatchInfo.getUriVariables());
}

======={*pathVariable}语法======
是否匹配:true
匹配到的值情况:{pathVariable=/a/b/c}

在没有PathPattern之前,虽然也可以通过/**来匹配成功,但却无法得到匹配到的值,现在可以了!

5、整体上可认为后者兼容了前者的功能

具体介绍可查看:
《Spring5新宠PathPattern,AntPathMatcher:那我走?》

在源码中也有详细说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
org.springframework.web.util.pattern.PathPattern

? matches one character
* matches zero or more characters within a path segment
** matches zero or more path segments until the end of the path
{spring} matches a path segment and captures it as a variable named "spring"
{spring:[a-z]+} matches the regexp [a-z]+ as a path variable named "spring"
{*spring} matches zero or more path segments until the end of the path and captures it as a variable named "spring"


Note: In contrast to org.springframework.util.AntPathMatcher,

** is supported only at the end of a pattern. For example /pages/{**} is valid but /pages/{**}/details is not. The same applies also to the capturing variant {*spring}.

The aim is to eliminate ambiguity when comparing patterns for specificity

在springboot 2.6后,Spring MVC处理程序映射匹配请求路径的默认策略已从AntPathMatcher更改为PathPatternParser

Actuator端点现在也使用基于 PathPattern 的 URL 匹配。需要注意的是,Actuator端点的路径匹配策略无法通过配置属性进行配置。

如果需要将默认切换回 AntPathMatcher,可以将 spring.mvc.pathmatch.matching-strategy 设置为 ant-path-matcher

1
spring.mvc.pathmatch.matching-strategy=ant-path-matcher

springboot2.6.X与swagger3兼容

为什么改变了下pathmatch方式,就会影响到swagger,没想明白,毕竟swagger的路径,PathPattern也是可以正常解析的。

debug了下代码:

不配置spring.mvc.pathmatch.matching-strategy

应用在启动时,会自动设置PatternPaser

可以看到默认值就是PATH_PATTERN_PARSER,也正是springboot2.6后的默认方式:

不配置时spring.mvc.pathmatch.matching-strategy,pathPatterns是被赋值的:

springfox.documentation.spring.web.WebMvcRequestHandler#getPatternsCondition时,就是null。

这样也就出现了文章开头的兼容问题。

在配置ant-path-matcher后,RequestMappingInfo中的pathPatterns和patterns的赋值变化,pathPatterns是无值,patterns是有值。

1
spring.mvc.pathmatch.matching-strategy=ant-path-matcher

解决方案

解决springboot2.6和swagger冲突的问题这篇文章算是列举方案比较全的。

如果只是通过BeanProcessor修改了HandleMapping,但不修改pathmatch,会访问不了swagger,会出现以下错误:

1
2
3
o.s.web.servlet.PageNotFound             : No mapping for GET /webjars/js/chunk-vendors.90e8ba20.js
o.s.web.servlet.PageNotFound : No mapping for GET /webjars/js/chunk-735c675c.be4e3cfe.js
o.s.web.servlet.PageNotFound : No mapping for GET /webjars/css/app.f802fc13.css

可以追加一下swagger资源的映射,最终出的方案:https://www.jianshu.com/p/1ea987c75073;

在整合actuator时,SpringBoot 2.6.* 整合springfox 3.0报错中也指出了,并且解释了原理。但没有理解作者表达的springfox.documentation.spring.web.WebMvcRequestHandler#getPatternsCondition时为null的过滤掉。

代码里面反而是把为null的提取出来了呀。

springdoc

既然swagger3.0更新不及时,就不用再纠结,直接使用springdoc也是很好的方案。

使用springdoc来替换swagger3.0,《从springfox迁移到springdoc》

总结

虽然解决了问题,但原理尚需追踪。不如用springdoc来得简单些。

架构师如何看待统一语言

发表于 2022-10-07
字数统计: 1.9k 字数 | 阅读时长 ≈ 6 分钟

DDD统一语言

统一语言,最早听到这个概念是在学习DDD的时候。

统一语言在DDD中,是一个很重要的概念。

DDD中的几个大词:问题域,解决域,战略,战术,统一语言,界限上下文

阅读全文 »

JDK8的map与flatmap区别

发表于 2022-09-25
字数统计: 586 字数 | 阅读时长 ≈ 3 分钟

map

map() method -> Data Transformation

map() takes Stream as input and return Stream

Stream map(Stream input){}

1
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

It’s mapper function produces single value for each input value.hence it is also called One-To-One mapping.

这个方法比较好理解,把一个事物映射为另一个事物,是一对一的关系。

在没有stream.map()时,就在使用apache和guava的类似api

apache中的ListUtils

1
public static <E> List<E> transformedList(final List<E> list,final Transformer<? super E, ? extends E> transformer)

guava中的Lists

1
public static <F, T> List<T> transform(List<F> fromList, Function<? super F, ? extends T> function)

flatMap

flatMap() -> map() + Flattering

flatMap() takes Stream<Stream> as input and return Stream

Stream map(Stream<Stream> input){}

1
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

It’s mapper function produces multiple value for each input value.hence it is also called One-To-Many mapping.

flattering

flatMap()其实是两个方法的合并,map()好理解,主要是flattering。

Before Flattening: [[t,u], [v,w,x], [y,x]]

After Flattening: [t,u,v,w,x,y,x]

其实就是把两层数组打平了。

实例

在stackoverflow上找的一个示例:

What’s the difference between map() and flatMap() methods in Java 8?

flatMap helps to flatten a Collection<Collection> into a Collection. In the same way, it will also flatten an Optional<Optional> into Optional.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Parcel {

String name;
List<String> items;

public Parcel(final String name, final String... items) {
this.name = name;
this.items = Arrays.asList(items);
}

public List<String> getItems() {
return items;
}

public static void main(final String[] args) {
final Parcel amazon = new Parcel("amazon", "Laptop", "Phone");
final Parcel ebay = new Parcel("ebay", "Mouse", "Keyboard");
final List<Parcel> parcels = Arrays.asList(amazon, ebay);

System.out.println("-------- Without flatMap() ---------------------------");
final List<List<String>> mapReturn = parcels.stream()
.map(Parcel::getItems)
.collect(Collectors.toList());
System.out.println("\t collect() returns: " + mapReturn);

System.out.println("\n-------- With flatMap() ------------------------------");
final List<String> flatMapReturn = parcels.stream()
.map(Parcel::getItems)
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println("\t collect() returns: " + flatMapReturn);
}

}

结果输出:

1
2
3
4
5
-------- Without flatMap() ---------------------------
collect() returns: [[Laptop, Phone], [Mouse, Keyboard]]

-------- With flatMap() ------------------------------
collect() returns: [Laptop, Phone, Mouse, Keyboard]

As you can see, with map() only:

  • The intermediate type is Stream<List>
  • The return type is List<List>

and with flatMap():

  • The intermediate type is Stream
  • The return type is List

参考

flatMap() Method in Java 8

做技术还是做管理

发表于 2022-09-12
字数统计: 1.1k 字数 | 阅读时长 ≈ 3 分钟

新晋管理者都有一些相同的烦恼和忧愁。

“管理事太杂了,没时间写代码,越来越虚”

“如何平稳技术与管理?”

“管理事太琐碎,离技术越来越远,未来职业怎么规划?”

“做管理最大的挑战,就是舍弃技术,特别难”

这些问题的本源是因为新晋管理者正在进入一个全新的领域,离开以往的舒适区。

以往的舒适区是技术范围,而且主要是技术实现。接受一个功能需求,通过技术实现出来。

而成为管理后,不再是技术实现,而是任务分配,团队建设,资源协调等等脱离技术的事项。

走出舒适区是相当痛苦的,它会让人产生不安全感,让人容易自我质疑,以及自我否定,进而退回舒适区。

新晋管理者都有类似这种前怕狼后怕虎的焦虑。

前怕狼后怕虎,狼是啥?管理之路不好走,事很杂,心里虚。虎是啥?离技术越来越远,失去了技术优势。

从自身安身立命安全感角度讲,是青黄不接的时候,管理还没有摸出门道,不知道怎么搞定,倍感焦虑;而熟练掌握的技术,却投入的时间越来越少。

这种纠结的状态,归纳为一个词叫:患得患失。还没有得到时,担心得不到;得到后,又担心失去。

阅读全文 »

DDD聚合设计的困境

发表于 2022-08-28
字数统计: 1.9k 字数 | 阅读时长 ≈ 6 分钟

为什么学了一堆DDD理论,但就是无法落地呢?很多人认为它只是个理论。

最近又看了一遍《IDDD》第十章聚合,结合已有的理论知识,来反思下这个问题。

DDD聚合是什么?

最容易与DDD聚合混淆的就是OO聚合关系。

由上图可以看出,OO聚合表示了一种关联关系;而DDD聚合表示了一种边界。

OO聚合关系(Aggregation) 表示一个整体与部分的关系。通常在定义一个整体类后,再去分析这个整体类的组成结构,从而找出一些成员类,该整体类和成员类之间就形成了聚合关系。

如上图中Question与Answer的关系。一个问题与很多的回答构成一个完整的问答关系。

在OO中还有一种比聚合关系更强的关联关系:

组合关系(Composition)也表示类之间整体和部分的关系,但是组合关系中部分和整体具有统一的生存周期。一旦整体对象不存在,部分对象也将不存在,部分对象与整体对象之间具有相同的生命周期。

继续以上图为例,Answer都是因Question存在而存在,当Question消亡时,Answer也自然消亡。

但像知乎,就算Question没了,Answer也会留存。

OO聚合与DDD聚合是什么样的关系呢?

因为聚合有隐含的构建关系和级联生命周期,通常会把OO组合关系构建成DDD聚合,其实组合关系只是聚合的必要条件,而非充分条件。

如果是聚合,那么聚合根与实体自然是组合,存在级联生命周期,但不代表存在级连生命周期都是聚合。

特别是,混淆了数据生命周期和对象生命周期,

例如在“获取客户订单”这一业务场景下,Customer 与 Order 之间也存在整体/部分的组合关系,但它们却不应该放在同一个 DDD 聚合内。

从数据生命周期看,一般如果数据库中顾客数据删除了,那么他对应的订单也会删除。

但不适合建模成聚合。

因为这两个类并没有共同体现一个完整的领域概念;同时,这两个类也不存在不变量的约束关系。

而且聚合后,订单就不再具有独立身份,每次查询订单时候,必须提供客户身份和订单身份,构成级连身份才可以。类似于当仅仅使用订单号查询订单时,是不行的,必须带上身份证和订单号一起,才能查询到订单。

聚合困境

看似把一堆实体和值对象放一起就组成聚合,在《IDDD》中提供了一个示例。使用过JIRA的人应该很容易理解。我简单按书中的思路叙述下。

第一次尝试

这次尝试,应该是完全按产品需求直接建模。

Product,BacklogItem,Release,Sprint的确是一组组合关系。

而且Product是聚合根。

1
2
3
4
5
6
7
8
public class Product {
private Set<BacklogItem> backlogItems;

private Set<Release> releases;

private Set<Sprint> sprints;

}

虽然这完整表达了模型,但实现很残酷。不得不考虑的问题:

1、持久化时,乐观并发,增加了操作失败概率

2、影响系统性能和可伸缩性

第二次尝试

将一个大的Product聚合拆分成了4个相对较小聚合。

虽然解决了事务问题,多个用户请求可以同时创建任何数量的BacklogItem、Release和Sprint实例。

但对客户端来说,这4个较小的聚合却多少会带来一些不便。

设计小聚合

一个完整的聚合

如果要加载一个完整的聚合,需要把所有这些实体与值对象都加载出来。那系统性能和可伸缩性大受影响。

为了解决这些问题,所有提出要设计小聚合。

小聚合不仅有性能和可伸缩性上的好处,它还有助于事务的成功执行,即它可以减少事务提交冲突。这样一来,系统的可用性也得到增强。在你的领域中,迫使你设计大聚合的不变条件约束并不多。当你遇到这样的情况时,可以考虑添加实体或者是集合,但无论如何,我们都应该将聚合设计得尽量小。

聚合之间不能直接加载到同一个边界之内,得通过唯一标识引用其他聚合。

通过标识引用并不意味着完全丧失了对象导航性。有时在聚合中使用Repository来定位其他聚合。这种作法也被称为失联领域模型(Disconnected Domain Model)。

这就是矛盾体,一方面希望保障模型的完整性,我们需要完整的聚合;另一方面又有各种实现限制条件。

这些原因正是《实现业务逻辑的本种方式》中提到的单体架构演变为分层架构的局限性。

总结越来,聚合有三点特性:

1、Global entities(aggregation root and entity) is the boundary of consistency

2、Global entities(aggregation root and entity) is the boundary of lifecycle

3、Object model assumes same lifecycle boundary within the global entity

DDD困境

由聚合的困境,管窥一斑,DDD落地的困境何尝不是类似原因:

1、Domain Driven Design,but technology may have a say

2、Using fundamentalism DDD,it only works for simple cases,so does transaction script,procedure of oriented programming

3、Smaller aggregation works as fine as a EJB2-refurbished architecture

由上面的聚合示例观察,第一点的确是现实。

我们使用DDD,就是想Domain Driven,可考虑了很多技术落地因素,打破了完整的模型。选择模型还是选择性能,是放在我们面前的第一道选择题。

而第二点在现如今多运行时分布式架构中,肯定不可能像在一个单休中加载完整的聚合对象。因此当要使用原味的DDD时,只能在简单的项目中,而DDD却说要在复杂场景下再去使用。不要简单问题复杂化。这又是个矛盾体。

这些问题怎么解决?

当前能想到的解决方案似乎只有在《DDD对象生命周期管理》提到的关联对象模式。既能保证模型的完整,又能兼顾性能。

总结

聚合设计时,尽量使用小聚合。这对吗?解决设计困境了吗?

如果使用小聚合,会造成一种现象。会出现很多的service。

只有使用service,才能在聚合间跨越对象生命周期,维持一致性。

这会慢慢演化成贫血模型,因为一部分逻辑在对象中,另一部分会放到service中。

所以我们得重新审视一些指导原则。或者时时提醒是不是过多的考虑了实现细节,破坏了模型。

怎么才能更好地保证模型完整性,而兼顾当前的技术限制。

技术人的三个阶段

发表于 2022-08-13
字数统计: 1.6k 字数 | 阅读时长 ≈ 5 分钟

技术人员的未来是什么?

我想作为一名技术人,在夜深人静时候,都会想想这个直击灵魂的问题。

作为一名搬砖人,哪里需要哪里搬?

不甘于作为工具人,但出路在何方?

新技术层出不穷,学到何年何月?怎么保持活到老学到老的状态?

在之前的一系列文章中,写过《程序员成长职级》、《最好的职业建议》、《首席架构师的打怪之路》等,其实也都是来源于对上述问题的思考。

怎么突破呢?其实市场也给了部分答案,比如市场要求从几年前的T型人才到π型人才,这也是作为一名技术人,需要三条腿:技术、业务、管理。

尤其业务,对于技术人员的重要性,很多技术人不理解,甚至工作多年的人也不理解。技术只是工具,达到目标的手段。而业务才是最终目标。不懂业务,最后必然会成为工具人。

很多技术人被沦为随时可替代的工具人也不自知,甚至对自身技术自鸣得意,孤芳自赏。

也许会有人反驳,我再深入解释一下。技术人想干什么?技术牛啊,怎么体现技术牛呢?写个数据库,写个操作系统,写个docker,或者写个开源框架等等。

为什么是这些东东呢?因为这些技术通用,与技术紧密关联,但其实这些只是更具技术属性而已,它们对外的统一特征是产品,首先是个产品,其次是个技术属性偏强的产品。

如果你对自己要达到的目标都不了解,再牛的技术也会有种无力感,报国无门空自怨。

回归到主题,技术人员的三个阶段。最近看到陶勇医生著作的《自造》,他讲了学医的三个阶段,我常见得对技术人是通用的。

阅读全文 »

程序员得懂点马斯洛理论

发表于 2022-07-23
字数统计: 1.5k 字数 | 阅读时长 ≈ 5 分钟

马斯洛需求层次理论是管理心理学中人际关系理论、群体动力理论、权威理论、需要层次理论、社会测量理论的五大理论支柱之一。

以往对马斯洛需求层次理论的理解很肤浅:人对需求是有层次的,层次间都有依赖关系。当低层次的需求被满足后,才会去考虑更高一层次的需求。

而且好像也没什么用处。知道之后既不能提升硬技能,也不能升职加薪。

马斯洛理论定义

通过《郭东白的架构课》的学习,了解到马斯洛理论的本意是:我们可能同时并行存在着多个需求,这些需求之间并不存在依赖或层次关系。

包含两个重点:

第一点:不是需求有层次,而是动机有优先级

如果这些需求得不到满足,那么它们各自会诱发动机。但动机有优先级,且具备抢占性质。所以任何时候,只有一个动机在主导着整个人的意识和行为。

第二点:动机跃迁模型

人有且只有一个主导动机。这个动机由人的内在需求所驱动,并独占且主导你当前的一切意识和行为,你整个人,包括你的视觉、听觉、嗅觉,你的思考、记忆、行为等。直到这个动机背后的需求被完全满足之后,更高层次的动机才可能进入主导位置。

学习了理论本意后,由于理论抽象度高,需要更具象的事才能更好地理解,经过反复思考,找到了一些日常工作中的常见的场景,可以用为进一步理解此理论的抓手。

指导架构

郭东白讲架构活动需要尊重和顺应人性,架构师必须洞察研发人员的人性,在方案设计和架构活动组织中充分考虑研发的人性,才能保障方案的正确性,以及方案的高效实施。

什么样的架构才算是尊重和顺应人性呢?不得不再次搬出当下两个流行的事物:中台和微服务

中台

对于中台概念,可以温习之前写的中台是什么。定义中台为企业级能力复用平台。

在做企业级业务架构时,做出来的模型,需要考虑两点:

1、凡是公用的部分,应该照顾到所有利益相关方的需求;

2、凡是已实现的功能都应该对新的需求方开放并支持必要的扩展。

部门利益是做企业级的最大障碍,跨越这个障碍是对业务架构师设计能力的最高挑战。

中台建设虽然需要兼顾各方的利益,但更多主要还是解决企业管理层对于公司长期生存与可持续发展的恐惧与焦虑问题

中台建设的这两个考虑点,似乎是郭东白架构课程中重要价值输出的浓缩版本。

前半段结合马斯洛理论,中台的建设通常都会伴随企业内的组织重构以及利益和职责的再分配,中台必然会触碰到前端业务团队利益,这自然威胁到了业务团队。

尤其很多公司建设中台是跟风式的,本身就没有明确的战略意图,相当于架构活动没有正确目标,甚至没有目标,更没有清晰的构建路径,冒然简单粗暴地下沉前端业务功能,让原本根错综复杂的前端业务团队缺失了安全感,按马斯洛理论,业务团队满足安全需求的动机占据主导位置,可以想象,整个中台的推进速度。

后半段则是任何企业活动的目标,都是为企业带来长期生存的优势。管理层、架构师都得考虑当下的活动是否真的带来生存优势。生存优势我简单理解为降本增效,ROI越高越有优势。

微服务

之前谈到微服务常会提到《康威定律与反康威定律》,随着近些年互联网行业的不景色,从以前的拆掉中台到现在的《拆掉微服务》。

康威定律有个常用示例:如果有四个小组合作开发一个编译器,那么你将得到一款具有四个步骤的编辑器。

结合马斯洛理论,每个小组都有了自己的一亩三分地,也就有了归属感和安全感,从人性角度,团队会更有活力、进取心和责任感。

技术管理

作为TL,在《码而优则仕》中提出三件重要事情。尤其激活团队。

TL如何激励团队?

都说现在00后是来整顿职场的,所以不能再像过去,员工以生存为目的,无条件接受资本主义压榨。

现在生活条件大大改善,对应马斯洛最底层需求已经完全满足。管理者怎么办呢?会通过内部激励机制,如晋升职级;和红线制度,明确惩罚事项。

有没有更高阶的方式方法呢?脱离上述的靠外部驱动。

马斯洛理论给出指导方针:让员工有更多的归属和尊重。激发他们内部需求。

总结

一个看似无用的理论,经过高人的讲解,可以感知到原来这个理论贯穿在每日生活的细节中。

马斯洛理论就是这样,不管是做架构还是做管理,只要有人参与的活动,都渗透着它的威力。

springboot之ClassLoader

发表于 2022-07-23
字数统计: 1.4k 字数 | 阅读时长 ≈ 5 分钟

上篇《ClassLoader#getResource与Class#getResource的差别》了解原生java获取资源方式以及方式之间的区别。

这篇介绍一下springboot的加载方式。

要想调试springboot加载方式,不能直接在idea中运行主程序,要使用真实场景下的java -jar方式运行,需要做两件事:

1、需要打包springboot应用程序

2、在IDEA中用java -jar springboot.jar来运行才能debug

springboot使用maven plugin打包成可运行的jar文件

1
2
3
4
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

SpringBoot打出的jar包,可以直接通过解压的方式查看内部的构造。一般情况下有三个目录。

  1. BOOT-INF:这个文件夹下有两个文件夹classes用来存放用户类,也就是原始jar.original里的类;还有一个是lib,就是这个原始jar.original引用的依赖。
  2. META-INF:这里是通过java -jar启动的入口信息,记录了入口类的位置等信息。
  3. org:Springboot loader的代码,通过它来启动。

MANIFEST.MF文件的内容:

1
2
3
4
5
6
7
8
9
10
Manifest-Version: 1.0
Implementation-Title: springboot-test
Implementation-Version: 1.0-SNAPSHOT
Start-Class: com.zhuxingsheng.App
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.1.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher

里面有两个重要的参数:

1
2
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.zhuxingsheng.App

Main-Class:记录了java -jar的启动入口,当使用该命令启动时就会调用这个入口类的main方法,显然可以看出,Springboot转移了启动的入口,不是应用自身的com.zhuxingsheng.App入口类。

Start-Class:应用自身的com.zhuxingsheng.App入口类,当内嵌的jar包加载完成之后,会使用LaunchedURLClassLoader线程加载类来加载这个用户编写的入口类。

在IDEA中正常启动应用程序,整个类加载体系与直接使用java -jar springboot.jar是不一样的,想
要在IDEA里面debug springboot应用程序

先引入loader依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>

再对应用程序通过maven package打包成jar

再在IDEA中设置:

并指定Path to JAR.

启动之后,先进入JarLauncher:

debug进入后,会使用springboot自定义的LaunchedURLClassLoader加载应用程序,LaunchedURLClassLoader类体系:

加载资源的过程如在《Classloader加载资源的方式》中提到的一样。

与之前的做个小实验,但这次做点小变动,在依赖的jar中也放一个META-INF/app.properties文件。

并在工程本身的resources里面也放一个META-INF/app.properties

此时系统中有两个META-INF/app.properties,通过下面的四种情况来加载资源文件,会获取到哪一个文件?

1
2
3
4
5
6
7
8
9
10
11
//第一种场景
final URL resource = Thread.currentThread().getContextClassLoader().getResource("/META-INF/app.properties");

//第二种场景
final URL resource = Thread.currentThread().getContextClassLoader().getResource("META-INF/app.properties");

//第三种场景
final URL resource1 = App.class.getResource("/META-INF/app.properties");

//第四种场景
final URL resource1 = App.class.getResource("META-INF/app.properties");

第一种 ClassLoader绝对路径

按《Classloader加载资源的方式》结论,应该会返回null。

然而实事并非无此:

这不得不提到在URLClassPath里面有两个内部Loader:

FileLoader 是加载文件夹中的文件

JarLoader 是加载jar中的文件

在《Classloader加载资源的方式》中的结论是基于FileLoader加载的,而现在的方式是使用JarLoader。

使用ClassLoader.getResource时,都是基于根节点查找,这点是没错的,只是根节点是BOOT-INF下的lib和classes:

去加载每一个jar中的文件,判断是不是存在:

可以看出,因为根节点不同,所以文件没有加载到,项目根目录里面的META-INF/app.properties,是在整体工程根目录的META-INF/app.properties中。

此时,找到的文件目录是在:

jar:file:/Users/zhuxingsheng/workspace/springboot-demo/springboot-test/target/app.jar!/BOOT-INF/lib/general-tool-utils-1.1.0-SNAPSHOT.jar!/META-INF/app.properties

第二种 ClassLoader 相对路径

可以看出使用的是AppClassLoader,加载的路径为

jar:file:/Users/zhuxingsheng/workspace/springboot-demo/springboot-test/target/app.jar!/META-INF/app.properties

可以看出 相对路径 与绝对路径的区别,以及与FileLoader的区别:

1、绝对路径是LaunchedURLClassLoader从classpath根节点查找;相对路径是AppClassLoader从当前jar为根目录查找

2、FileLoader绝对路径是:file:/META-INF/app.properties,而JarLoader的绝对路径则不同了,会带上整个classpath

第三种 Class 绝对路径

由《Classloader加载资源的方式》知道,这种方式与第二种场景效果一致:

1
2
//第二种场景
final URL resource = Thread.currentThread().getContextClassLoader().getResource("META-INF/app.properties");

路径地址在:

jar:file:/Users/zhuxingsheng/workspace/springboot-demo/springboot-test/target/app.jar!/META-INF/app.properties

第三种 Class 相对路径

类似于:

1
final URL resource = Thread.currentThread().getContextClassLoader().getResource("com/zhuxingsheng/META-INF/app.properties");

总结

此篇一是介绍了怎么在IDEA中debug出运行java -jar springboot.jar的效果。二是介绍springboot类加载机制,以及绝对路径与相对路径的区别。

当依赖jar包中有与工程目录下有同路径同名资源文件时,为了不必要的冲突,在classloader#getResource时,不要使用绝对路径。

如在apollo的源码中:

也会特意使用substring处理掉绝对路径。保证加载资源的正确性。

12…14
朱兴生

朱兴生

140 日志
3 分类
52 标签
© 2016 — 2022 朱兴生 | Site words total count: 323.5k
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4
沪ICP备18040647号-1