码农戏码

新生代农民工的自我修养

0%

微服务的核心思路就在于将业务能力封装成独立的松耦合的服务。通过这样一组服务,构建企业内的能力生态系统。除了能满足当前应用的需要之外,也为未来可能的新应用提供了紧实的基础。

《拆完中台再拆微服务》中也阐述了微服务是为了提升程序效能和团队效能。

当提到微服务时,总会想到各种各样的好处:

1、使大型的复杂应用程序可以持续交付和持续部署

2、每个服务都相对较小并容易维护

3、服务可以独立部署

4、服务可以独立扩展

5、微服务架构可以实现团队的自治

6、更容易实验和采纳新的技术

7、更好的容错性

当然,没有一项技术是“银弹”。

微服务架构也存在一些显著的弊端和问题:

1、服务的拆分和定义是一项挑战

2、分布式系统带来的各种复杂性,使开发、测试和部署变得更困难

3、当部署跨越多个服务的功能时需要谨慎地协调更多开发团队

4、开发者需要思考到底应该在应用的什么阶段使用微服务架构

近些年,反对微服务的声音越来越多。

在twitter上有个很热门的贴子:
twitter.com/xuwenhao/status/1593469165892820992 作者显明的数落了微服务的种种不是。为了更方便你了解作者的看法,我简单罗列下:

1、微服务的适应场景非常有限。这么多巨头互联网公司的核心业务逐渐微服务化,的确是在特定的历史时期和场景下的解决方案。

2、大型系统的开发,核心的挑战其实只有一个,就是“控制复杂性”。微服务不会减小复杂性,只会转移复杂性,它天然是为了解决极度复杂的计算、存储问题,中小规模系统其实是根本不需要的,至少在成为大型系统之前。

3、在系统开发上,控制复杂性的方式,可以用三个关键词来描述,那就是“抽象”、“封装”和“复用”。

4、服务化的第一个挑战:是“服务”并不是无状态的,“服务”也绑定了数据。以电商业务为例。商场生成订单,必在服务内持久化下来。然后,这个订单会发给到订单履约系统,也会持久化下来。然后两边都有可能触发订单状态的变更,商场用户可能取消订单,履约系统可能因为商品缺货也取消订单。两边都需要有对应的接口和实现,去完成这样的状态同步。这个过程中,就容易引入数据不一致的问题。

5、第二个挑战:因为是不同的服务,就会面临“向前兼容”的问题,不同的系统并不是完全同步迭代的。而已经发布的服务,意味着对外有了明确的协议承诺。在服务发布新版本的时候,必须要确保向前兼容。

6、第三个挑战:因为服务化划分了明确的边界,系统更容易变成异构的,更容易引入更多的技术栈。并且有些功能,会在两个不同的语言、框架下各实现一遍,也容易进一步放大之前所说的业务数据不一致的第一个挑战

三种典型的伪微服务

在James Lewis 和 Martin Fowler的名作《微服务》中,将微服务定义为一种架构风格,并总结了它的九种特质:

1、通过服务实现组件化

2、服务按照业务能力划分组织

3、服务以产品而不是项目研发

4、逻辑集中在服务中,编排简单

5、每个服务自主决策(技术栈、语言等等)

6、每个服务自主管理数据(不强制使用统一数据源)

7、基础设施自动化

8、将服务失败当作常态纳入设计考量

9、演进式设计(不求一步到位)

在实现中,如果对上面的特质理解出现偏差,就会出现三种典型的伪微服务风格

1、分布式服务

服务按照业务能力划分组织

微服务中的服务应该以业务能力为粒度。这间接回答了“微服务到底多微合适”,既不是单纯的技术能力(比如查询、获取系统时间),也不是完整的应用,而是用以支撑构建应用的业务能力。

通常所说的“恰当粒度”是在业务与实现两个维度上平衡的结果。并不会存在“只从单一维度入手,越怎么样就越好”这么简单粗暴的结论。所以微服务并不是越小越好,当小到不能表示业务能力,就不再是微服务了。

如果不顾及服务是否按照业务能力划分组织,就是一种典型的伪微服务模式。被称之为分布式服务。当然分布式服务并不是反模式,它有其特有的用处,只不过它并不是微服务而已。

2、微工作组

服务以产品而不是项目研发

这主要是从生命周期角度看,产品和项目的差异体现在团体结构和生命周期上。

产品的生命周期分为初始、稳定、支持和结束生命几个阶段。那么产品的不同版本,可能处在不同的生命周期中。所以产品团队需要在同一时间内,支持多个处在不同生命周期的产品版本。而项目通常假设只有唯一产物,随着项目生命周期的进项,项目化服务一直在改变。

因此产品化服务的生命周期,实际上相当于承诺在产品生命周期内,服务是不变的。也就是说只要1.0不结束生命,那么我们就可以一直使用它。哪怕发布了1.5、2.0、3.0,只要1.0满足我的需要,并且还在生命周期内,作为消费者,可以无视你的后续版本。

微服务需要服务间不仅仅在接口上松耦合,还在要生命周期上松耦合。也就是微服务可以自主发布,其他服务不应该受到影响。产品化是实现这一点的根本途径。

如果服务缺乏产品化生命周期,那就会产生一组在生命周期上紧密耦合的服务,从而完全丧失微服务的意义。随着服务数量变多,这种生命周期的耦合还会带来难以承受的沟通成本。

3、傻服务

逻辑集中在服务中,编排简单

逻辑越在服务中集中,所需要的编排就越简单,通常通过RESTful API或者轻量的消息机制即可完成。

如果服务中的逻辑简单,那就会有大量的逻辑泄露到编排逻辑中,此时就需要使用复杂的编排工具辅助我们工作。

选择编排复杂的逻辑,听越来很有道理:既然我们希望在不同场景下复用服务,那么总有一些需要改变的订制代码,我们需要将它们与服务本身分离。分离之后,就能通过编排引擎,帮助我们在不同的场景下重用这些服务

但按照这个逻辑下去,服务往往会变成对于数据的CRUD,然后大量的逻辑存在于编排引擎中,这也是典型的伪微服务。像傻服务一样。

总结

其实任何技术都可以说它既不是银弹也不是哑弹。就是优势与缺点并存。我们需要权衡的是在什么样的阶段引入什么样的技术来帮我们更好更快地解决问题。

正交设计,什么是正交设计,在之前的几篇文章中,《架构的本质是业务的正交分解》《应用对变化》都有学习记录。

这两篇文章越看越感觉有道理:

1、系统应该被分解为“最小化核心系统 + 多个彼此正交的周边系统”

2、消除重复

3、分离关注点

4、管理依赖:最小化依赖、向稳定方向依赖

对于这四个策略的理解,感觉自己的认知层次还是太低了。更多的时候还是在作为编码原则。而非架构原则。

在《架构师的自我拯救》提出了技术人员的商业价值,包括两部分:快速试错和快速规模化。

那么我们能在企业的竞争中做些什么,同时也自己创造增量价值呢?其实也被包含在这两方面。

比如快速试错。为什么需要快速试错?为了验证商业想法是否能得到市场认同,从而获得商业价值。必然需要高速响应业务和技术的需求。

这面临的挑战就是交付时间压力。市场不等人,竞争对手也不等人。

除了战略层面的尝试方向对不对,如果有人承诺方向一定对,那自然不用试错,直接火力全开,饱和式攻击就行。

绝大多数情况是没人担保的,只能尝试。对应到技术人员,挑战在于只是很小的一次尝试,不要把系统改得面目全非。

对应到架构,就是我们系统的得符合“开闭原则”。能稳住核心系统的不变性,又能保持系统有序的增量。把大多数的尝试尽量封装到一个小的领域内。不会因为多次的业务尝试,导致系统随着时间变得混乱。

想要做到这些,需要如下架构原则:

1、单一职责,把每个业务尝试封装到一个单一模块中。一旦尝试失败,就可以迅速把业务逻辑下线,避免影响整体的复杂性。

2、最小依赖,整体架构设计要保障大多数业务尝试可以在业务层完成。如果每个业务方的需求都会侵入到底层的逻辑,那么每次尝试都会变成跨模块,甚至跨团队合作,这种架构会大幅降低业务尝试的速度

3、最小暴露,相当于最小被依赖,在业务尝试期接口不对部门或企业外部暴露,包括API、数据共享、事件、消息等一切对外界造成影响的通信机制。尤其是输出,这样才能最小化它的爆炸半径。否则该业务尝试的数据模型会污染到其他业务,在尝试失败之后对其他业务的影响也会很难剥离。

原则很简单,但怎么落地这些原则,最大的挑战来于短视疸。如互联网火热时,人员更替很频繁,导致技术人员的稳定性差,想要快速拿到结果就会设计短视。还有企业组织结构,像康威定律一样,组织间的利益争夺,造成组织内最优设计,但企业整体设计熵增。

作为架构师,可以:

1、提升对封装能力重要性的认知。这是一个技术人员的基本功,从写代码的第一天就需要。只不过随着职业发展,从封装代码架构,到封装业务逻辑,最后到封装业务尝试。

2、建设复杂度控制机制。这里设计评审很关键。业务尝试也要有设计评审,而评审的一个固定环节就是逻辑、数据和接口的最小爆炸半径的设计。

3、推行最小必要架构原则。任何增加功能、引入复杂性的设计,都要做一个正式的评审,而简化的行为则不需要。


当然,这些内容,是在郭东白架构课程里面看到的,他提出这是一个架构师提升企业架构设计对外部的适应能力。我理解这就是应对变化的能力。怎么能更好的应对变化,最佳原则就是“开闭原则”。而这些内容我更感觉是正交设计的另一种表述。这些原则与正交设计是一脉相承的。

如果整个系统的各个功能模块是正交设计的,那自然能灵活应对变化。一个庞大系统无法适应变化时,主要是架构僵化以及各个模块之间的关系错综复杂,导致牵一发而动全身。不能改不敢改。

应对变化时,不要饱和式攻击取胜,需要对阶段性精确目标最大投入取得成果。怎么才能做到不饱和式攻击,在架构层面就是要做到正交分解。


总结一下:架构师主要职责就是为了抵制熵增。而抵制熵增不管从战术还是战略,正交设计都是一种很好的实践方式和指导原则。

一个简简单单的基础数据结构,却发展成了形形色色的消息队列中间件。世界就是如此奇妙。

如上图,一个简单的队列数据结构,由生产者往里插入内容,由消费者从里面获取内容进行消费,就构建出一个简单的消息队列模型。

消息模型有两种:

1、点对点模型:也叫消息队列模型。多个消费者共同消费同一个队列,效率高

2、发布/订阅模型:发送方也称之为发布者(Publisher),接受方称为订阅者(Subscriber)。与点对点模型不同的是,这个模型可能存在多个发布者向相同的主题发送消息,而订阅者也可能存在多个,它们都能接收到相同主题的消息。

一个简单的Kafka架构图:

Read more »

年前老板跟我讲了句话,“兄弟们得自我拯救”。

感觉十分在理。

这几年,行情很差,以肉眼可见的速度一步步衰退。大厂裁员,小厂倒闭,与互联网相关的行业,哀鸿遍野,各个公司都在进行降本增效。

辛辛苦苦一年,年末了,年终还得来个折上折。但从公司角度,长久生存是第一要务。的确很是无奈。

战略这种词太大,我这种底层也不太明白。但回首公司几年,有几句口号,回荡在耳边,加上老板这句,串联在一起,还蛮有意思:

躬身入局、往前走一步、自我拯救!

躬身入局,记得当时说的是高管要躬身入局。我跟一位CTO朋友分享,他立刻反应那你这底层要躬身出局。感叹人家能当CTO也不是白来的,这敏捷的思维,机智的身形。

对于我这底层,就是躬身出局、往前走一步、自我拯救。

Read more »

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
11

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

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

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

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

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

总结

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

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

七牛CEO许式伟讲:架构的本质是业务的正交分解。

好独特的见解。

做架构到底是做什么?

《首席架构师的打怪升级之路
中提到:架构师是具备架构能力的人,架构能力是指为相对复杂的场景设计并引导一个或多个研发团队,来实施结构化软件系统的能力。

关键信息:复杂场景结构化

《软件设计之美》中总结了软件复杂性来自业务复杂性和技术复杂性。应对的办法是通过分而治之,控制规模大小;保持结构清晰与一致性来降低认知负荷。并且要有一定的前瞻性,拥有可扩展性,能应对变化。

关键信息:规模可控、结构清晰、应对变化

总结以上两篇的关键信息:做架构就是通过规模可控、结构清晰的小模块去组合成大模块,进而形成更复杂的软件系统。并且拥有足够的扩展性应对未来的变化。

架构的核心就是【组合与应对变化】;简洁点,三个字:“分、合、变”

这些其实与“业务的正交分解”方法是一脉相承的。

正交分解

既然是业务的正交分解,自然得理解正交是什么意思?

《应对变化》详细介绍过正交设计。

简而言之,主要是三个要点:

1、消除重复

2、分离关注点

3、管理依赖:缩小依赖的范围和向稳定的方向依赖

想要把一个复杂的系统拆解成一个一个可被理解掌控、并且又能被结构化地合并成大模块的小模块。正交设计是必须的。

这考验了架构的拆解能力,拆解的合理性就是解耦的合理性;并能在合并时每一个模块保持高内聚。

开闭原则

正交设计主要应对的是“分、合”,那么怎么应对“变”?

就得提到著名的开闭原则。开闭原则是架构治理的根本哲学。

之前也整理了下OCP原则,《SOLID之OCP》,只要我们面向接口编程,就能大概率的符合开闭原则。当时的理解回头看还是比较肤浅的。

一些人对开闭原则的错误解读,认为开闭原则不鼓励修改软件的代码来响应新需求。这显然比较极端。

一个软件产品只要在其生命周期内,就会不断发生变化。变化是一个事实,需要让软件去适应变化。我们应该在设计时尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现“拥抱变化”。

其实,开闭原则的背后,是推崇模块业务的确定性。可以修改模块代码的缺陷,但不要去随意调整模块的业务范畴,增加功能或减小功能都不鼓励。这意味着模块的业务变更是需要极其谨慎的,需要经得起推敲的。

开闭原则指出尽量通过扩展软件实体的行为来应对变化,满足新的需求,而不是通过修改现有代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。

总结一下,开闭原则就两点:

1、模块的业务要稳定。当要修改模块业务时,不如实现一个新业务模块。而实现一个新的业务模块来完成新的业务范畴,是一件轻松的事。这个角度,鼓励写“只读”的业务模块,一经设计不可修改。需要变化不如把它归档,放弃掉。这种模块业务只读的思想,是架构治理的基础哲学。

2、模块的业务变化点,简单一点,通过回调函数或者接口开放出去,交给其他业务模块。复杂一点,通过引入插件机制把系统分解为“最小化的核心系统+多个彼此正交的周边系统”。

将开闭原则应用到业务系统。业务对外只读,意味着不可变,但不变的业务生命周期是很短暂的,所以要可扩展。要扩展还要不变,就你倒逼着要做兼容,而兼容可能导致现有的功能职责不单一,这又倒逼着要对现有的功能做再抽象,以适应更广的“单一职责”。

所以不改是不可能的,只是改的结果应当是让项目往更稳定方向发展,而这很难。无论是新的抽象还是职责范围的扩张,需要强大的分析能力和精湛的设计。

这种不变与变其实也印证了架构第一定律:一切都是权衡

在团队中,一直在灌输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,一切都是花架子,形似无神。

最近项目中使用了高版本的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来得简单些。

DDD统一语言

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

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

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

Read more »

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