码农戏码

新生代农民工的自我修养


  • 首页

  • 归档

  • 标签

  • 关于

  • 在线工具

  • 搜索

什么样的继承才是好的继承

发表于 2023-03-25
字数统计: 1.7k 字数 | 阅读时长 ≈ 6 分钟

继承的本质是提高代码的复用。

然而自从继承出世后,被人们过多滥用,以至于像耦合恐惧一样恐惧继承,得了“继承创伤应激障碍”。

是不是有像耦合必然性一样,解决继承的创伤呢?

里氏替换原则

这个原则最早是在1986年由 Barbara Liskov 提出:

若对每个类型T1的对象o1,都存在一个类型T2的对象o2,使得在所有针对T2编写的程序P中,用o1替换o2后,程序P的行为功能不变,则T1是T2的子类型

在1996年,Robert Martin 重新描述了这个原则:

Functions that use pointers of references to base classes must be able tu use objects of derived classes without knowing it

通俗地讲,就是子类型必须能够替换掉它们的基类型,并且保证原来的逻辑行为不变及正确性不被破坏。

比如那个著名的长方形的例子。Rectangle 表示长方形,Square 继承 Rectangle,表示正方形。现在问题就来了,这个关系在数学中是正确的,但表示为代码却不太正确。长方形可以用成员函数单独变更长宽,但正方形却不行,长和宽必须同时变更。

1
2
3
4
Rectangle rect = new Square();
rect.setHeight(4); // 设置长度
rect.setWidth(5); // 设置宽度
assertThat(rect.area(), is(20)); // 对结果进行断言

当正方形 Square 替换长方形 Rectangle 时,发现程序不能正确运行,这样就不满足LSP,也就不适合使用继承。

还有那个同样著名的鸟类的例子。基类 Bird 有个 Fly 方法,所有的鸟类都应该继承它。但企鹅、鸵鸟这样的鸟类却不会飞,实现它们就必须改写 Fly 方法

1
2
3
4
5
6
7
8
9
10
11
12

public class AbstractBird {
//...省略其他属性和方法...
public void fly() { //... }
}

public class Ostrich extends AbstractBird { //鸵鸟
//...省略其他属性和方法...
public void fly() {
throw new UnSupportedMethodException("I can't fly.'");
}
}

这样的设计

一方面,徒增了编码的工作量

另一方面,也违背了我们之后要讲的最小知识原则(Least Knowledge Principle,也叫最少知识原则或者迪米特法则),暴露不该暴露的接口给外部,增加了类使用过程中被误用的概率。

想验证有没有违背里氏替换原则,主要是两方面:

1、IS-A的判定是基于行为,只有行为相同,才能说是满足IS-A关系。

2、通常说来,子类比父类的契约更严格,都是违反里氏替换原则的。

在类的继承中,如果父类方法的访问控制是 protected,那么子类 override 这个方法的时候,可以改成是 public,但是不能改成 private。因为 private 的访问控制比 protected 更严格,能使用父类 protected 方法的地方,不能用子类的 private 方法替换,否则就是违反里氏替换原则的。相反,如果子类方法的访问控制改成 public 就没问题,即子类可以有比父类更宽松的契约。同样,子类 override 父类方法的时候,不能将父类的 public 方法改成 protected,否则会出现编译错误。

而像长方形例子中,正方形继承了长方形,但是正方形有比长方形更严格的契约,即正方形要求长和宽是一样的。因为正方形有比长方形更严格的契约,那么在使用长方形的地方,正方形因为更严格的契约而无法替换长方形。

多态和里氏替换有点类似,但它们的关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里氏替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原程序的正确性。

其实还是回归继承本质,就是为了代码复用,子类最好不要覆盖父类的任何方法,只做额外增强

组合优于继承

组合优于继承:如果一个方案既能用组合实现,也能用继承实现,那就选择组合实现。

上面提到的鸟类例子,通过 AbstractBird 类派生出两个更加细分的抽象类:会飞的鸟类 AbstractFlyableBird 和不会飞的鸟类 AbstractUnFlyableBird。当还要关注“鸟会不会叫”的时候,
那就需要再定义四个抽象类(AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、AbstractUnFlyableTweetableBird、AbstractUnFlyableUnTweetableBird)

类的继承层次会越来越深、继承关系会越来越复杂。而这种层次很深、很复杂的继承关系

一方面,会导致代码的可读性变差。因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类代码、父类的父类的代码,一直追溯到最顶层父类的代码。

另一方面,这也破坏了类的封装特性,将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改,就会影响所有子类的逻辑。

简而言之,继承最大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。

而利用组合、接口、委托三个技术手段,重构一下上面的继承问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

public interface Flyable {
void fly();
}
public class FlyAbility implements Flyable {
@Override
public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility

public class Ostrich implements Tweetable, EggLayable {//鸵鸟
private TweetAbility tweetAbility = new TweetAbility(); //组合
private EggLayAbility eggLayAbility = new EggLayAbility(); //组合
//... 省略其他属性和方法...
@Override
public void tweet() {
tweetAbility.tweet(); // 委托
}
@Override
public void layEgg() {
eggLayAbility.layEgg(); // 委托
}
}

总结

尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。继承改写成组合意味着要做更细粒度的类的拆分。这意味着,我们要定义更多的类和接口。类和接口的增多也就或多或少地增加代码的复杂程度和维护成本。

如果类之间的继承结构稳定,继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,就尽量使用组合替代继承。

httpRequest中重复读取inputstream注意事项

发表于 2023-03-04
字数统计: 722 字数 | 阅读时长 ≈ 3 分钟

在Filter中获取请求体,就得解析一次request的inputStream。

而在tomcat的设计中,request的inputStream只能读取一次,读取完一次后,虽然inputStream还在那,也不会变成空,但里面的内容已经被没有了。

解决方案很简单,就是继承HttpServletRequestWrapper,缓存request中流的内容。

比如实现一个类:ServletRequestReadWrapper

整个类的结构是这样的:

然而世事总不那么一帆风顺。

当请求头Content-Type值为 multipart/form-data 时,情况就出现了问题。在https://www.zhihu.com/question/434950674 这个回答中有说明。

在使用multipart/form-data上传文件时,controller类似这样的

1
2
3
@PostMapping
public void upload(MultipartFile file) throws IOException {
}

想要得到一个MultipartFile类型的内容,原理与其他类型一样,spring从request中获得流内容,并反射创建MultipartFile。

但相对于普通类型,Multipart并没有从request.getInputstream中获取内容,而是从另一个方法getParts里面获取,但getParts返回的内容也是解析的request.getInputStream中的内容。

是不是有点奇怪,为什么在wrapper里面已经缓存了inputstream内容,为什么到了右下解再去getInputStream时,却没有了呢?

tomcat的request类结构是一个装饰器模式

requestWrapper中的getInputStream其实还得来源于真实的Request对象。而在整个处理过程的末端,获取inputStream,并不是从requestWrapper中获取的,而是从真实的Request对象中获取。此时流内容已经被读取过,自然就读取不到了。

再回味下主要的源码,HttpServletRequestWrapper中getParts()

1
2
3
4
5
6
7
8
@Override
public Collection<Part> getParts() throws IOException, ServletException {
return this._getHttpServletRequest().getParts();
}

private HttpServletRequest _getHttpServletRequest() {
return (HttpServletRequest) super.getRequest();
}

到了RquestFacade的getParts()

1
2
3
4
5
@Override
public Collection<Part> getParts() throws IllegalStateException,
IOException, ServletException {
return request.getParts();
}

再到Request的getParts()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Collection<Part> getParts() throws IOException, IllegalStateException,
ServletException {

parseParts(true);
}

private void parseParts(boolean explicit) {

...
//解析流中文件内容,upload.parseRequest中会调用request.getInputStream
//而传入的request,并不requestWrapper,而是真实的Request本身
//而此时Request中的流已经被读取过了,所以解析出的文件内容为空
//controller中的file变成了null
List<FileItem> items = upload.parseRequest(new ServletRequestContext(this));

...

}

到此,原理已经讲清楚了。怎么解决呢?

1、在Wrapper中,非multipart缓存inputstream,是multipart时,则缓存parts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ServletRequestReadWrapper(final HttpServletRequest request) throws IOException, ServletException {
super(request);
if (HttpUtils.isMultipartContent(request)) {
parts = request.getParts();
} else {

final ServletInputStream is = request.getInputStream();

if (null != is) {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
this.bodyLength = ByteStreams.copy(is, os);
this.body = os.toByteArray();
}
}
}

2、放弃multipart的消息体。

得到消息体的内容,有时只是为了记录日志,像文件上传,其实得到的消息体也没啥好记录的,所以放弃记录mulipart类型消息体。

云时代微服务划分原则是什么

发表于 2023-02-27
字数统计: 4.9k 字数 | 阅读时长 ≈ 16 分钟

微服务架构是云时代的首选架构风格。

Martin Fowler 在 2014 年写的文章《微服务的前置条件》中提到如果使用微服务架构,则需要先拥有一些先决条件:

1、快速的环境提供能力

2、基本的监控能力

3、快速的应用部署能力

而这三个能力,正是云原生提供的基本能力。

新技术带来的价值

既然现在是云时代,是不是我们就不用考虑其它架构风格,直接使用微服务架构呢?答案显示并不是。我们不要为了新技术而新技术。当然,对于找工作肯定是有利的。但我们还是从实际出发,从业务价值出发。

在《架构师的自我拯救》中提到,架构师还得多考虑商业价值。使用新技术就是我们的借力。目标还是为了提升商业价值。

所有的技术演进,都是围绕着用户对这个产品的核心诉求展开的,通过技术层面的架构改造,来解决用户当下的痛点。

在阿里大牛毕玄的访谈中,提到了服务化的核心是解决了两个问题:

一、为了解决系统的水平伸缩能力的问题。每个应用,每个系统,承担的责任变少了,伸缩性就变强了。对应到商业价值,以更细致的粒度,控制系统运营的成本。

二、研发协作问题。以前100人开发一个系统,大家都没有分工,接一个需求,要改就从头到尾全改,这个时候有可能会出现冲突,因为可能每个人都在改同一个地方,大家合并代码的时候就非常痛苦,效率很低。

服务化分工了,你就改这块儿,他就改那块儿,虽然增加了协作成本,但它毕竟能让100人甚至上千人的研发团队可以并行做下去,现代软件变得更复杂,做任何软件上来就是一帮人,必须考虑到一帮人协作的问题。

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

云时代的特性

假如一个电商网站,平时只有几千用户同时使用,只需要100台机器就足以支撑这个系统了;而到了双十一,用户量可能猛增几百倍,那需要比如说10000台机器支持这个系统。

而云平台,可以动态调整系统需要的机器数量,让我们按需使用。这样我们就不需要在闲时投入过高的机器成本,也就是非双十一期间,维持10000台机器的开销。但同时也不会错过在业务高峰获取收益的机会,因为云平台会自动帮我们从100台扩容到10000台机器。

这种动态调节的能力被称为云的弹性,它是云平台一切美好特质的基础。

为了实现这种弹性,运行在云平台之上的系统,需要满足一个条件:这个系统可以通过水平扩展进行扩容。

水平扩展,指通过增加机器数量、构造集群的方法,来满足容量的需求。

垂直扩展,指当遇到扩容需求时,通过更换更强有力的机器,来获得更好的性能。

各种基础设施服务云平台,它们其实只有复制和剪切两个能力:

1、根据给定镜像,产生符合镜像内容的机器的能力。也就是将镜像复制为机器的能力。

2、撤销不需要的机器,将其放回资源池的能力,也就是剪切机器的能力。

通过复制和剪切这两个能力,云平台就能对某个镜像提供水平扩展。这种扩展方案通常被称作为弹性负载均衡。

那么怎样利用弹性负载均衡提供的水平扩展,才能更有效地架构系统呢?关键在于将弹性需求不同的组件分离。

假如你在运营一个在线电商平台,我们可以粗略地将这个电商 平台的业务分成产品目录和支付两大块。在双十一期间,肯定会遇到比平时更大的流量,因而需要更高的系统容量去支持业务。

但问题来了,产品浏览和完成支付两个部分增加的流量是一致的吗?从个人实际参与抢购的经验看,双十一之前,用户对产品浏览的需要比平时多;而在双十一当天,可能会对支付的需求更多。

因此我们对支付和产品目录两块功能,制定不同的水平扩展策略,然后由不同弹性负载均衡控制。这样就可以有针对性地在不同阶段为两块功能提供不同的容量。

按这个角度,我们把弹性作为主要指标,对系统进行划分,将不同弹性变化的组件放入到不同的弹性边界中。

通过弹性边界,可以更细致的粒度,控制系统运营成本,也才能真正发挥云平台的能力。所以当要想要利用云平台架构软件时,寻找合理的弹性边界是很重要的事。

微服务划分原则

微服务怎么划分?总体来讲,从功能性需求和非功能性需求两方面考虑。

从功能性方面考虑:微服务的划分应该有利于保证系统概念的一致性,更容易灵活扩展功能,而这些又要求开发团队顺畅的沟通协作。

DDD限界上下文正好在这方面提供了理论指导,奠定了划分基础。

根据限界上下文划分,既考虑到了传统模块化思维中对业务概念的松耦合、高内聚的要求,又考虑到团队的认知负载和认知边界。

这样一方面解决了团队协作和概念一致性问题。另一方面,每个限界上下文又是一个业务概念内聚的边界。在这个边界内部,就更容易建立可维护、易扩展的模型。

合理的微服务划分,应该是对于多数需求变更,只需要改动一个或少量的微服务。而划分不合理的话,对多数业务需求,都要修改多个微服务。

从非功能考虑:在性能、安全、可用性,甚至发布周期,看是不是需要进一步划分。或者考虑把几个限界上下文合并到一个微服务。极端情况下,把有上下文合并到一个服务,又变成一个单体。

到此,微服务的划分,我们当前都是以限界上下文优先来划分的。这也符合面向对象建模中的聚合概念,是一种“一致性优化”的模型结构。是“概念一致性边界”,“事务边界”。

但结合上一章节“弹性边界”,在云时代,弹性是个很重要指标。如果两个上下文明显具有不同的弹性诉求,那就应该拆分。而如果具有一致的弹性诉求,就可以不拆。

弹性边界跟软件模块之间存在依赖关系一样,弹性边界间也会存在依赖。而弹性边界间的依赖(也就是服务间调用关系,或是数据交换关系),会造成流量的传递。如果上游弹性边界突然需要处理大量的流量,那么这些流量自然也会传递到下游弹性边界中。

这在实现中常发生,当平台的入口系统流量上升后,后面依赖的系统流量也上升了,整个一条调用链路上的系统都得扩容。这种不同弹性边界间流量的传递就是弹性依赖。

只要组件之间存在交互,弹性依赖就不可避免。在云平台更擅长处理依赖于吞吐量的弹性依赖,但对依赖于响应时间的弹性依赖,就没有好办法了。这背后的原因在于水平扩展并不能保证改进响应时间,而只能提高吞量。也就是云平台的弹性并不总能改进响应时间,但一定可以提高吞吐量。

正因为云平台不擅长处理依赖于响应时间的弹性依赖,这类弹性依赖被称为弹性耦合,以表示与依赖于吞吐量的弹性依赖的区别。

怎么避免弹性耦合,才能充分利用云平台的能力。最简单的方式,是将组件的同步调用模式改为异步。因为服务与组件间的同步调用,会带来响应时间的依赖;而异步调用,则能将其改变为吞量的依赖。也就是将弹性耦合变成了弹性依赖,使得整个系统可以更好地利用云平台能力。

不过,当由同步变成异步时,意味着,原先产生的数据可能存在中间状态。比如支付,由原来的开始支付 -> 支付成功/失败;变成开始支付 -> 支付中 -> 支付成功/失败。

带来的中间件状态,在业务上是否有特殊含义,这在业务建模时,是需要考虑的第一个问题;再者,异步带来的一致性改变,对业务会产生什么影响,是需要考虑的第二个问题。

归根到底,为了解决弹性耦合的问题,我们需要将原味面向对象风格中默认的同步模型改为异步。

高内聚

微服务划分原则,不管是以限界上下文为主,还是以弹性边界为主。更多的还是要考虑业务形态。为业务赋能才是目标。多维度去权衡划分原则。

不能只考虑限界上下文,不管业务功能性还是团队认知负载是合理的,但弹性耦合了,一个扩容,链路上的所有系统都得扩容。也不能只考虑弹性边界,把简单同步的业务上下文都使用异步处理,忽略业务上的耦合必然性。为了异步而异步。

总结起来,还是要权衡,不要单维度走极端。这些永远正确的废话,真的很有道理。最近看饿了么CTO张雪峰的访谈,正好聊到了微服务问题,可以结合理论体会一下:

饿了么原来就是个单体,所有的业务逻辑就是一个东西、一个源代码库,C 端、B 端、D 端(Delivery,物流)全在一起,牵一发动全身,也就是说你在部署的时候,每个服务器都要布一坨这个东西,一是影响性能,二是发布很麻烦。只要有同学发布,即使跟你无关,你也要发布一遍,所有的机器都要扫一遍。我们做技术的就要拆解,肯定要至少再分一级。

拆分与否,我们当时就遵循一个原则:只要一个人有变化,一堆人要随着你动,或者叫“牵一发动大部分”的时候,这一定是有问题的。其实这也是逻辑原则或数学原则。所以我跟他们说,不要扯什么领域驱动、微服务了,就用这个原则。这个原则确实最容易讲清楚,但实践的时候要多次试、反复试。
单体是一个极端,微服务或单一原则是另一个极端。

饿了么从来没有真正提过微服务,从来没有过,我不去用这个概念。我们就是从业务的合理性去拆分。对领域驱动呢,我当时也是持观望态度,不能说保留态度,我觉得领域驱动是一个模棱两可的东西(顶尖 DDD 牛人或在大规模超复杂体系下成功实践过的同仁勿喷,毕竟让绝大部分技术同学吃透 DDD,无论 ROI 还是效率都很低),就跟架构一样,所以我希望回归朴素,就是从逻辑的角度,或者数学角度,给大家解释。所以当时我们也不做领域,我把以前的经验带过来,开始有一些中台的萌芽,比如说把交易系统、营销系统拆出来,把用户系统拆出来等等。

从逻辑上讲,当你十次里面有八次“牵一发要动大部分”的时候,你就没必要去拆,你就让它耦合(内聚)在那,哪怕最后合出来一个巨大的东西,那证明这个业务就是这样的,没办法。你要么抱怨很倒霉进入这个业务领域,要么你就自己想办法克服。当然还有一个办法就是你通过技术去改革这个业务,那意味着这个业务甚至整个行业的游戏规则都要变,在短时间内几乎不可能。之前也讲过,对绝大部分公司的技术团队来说,妄图通过技术驱动业务,还是省省吧。


后来我发现物流系统还有个很大的问题,搞物流系统这批同学,就是另一类极客。饿了么刚开始拆分服务,物流拆分得很夸张,直接同步变异步了。我说你们犯了一个错误,叫“为了异步而异步”。
大家以前的代码(交互)尽量都是一路撸到底嘛,直接写完,这个叫单体。后来搞微服务就要拆开了,结果他们不光拆开,拆开之后,还要用消息通知。我举个不太恰当但大家明白意思就行的例子,比如说算工资,本来可以直接算出来,他们非要先送一个你的职级,再送一个你的社保基数,然后送过来之后还不是马上给你,你要自己去取,我只是通知你有这个数据了。你取过来之后慢慢算,算完之后再推给另一个涉及工资计算的模块,诸如此类。物流同学就是用类似方式,他们真的把异步做到了“极致”(饿了么价值观:极致、激情、创新)。
但是他们做异步的初衷是什么?是因为物流的量很大。以前宕机是因为量很大,用同步的话服务器撑不住,所以就改异步。他们说至少可以缓和五秒钟,但后来我发现这五秒钟没意义。
我自己也体验过,比如我点个外卖,提交订单之后习惯性去刷一下,看看商户有没有接单,然后过一分钟看看骑手有没有接单。还要看地图,有时候看到小哥明明经过我这了,怎么先去送另一个人了?可能很多人都有这样的疑问,这个不能怪骑手,也不能怪系统,有各种原因,此处暂时不表。
大家都会去刷,后来我们发现用户在饿肚子时的心理承受能力就是三到五秒(淘宝、携程没这问题,大家对订单或物流状态变化的容忍度高很多),你是通过异步让这个订单不至于当场爆掉,但你延后五秒之后,堆积起来也很厉害,东西多了之后,最后还是爆掉,你只是让用户前五秒感觉系统没有宕机,但最终结果还是宕机。最后我们异地多活搞出来,几乎就没有大的事情了。

我们原来设想异地多活只能一次性切换,因为我们的业务是强耦合的,不像携程,携程机票、酒店关联度不大的,你要订机票 + 酒店,做个简单聚合就行了,但我们不一样,饿了么是用户下了单,商户接了单,物流就要送单,上下游其实是强耦合(高内聚)。
程序员可能会说,现实业务没你说的那么理想,该强耦合就强耦合,其实不是强耦合,另一个词叫高内聚,该内聚的时候你不要去追求什么微服务那些乱七八糟的东西,就应该高内聚,因为就是一个业务形态,业务才是最重要的判断耦合或内聚的依据。谁(调用方 / 消费方)也离不了谁(被调用方 / 服务方),你每次调用都涉及到它,干嘛非强扯开来?没太大好处,当然,可以分开发布算一个好处,但也仅是技术上的好处,不是业务或领域上的好处。

总结

我们将微服务架构看作是一种以业务上下文为弹性边界的云原生架构模式。弹性优先原则不仅仅适用于微服务架构,而是适用于所有云原生架构。

把功能需求和流量传导分为静态和动态的角度

静态的功能需求:

如果少数大需求需要跨微服务,是正常的,但是也要注意任务拆分,把大需求拆分到多个微服务,约定好接口,各自开发,各自部署,集中联调。

如果大多数需求需要跨微服务,那多半你的微服务拆分的是有问题的,你需要重新考虑你的微服务的拆分是否合理,必要的话合并一些微服务。

动态的流量传导:则是弹性边界,一旦流量上升,链路上所有系统都会被传导并进行扩容,那是不是拆分也不太合理,或者能否从弹性耦合变为弹性依赖。

微服务架构只是一种技术手段,使用微服务架构的目的,不是为了让你的架构更流行更酷,也不是为了让你的服务尽可能小,而是借助微服务的架构,让团队规模变小,大开发部门变成各个小的开发小组,并且各个小组应该尽可能独立,减少相互依赖,减少沟通成本。

而一个常见的问题就是错把手段当目的,为了微服务而微服务,服务拆的太细,反而维护和沟通成本大增。

理想的微服务架构,一个需求在一个微服务内就解决了,独立测试独立上线,基本不需要太多跨团队的协作。同时一个小团队维护的微服务也是有限的。

微服务不是银弹也不是哑弹

发表于 2023-02-19
字数统计: 2.1k 字数 | 阅读时长 ≈ 7 分钟

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

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

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

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,然后大量的逻辑存在于编排引擎中,这也是典型的伪微服务。像傻服务一样。

总结

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

架构师眼中的正交设计

发表于 2023-02-11
字数统计: 1.4k 字数 | 阅读时长 ≈ 4 分钟

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

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

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

2、消除重复

3、分离关注点

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

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

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

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

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

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

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

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

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

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

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

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

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

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

作为架构师,可以:

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

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

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


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

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

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


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

Kafka基础术语

发表于 2023-02-05
字数统计: 2.1k 字数 | 阅读时长 ≈ 7 分钟

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

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

消息模型有两种:

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

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

一个简单的Kafka架构图:

阅读全文 »

架构师的自我拯救

发表于 2023-02-05
字数统计: 3.1k 字数 | 阅读时长 ≈ 10 分钟

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

感觉十分在理。

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

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

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

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

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

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

阅读全文 »

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

发表于 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

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

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

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

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

总结

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

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

架构的本质是业务的正交分解

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

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

好独特的见解。

做架构到底是做什么?

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

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

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

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

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

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

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

正交分解

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

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

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

1、消除重复

2、分离关注点

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

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

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

开闭原则

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

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

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

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

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

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

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

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

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

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

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

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

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

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,一切都是花架子,形似无神。

12…15
朱兴生

朱兴生

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