一周技术思考(第17期)-废墟的召唤

大家好,这里记录,我每周读到的技术书籍、专栏、文章以及遇到的工作上的技术经历的思考,不见得都对,但开始思考总是好的。

 

废墟的召唤

 

我诅咒废墟,我又寄情废墟。-余秋雨

 

当我看到这句话,很合意了我近段时间所面临的要解决的问题情境。因为是我在写文章,我便自己做了主,借此机会,接下来我想抒情一下。

 

回想这么多年的程序员生涯,大体很符合一个架构上的场景,读多写少。大部分是在读别人的代码,小部分时间是写自己新的代码,更确切点说是大部分在读别人代码的基础上,来写自己的代码。

 

那些混乱的代码就是废墟,但你又必须在废墟上工作。

 

直到读了《遗留系统重建实战》中的序言里面的下面这段话,就更戳中了心中的疙瘩。

 

曾看到大段大段混乱的代码辗转反侧,不得入睡,生怕改错了一行而引起其他未知功能的缺陷;

 

曾对代码中一个几千行的方法中某些奇怪的注释--”请勿动这段计算逻辑“ 而深感无力;

 

有没有一丝阳光能够划破这重阴霾,照亮我的空间,驱除那一片片废墟,是有的,直到...

 

我维护了一个电信的系统,其设计精良简单、实现稳定可靠,且容易修改与添加新的功能。其设计思想让我发现了代码的奇妙之处和威力所在,从此点燃了我的软件开发之路,一往直前。

 

营造之初就想到它今后的凋零,因此废墟是归宿;更新的营造以废墟为基地,因此废墟是起点。--余秋雨

 

 

《重构 改善既有代码的设计》你没看过,《软件修改的艺术》你没看过,《遗留系统重建实战》你没看过,《代码整洁之道》你没看过,但你从业这么多年,真的就没有闻到过下面这24种味道?还是你已经麻木了,只顾交付需求了事?

 

摘自《重构 改善既有代码的设计》目录栏

 

但是,又在你交付的需求过程中,真的就没有被混乱代码坑过吗?就不痛?

 

就不想着哪怕一丝丝改变?

 

你缺少的不是梁咏琪,就是实在的勇气,拾起勇气,去做别人的那道光吧。

 

图自网络

好了,到这里,应该有一根分割线。

 

上面,我承认是我感性的表达,在这第一个主题结束前呢,我想再理性理性。

 

你说代码质量好坏,是用什么来评判的。

 

那我先说下,基于一个应用系统的基础,我们通常会面临的两个问题。第一个问题是,需求交付时长的问题;第二个问题是,线上出现事故的问题。那么,如果一个应用系统自上线以来,一直没有发生过这两个问题,是不是就默认你的应用系统架构是好的,应用系统的代码也是好的。

 

你会在这样的情况下,捯饬自己的代码吗,当然,是会有同学去做的,我之前也说过,每个团队中,都还是有一些有追求的程序员。他们所追求的正如下图所示的那样,要编写可维护的代码,能用业务语言识别的代码,能让同伴容易阅读的代码。

 

图自极客时间专栏《10X程序员工作法》第21讲

 

可这种捯饬会毕竟花费时间,连你的老板都面临着交付需求有压力的情况下,最终大家会不会向这种交付压力屈服呢,尽管你内心一直在呐喊,重构好了,后续就会更快,可眼前这一次当前的需求交付就慢了,你保证大家不会摇摆么。

 

另外,你又怎么能确保,重构了之后,一定够快呢,从你接到需求,到上线,这中间有好多步骤,影响时长的,不仅仅是代码呢,大家可以翻看之前的第14期的内容,就有介绍到业务方感知的需求交付周期到底包括哪些阶段。

 

你又说,坏代码,还会增加线上出问题的概率呢。

 

可是,你修改代码,尤其是,你真拿到梁咏琪给你的勇气之后,重构大段代码的时候,不是也会直接导致增加出现线上问题的几率吗。

 

尤其,还是在有团队定下了线上事故处罚制度之后,这时,大家是不是会想,改和不改哪个出现线上问题的几率更大呢

 

那,向左不行,向右也不行,怎么干嘛。

 

我的建议是,惩罚制度是要有的,犯了错,就要承担责任。可另外一方面,团队也要鼓励试错以及对于愿意试错,勇于改变的程序员们,是否团队也应该有相应的激励机制呢。至于应该怎样去鼓励试错,怎样激励,这是每个团队管理者都应该思考的问题,而且团队管理者还要思考自己有让技术专业人员有专业的时间去做专业的工作么

 

不然,那些坏代码不就真的堆成废墟了吗。

 

在结束这部分内容之前,再最后“加个当然”。当然,一个应用的质量也不能是仅仅归结到代码质量的问题上来。一两个人维护的系统,而且就是这俩人说了算的上下文环境中,系统有问题,完全可以归为代码质量。十个人维护的系统,尤其是十人之外还有配合部门,这样的上下文环境中,系统有问题,就不仅仅是代码质量这样简单。

 

有改变坏代码的勇气固然重要,也是必要,但同时呢,现在是一个多方协同合作的过程,这中间还需要有一种契约精神,接下来,就让我看看程序中的契约是怎么回事。

 

程序中间的“契约精神”

方法与方法之间,类与类之间,服务与服务之间,实际上是存在着某种契约的。所谓的契约,就是双方要遵守的合同,更准确点说就一旦一方毁约,另一方就会收到影响,甚至是损伤。

 

程序内也会像现实那样?确信无误,继续看。

 

有一个ServiceA类,它提供了一个方法createMessage,入参分别是两个String类型的参数,如下:

 

有一个ServiceB类,它是ServiceA类的消费者,会调用createMessage方法,如下:

 

public class ServiecB{
private ServiceA serviceA;
public void callMessage(){
  String parameterA;  String parameterB;//some logic ...  serviceA.createMessage(parameterA,parameterB);   }} 

 

注意,这里的createMessage方法就是ServiceA和ServiceB之间的契约。

 

好,那我稍微改变一下这个契约,会发生什么呢。

 

public class ServiceA{void createMessage(String parameterA,Integer parameterB){}}

 

也就是我把第二个参数的类型改变了,很显然,消费者ServiceB肯定会受到影响,不过,如果这两个类是在一个单体应用下,或者就直接说是在同一个工程内,我们的强大的编译器就会提前帮我们发现了,开发工具很容易帮我们发现契约失效了。

 

但,如果这一幕发生在微服务应用之间呢,为了帮助大家回忆并显性的认识,我贴出下面这张图。

 

图自《JAVA微服务测试》6.1

 

估计你肯定猜到了,就不会有那么的幸运,再像单体程序里的那样,有个编译器这样的“上帝之手”来照顾我们了。正像上面这张图中的示例一样,每个微服务都拥有自己的运行环境,这种情况下一旦发生违反契约的事件,就是生产性事件,再也没有编译器来帮助我们了

 

在微服务应用环境下,应用之间的契约仍然是那个把双方或者多方,纽在一起的方法,生产者一旦发生变更,就会导致消费者无法正常运行。

 

这些常见的变更,有大致如下几种情况。

 

1、服务更改了接入点地址,比如服务注册地址变了,或者是HTTP URL变了;

2、服务新增了一个必填的参数;

3、服务修改/删除了一个已有的参数;

4、服务修改了对输入参数的校验逻辑;

5、服务修改了返回类型或者状态码;

 

一起看个《JAVA微服务测试》中的栗子吧,如下:

 

{"id":1,"body":"this is a blog","created":"2021-06-01","author":"程序架道"}

 

一个微服务应用的生产者,产生一段JSON格式的数据,返回一篇博客文章相关内容。

 

消费者A只需要body和author字段,没有问题,忽略掉其它字段即可。

 

过了一段时间之后呢,来了一个新的消费者B,它除了需要body和author字段以外,还需要一个新的authorid字段。

 

{"id":1,"body":"this is a blog","created":"2021-06-01","author":"程序架道","authorid":"1234567"}

 

如果你是那个生产者,你会想到有两种方法,方法一,是直接增加一个 authorid字段,就像上面那样。如果消费者A遵循了Postel原则,它是可以继续成功消费这段数据的。

 

对你发送的内容要严苛,对你接收的内容要宽容--Postel原则

 

关于Postel原则,是来自一个人,名字叫做Jonathan Bruce Postel,他在数字电路中经常提及

”你接受什么是自由的,而你发送什么是保守的“,换句话说就是”在实现中发送行为应该是保守的,而接受行为应该是开放的“,后来人们便把这一原则定义为Postel原则。再后来,人们把这个原则引入到了程序互相调用场景之中,便又了上面那行”对你发送的内容要严苛,对你接收的内容要宽容“,此时人们也将其成为方法调用之间的健壮性原则。

 

--以上斜体部分内容来自维基百科

 

 

方法二,是增加一个authorinfo复合对象:

 

{"id":1,"body":"this is a blog","created":"2021-06-01","authorinfo":{"name":"程序架道","id":"1234567" }}

 

但是,如果采取方法二,消费者B的需求是满足了,但是跟消费者A之间的契约就变了,一个生产者的契约有时候是要签约多方的,这点大家都能够理解的,对吧。

 

而且,如果你不顾当时的“契约精神”,一旦这样做了,你影响的将是所有与你签约的,按照类似消费者A那种使用方式,来消费的消费者。这锅啊,你背定了。

 

估计你是不会这样做到的,你一般会采用废弃标识而不是直接废除的方法,对不对,就像下面这这样:

 

{"id":1,"body":"this is a blog","created":"2021-06-01","author":"程序架道",//标识废除"authorinfo":{"name":"程序架道","id":"1234567" }}

 

这样就有了一个让消费者进行更改的过渡期,发邮件周知他们。

 

这个主题说的内容呢,其实大多数人都知道,也会做按照正确的方法去做,今天我们把它拿出来,重点是强调[契约]意识,或者叫做[契约]精神,你没写的一个被别人调用的方法,被外部应用调用的服务,你都要有这样强烈的契约意识,因为你主动识别,并提前告知消费方,将会最大的节约找出BUG的成本。

 

不知,你是否曾想过,无论在单体应用还是微服务应用环境性,问题发现的越晚,就越难修复,而且问题的严重程度也不一样,编译期和运行期能一样么,而且类似刚才我们说的,那种服务之间的契约,如果没有提早发现,就是运行期的BUG。

 

我给大家贴一张在一个开发周期内,不同阶段发现一个BUG并进行修复所需要的成本曲线图,大家自行体会一下。

 

来源:Applied Software Measurement by Capers Jones(McGraw-Hill,1996)

 

 

什么是契约

What,又来,上面不是讲过了吗。

 

是,既然讲到了,我们就打算一次都讲了,这次往方法的外一层的角度来考虑一下,来到类这一层。

 

演化一下契约的定义,或者叫做重申,”两方或多方直接做某事或不做某事的约定“以及”依法必须遵守的约定“,当然在程序世界里,这里的依法就是按照规则。

 

不是跳到类这一层了么,那么,我们就用抽象类来描述吧。

 

来举一个教科书的栗子,如果你让一名程序员兄弟画一个形状,那名兄弟肯定会问你,画什么形状,因为形状这个概念是抽象的,如果你让这位兄弟画一个长方形,他则不会问你这样的问题,因为长方形是具体的概念。

 

这里形状我们可以定义为一个抽象类,它里面定义了一个抽象方法draw,长方形继承这个形状抽象类,从而实现了这个抽象方法draw,但是,注意,但是,如果长方形继承了形状,却不提供draw方法的实现,编译器就会不通过,这是因为长方形类没有满足形状的契约

 

 

这里的契约就是我们所说的违反了双方的约定,你也违反了规则。

 

好久没聊这么基础的内容了,既然写到这里了,索性抛一个问题,你应该耳熟能详,但却不能熟视无睹,试试看,在你工作多年以后,对下面这个问题又是如何理解的。

 

抽象类可以提供抽象方法,也可以提供实体方法,而接口只能提供抽象方法。为什么要有这样的区别呢?为什么有了抽象类还要有接口呢,仅仅是为了变相的实现多重继承吗?

 

本周的最后一个主题,我想结合着微服务这个话题,谈谈我们日常工作中很容碰到的一个问题,是什么呢,继续看。

 

是将新功能添加到已有的服务,还是新创建一个服务

我们要开发一个新功能,是在已有的服务内开发呢,还是新创建一个服务呢,这确实是个问题,那么,当我们遇到这样的情况,我们应该怎么判断呢。

 

其实,针对这样的情况,你就两种选择对吧,要么,要么。”你想做个大人,全都要”,好像在这里不现实。在平时设计评审的时候,我也听到过有不少人会甚至认为,我们的微服务就是只能追加,他们的观点是开发新的服务好过将功能添加到已有的服务中。

 

我个人是很不认可这他们的说法,更不认可他们的做法。

 

其实,有个原则可以帮助我们,忘记了“单一职责原则”了吗。

 

说到“单一职责原则”,关于它的定义,还有一个”定义轨迹“值得说下,历史上曾经这样描述过:

 

“任何一个软件模块都应该有且仅有一个被修改的原因”

 

再后来,修正了一次,定义变成:

 

“任何一个软件模块都应该只对一个用户或系统利益相关者负责”

 

最新的,定义如下:

 

“任何一个软件模块都应该只对某一类行为者负责”

 

最早定义中所包含的“被修改的原因”,这个原因显然是人提出来的,后来又把提出原因的人,说成了“系统利益相关者”,直至最后改成了“一类行为者”,最新定义里面的行为者,就是指一个或多个有共同需求的人,既然是有共同需求的人,那么,来自他们提出来的需求变化原因肯定是相同的,定然,也就只有一个被修改的原因了,可见这个定义的与时俱进性。

 

好了,根据单一职责最新的定义,我们就有这样的判断方法了:如果把一个新功能放入已有的服务中,将来这个服务内所包含的所有功能,他们变化的原因是否相同,如果相同就呆在一个服务内,如果不同就放入一个新的服务。

 

让我们一起来看一个《微服务实战》第2章中的例子,来验证一下我们刚才说的“理论”。

 

有一个股票交易软件中的订单管理服务,之前包含了[记录订单状态和历史]这样的功能,现在需要新增一个[提交订单到市场]的功能,如果我们但从[订单]这个角度来看,都是订单管理范畴的,似乎,把他们放在一起,看上去是合理的,但真是这样吗。

 

我们利用先前的“变化原因是否相同”这个理论指导来去分析,在提交订单到交易市场这个功能范围的变化驱动因素是所支持的市场功能和范围,比如提交到A股交易的特殊要,提交到深股交易的特殊要求等等,但是在记录订单状态和历史这个功能范围关联更紧密的是产品订单的类型和不同的账户限制以及促销相关关联因素,比如大客户的订单类型和普通客户的订单类型的特殊需求,促销类型不同订单列表信息返回字段属性不同的特殊需求。

 

如果像上面那样经过充分的分析了,也结合当前已知的业务信息了,但是,仍然存在模棱两可的情景,也就是还有模糊地带存在,这个时候应该怎么办呢,这里有一条有用的经验可以帮助我们:

 

宁可选择较大一些的服务,等功能变得更加特殊或者更加明确属于一个独立的服务时,再将功能从中拆分出去,这样会容易很多。

 

恭喜你,又思考一次。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值