蛋式编程(Egg-Style Programming)与业务内设计与组件式编程(Component-Style Programming)(上篇)
§ 1 蛋式编程(ESP)编年史
本文旨在尽量写得有逼格一点
Java 是天生适用于网络应用的语言
这是不知道从什么时候开始的说法,严格说其实(几乎)所有语言都能用于网络开发。但从实际结果上来看,java确实是在网络应用中应用场景最广使用最多的语言。
深究原因众说纷纭,但无外乎语法简单,无指针(和其他很多零碎)处理,方便开多线程,高可移植,免费这些原因。另外,最早jdk也确实提供了对应的applet和servlet。
这只是引子,再有好奇的同学自行百度并欢迎补充编辑,在这里,“英雄来历莫问源流”,一嘴带过不提
于是真的有好多人用它开发网络编程
这时,java本身提供的servlet崭露头角,这时他还只单纯的处理一些体量不是太大的功能,本身数量不多,逻辑也不是太复杂。
servlet 初长成
至少在当时,servlet 无论书写还是配置,都很简单,且满足当时的业务场景
最简单 servlet 示例
public class XxxServlet extends HttpServlet {
public void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException{ doPost(request,response);}
public void doPost(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException{
//...do business...
}
}
web.xml 中的 servlet 配置
<web-app>
<servlet>
<servlet-name>AServlet</servlet-name>
<servlet-class>AServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>AServlet</servlet-name>
<url-pattern>/AServlet</url-pattern>
</servlet-mapping>
</web-app>
于是servlet 开始茁壮成长,随着人们对网络应用的功能更多的期待,它的哥们们(如 JSP)也应运而生并广为流传,servlet能干的事越来越多,能处理的东西也越来越复杂
不管是啥,多了总会乱的
servlet 越来越强大必然导致越来越多的人使用它,并且给他更多的任务和更复杂的业务,最终形成了一种比较流行的、经过实际检验的一种网络应用解决方案。于是一个网络应用中的 servlet 膨胀了,无论是它的数量还是质量。俗话说,鸡多不下蛋,人多打瞎乱,体量的扩大导致 servlet 数量、复杂度的上升,进而导致servlet管理难度的上升。
servlet 控制器亮了
然后,出现了这样一种 servlet:
<url-pattern>/AServlet_*</url-pattern>
servlet 在配置(<url-pattern>)中使用通配,并复写了 service 方法,并将调用时的*作为参数或者遍量,按不同的值调用不同的方法。这就是基于 servlet 的控制器,它最大的作用就是直接控制了 servlet 的数量,一个 servlet 里可以包含若干业务方法。
之后,结合上面的方法和请求转发/重定向,除了这种控制请求访问自己不同方法的 servlet 控制器,还出现了控制请求访问其他 servlet 的控制器以及访问其他业务处理类的控制器。
servlet 控制器在众多 servlet 中脱颖而出、鹤立鸡群。
MVC 出现了
这种具有请求路由功能的 servlet 控制器非常好用,通常会被按不同业务进行区分和命名,一个控制器负载某一类业务下的若干功能,混乱的 servlet 瞬间仿佛有了条理规整了起来。在这个基础上,原本众生平等的 servlet 明确的区分出了角色:调度者(Controller 的前身)和执行者(Service 的前身)。
随着 jsp(特殊的 servlet) 的进化和流行,前端页面也划分出了模板和数据,并结合 Ajax 技术确立了用 jsp 制作页面的骨架,ajax 结合正常页面跳转访问 servlet 获取业务数据,通过ajax/El表达式结合填充数据以组合成最终页面的套路。并基于这个思路又出现了如freemarker之类的框架,当然还有类似easyUi的框架虽然区别较大,但大思路是一个因此可以归为一类。于是,页面也划分成了两部分:视图(View)和模型(Model),分别对应前面说的模板和业务数据。
这几乎是一个通用的解决方案:请求通过控制器备份发给不同的业务执行器,执行器处理的结果传给控制器组织成数据模型,并由控制器通知给相关的视图,视图和模型通过表达式等手段结合成最终的页面源代码,并返回给浏览器渲染为最终的展示效果。
这就是 MVC 三层架构(模型+视图+控制器):不同的对象被分门别类划归为不同角色/种类,虽然不同,但每一类对象都有相似的设计目的,于是可以将它视为一个逻辑层。
快速发展的 MVC
当 MVC 三层架构作为一个成熟的概念和解决方案流行后,如何设计一个项目仿佛变得简单起来,人们沿用了这里的层的理念,在 MVC 的基础上将一个项目拆分成了多个层次:
- 视图层:前端页面
- model层:填充视图用的数据
- controller层:请求控制器
- service层:被controller控制/调度,用于实现业务
- dao层:数据库访问 持久层:表和数据
每一层都有比较明确的功能定义(或者称为边界更能达意),当一个类被写在某一个层时,它作用的性质其实已经可以猜到了,具体是干什么一般在命名上体现对应的业务。
比如 BillingController。很明显,这是一个用于分发请求,组织返回,调度service的控制器,从名字上看,是负责订单业务的。
这极大的简化了设计难度。当一个项目开始开发时,层作为一个维度,业务作为另一个维度,整个项目的几乎所有文件(除了配置文件啥的)几乎都被这两个维度涵盖,于是coding开始变为了一场填表格的游戏。
项目经理或者teamleader完全可以先把框架架起,声明好各个包,勤劳的还会先把预设的类声明上(比如整个项目设计3个业务,他们会声明三个空的controller等)。然后等到数据几乎确定的时候,开始建表,表创建好后,实体就有了,基础的dao也有了,基础服务也有了。
上述的这些自底而上的coding,不要说设计,甚至几乎不需要业务支撑,完全属于体力活。而后,才开始轮到业务。通常的,UE图出现了,研发同学们明白了图中每一个能点的地方都需要什么结果,能产生什么参数,需要完成什么样的处理。并由此明确控制层的接口,进而明确需要哪些service,用到哪些数据访问操作……这样又进行了一轮(若干轮)自顶而下的coding……于是层+业务的二维表格被填充的已经差不多了,后面只需要以业务或者测试为驱动完成开发工作就好了。这么个流程几乎屡试不爽。
同时,很多经典框架雨后春笋一般拔地而起,纷纷戳中了众多项目组研发人员的爽点……于是,MVC生态初现。
千秋万载一统江湖的 MVC
不得不说的两个框架,strust(2) 和 Hibernate。
Strust 的 valuestack配合其OGNL表达式极大的简化了视图与模型的结合难度,进而进一步提高了视图与模型的隔离度;提供了灵活的可自定义的过滤器和拦截器接口,允许用户方便的扩展;Action和ModelDriven 不仅极大的规范化了 Controller 的配置,更重要的是它极大的明确了MVC的概念。
说它强化了mvc概念是因为原本基于 servlet 的mvc大家还能玩出不同的花色,而这样完备的强大的方便的MVC框架一出现,大家纷纷舍弃了自己原创而投入了 strust 的怀抱。作为一个框架,它仅仅是一种技术,并不是魔术,所以并不是万能的,这要求开发者必须按照它规定的轨道放卫星,换句话说:它定义了一些自己概念和规则——比如action和它的配置方式。而框架的流行必将导致更多的人接受和使用这些概念。这些概念,都是源于 MVC 的,算是对 MVC 的加强:分工更明确、更易用、更具体。
Hibernate 是一个大名鼎鼎的 ORM 框架,针对持久层和数据访问层,也是层的概念,和strust几乎无缝衔接。它提供了基本CRUD的模板类,上一小节里的体力活被大大的简化了;它甚至提供了托管模式(虽然笔者认为它很坑),可以把流经 Hibernate 的数据对象直接视为对数据库数据的映射(比如修改某 entnty的某字段,对应数据库对应表对应行的数据会自动更新);并提供了内嵌的缓存机制以期提高效率。
当然,不能漏掉传奇框架 Spring:
- 它是一个强面向接口编程的框架,面向接口的设计思想被它体现的淋漓尽致
- 它是一个 IOC 框架,弱化了对象的存在感,不用你声明对象,也不用你销毁,不用关心哪个具体的对象包含哪个具体的对象。在 Spring 中,声明即存在!
- 他是一个强配置化(后来进化到了强注解)的框架,而所谓的配置和注解,在这里其实就是一个通知框架的手段,相比其他(不通过配置的实现)相比,它更加有套路,更小量,更集中。
- 他是一个强易用化(在当时和 EJB 容器相比)的框架,几乎一切都是易于使用的
很自然的,这三个家伙合体了,SSH 出道了,这个组合风靡了几乎所有项目。它套路明显、业务以外无设计,因此可以快速无脑搭建,并且适应力极强,相关框架非常成熟。而后,在同样的道路上,也出现了更多更优秀的框架及其组合,极具代表性的比如 SSM。
当然这些框架都是脱胎于 MVC 的,他们对MVC生态的成熟意义重大,而这种设计思路也逐渐走向神坛、君临天下。
并不万能的 MVC 冕下
即使 MVC 冕下神威如渊如狱,但它依旧不是万能的。
在 MVC 的生态环境下,对比之前的内容,其实不难发现这些基于 MVC 的框架都没能对业务层提供什么太有效的帮助。不论是对视图的调度(MVC),对数据的基本操作(ORM),对对象管理的简化(IOC/DI),对统一重复工作的剥离(AOP),都没有对实际的业务产生什么本质的影响。
这也是理所当然的,如果真的能做到这种程度, 也就意味着框架的组合可以自动/半自动的帮开发人员完成业务,那这本身就是一件诡异和恐怖的事情(大家都可以失业了)。这就好像你可以准备尽可能专业的厨房,无论你需要什么它都已经准备了,比如烤箱、急冻冰箱、各种模具,并且绝对会出现在你方便操作的位置上,但你不可能期望它可以自动的烹饪一桌你期望的菜肴。
于是,MVC 生态的最终效果出现了——业务聚焦(Business Focus)。与业务无关的几乎一切,都由 MVC 生态代劳了,只有纯粹的业务留给了开发人员也只能由他们完成。
不好的东西出现了
MVC 生态对于研发人员来讲就是一个强大的 buff,可以使他们的工作更加纯粹——纯粹于业务。而在一个较长的时间中,纯粹于业务的开发状态并没有带来什么影响,反而带来了巨大的开发优势和效率提升,于是一时 MVC 一时爽,一直 MVC 一直爽。
在 MVC 的生态下,(相当一大部分)程序员们极度重视业务,重视有助于业务开发的框架和技术,于是业务开发的效率提升得幅度很大,速度很快。干活效率提高导致了工期缩短和工作强度变弱,于是各个研发团队又被注入了更多的研发任务,导致工期紧凑、工作强度变强,这又反过来催生了更多的帮助研发人员缩减工作量的工具和框架,于是 while(true) 了。
偏偏,偏传统项目(泛指非大中型互联网项目)在市场上的主导地位占据了比较长的时间,成规模的互联网项目也没有普及(在不算短的时间里并不是所有公司都有能力开发中大型互联网项目),这进一步为 MVC 生态提供了温床。
需要澄清的是,MVC 生态并没有本质的问题,其存在极度合理,本文初衷也不是用来专门怼他的。
项目架构的进化
MVC 生态已经发展到了相当成熟的地步,这带来了相当强大的承载能力。人的欲望是无穷的,客户的欲望落实到公司上叫做生意,递进到研发人员身上时需求就出现了。MVC 生态膨胀的承载能力开始承载越来越多的用户的越来越多的需求,这些需求让原来的 MVC 生态看起来好像不是太美妙了,比较之前咱们说了,不管是什么,多了总会乱的。
关于承载力:我实在不能找到太贴切的名词,只能强行用承载力命名。这其实包括框架体系中能容纳多少个需求功能(无论这些需求直接的条理清晰与否),项目运行中可以抗住多大的访问压力,或者简单的描述成使劲往这个框架体系中扩展需求并使它崩溃前的容忍能力。
于是,MVC 生态本身开始发生变化,当集中在 service 层的业务臃肿不堪、混乱到一定境界时,顺应MVC常规思路的人们理所当然的想到了拆分。
于是一系列演进开始了:
- 首先是应用数量的拆分,从单点过渡到了集群,而每一个集群的节点都是整体的一部分,整体主要体现在数据的统一,多应用,单数据库。
- 数据库也扛不住了,于是人们将读写进行了分离,通过逻辑数据中的不同节点分离了读写,多种主从同步的手段则保证了数据库集群在逻辑上的统一
- 但是数据量这玩意的增长是无止境的,于是更多用于辅助持久层的东西出现了,缓存
- 更进一步的数据量增长又促进了分库分表的成熟,并成为一种常规手段
- 访问量/并发量的增长也是无止境的,而一些商业活动更会促进这种增长,还是在极短时间的极大增长(没错我说的就是电商的促销活动),迫不得已的消息队列出来发光发热
- 在这些技术发展的同时,一些架构理念也开始发展,其实截止到上面说的,已经是分布式的架构了
- 在完整部署多份应用的基础上,应用本身被按照业务和功能进行了拆分,形成了各个子系统,子系统可以独立部署和运行,分布式越发成熟了
- 前后端分离出现并实际使用,从此,前后端开发解耦,两边开始同步进行
- 服务的概念出现了,SoA的设计思想出现并被投入项目
- 容器技术出现并成熟,催生了微服务的诞生,通常这俩是结合着用的
- 流行的微服务框架诞生,微服务+中间件的大生态成熟了
其中细节因笔者阅历限制,做不到完善,但大体脉络差不多表达出来了,好考古的同学请担待,实在能力有限。
蛋!!!
拆分已经被大神们玩得炉火纯青了,毕竟这东西用顺了,MVC 的各个层也是拆分出来了,此时,一个项目架构大约可以示意为下图:
这个图(实际上不是专门画的,所以)省略了很多东西,只是为了示意一下到此时,项目的结构已经和原本的 MVC 生态不太一样了,但这种基于微服务+中间件的生态依稀还是分了三个大层次:
表现/输出层:这里有页面,对外的接口/协议/服务以及其他类似的输出也勉强可以算作这一层的内容
服务层:主要是各种服务、业务、逻辑和部分中间件
持久/数据层:主要是保存数据,但不限于数据库,缓存、分词中间件(比如ES)都具有持久层的能力,通常是多方配合的
$\color{red}{这就是标题中蛋式编程的——蛋}$
表现层有专门的开发人员,对后端研发而言,表现层(甚至所有对系统外部)的交互都是一层薄薄的接口或协议,这是蛋壳。
持久层相对比较集中,无论是功能还是开发量,并且比较核心(一方面比较重要,另一方面稳定项目的工作量很少在这一层上),这是蛋黄。
服务层则相当于蛋白,在表现层之下,在持久层之外,量大、混沌、流动。
问题仿佛解决了,然而并没有
我们不妨把这种新的体系叫做蛋式体系
蛋式体系的叫法只是出于对这种层次划分的比喻,并为蛋式编程的概念做铺垫,其架构体系其实是对微服务/分布式架构体系的基本通用套路的粗浅描述
蛋式体系从实际上提高了整个项目体系的承载力,之前遇到的很多问题仿佛都不存在了,世界如此美好,此贴完结
……
……
$\color{red}{怎么可能!!!}$
随着蛋式体系的健全,以它为核心,蛋式生态逐渐建立、完善,并在一定程度上流行起来。蛋式生态对软件开发的影响涵了盖从业务方承接需求到产品交付的过程中几乎所有方面,包含如何使用此体系、如何开发或迭代项目、如何运作全部的研发团队、团队间(成员)如何协作、具有何等的工作节奏等等等等……
我们说了不止一遍,用户的需求是无穷无尽的,而上面的这些发展和进化也不是万能的。这里有两点需要强调:
首先,拆分,尤其是这么多维度的拆分并不是没有代价的。比较有代表性的就是中间件,缓存引入后,随着实践的深入又引出了缓存的雪崩、穿透和击穿;mq引入后,逐渐的消息丢失与重复消费又成了一大难题(当然,这种结构机制导致的问题基本都可以通过完善机制的方式解决或规避)。
其次,可以参考servlet到MVC的演进,框架和架构的进化并不能直接影响业务本身,到蛋式生态的进化也不行。
而为什么之前的问题仿佛都不存在了呢,这是因为 MVC 生态到蛋式生态的演进是增加整个框架体系的承载力。原本业务的总量和混乱程度所凸显出的问题,因为承载力的提升而归于了平静;同时,并且蛋式生态其实具有极强的伸缩性(最直接的:加节点),并且因为使用者众多,使蛋式生态中归因于框架体系(机制)本身的坑被填的差不多了。
从另一个层面上,MVC 生态到蛋式生态演进确实打破了MVC 生态本身的瓶颈,提升了整个架构体系的业务承载力。诸如海量业务造成的管理不方便,大访问量下无法抗住压力等问题,确实可以在这个过程(架构的演进和技术进步)中解决了一部分。
那么,架构体系的问题已经勉强是被解决了(分布式的一个特点就是当体系和机制趋于完善时,整个项目体系的伸缩性会变得特别好),而真正难解决的问题才开始有机会暴露出来。一定有同学迫切的想知道到底是什么问题,此贴到现在到底想说什么。其实前文已经做过铺垫,踩到过痛点的同学估计已经猜到了,咱慢慢细说。
困境
蛋式生态就像是一张舒服的床,周围摆满了丰盛的食物,躺在床上的业务被迅速催肥了。更糟糕的是,床是可以伸缩的,业务的丰满并没有带来什么棘手的问题就被扩展后的床包容了。于是,业务就被催肥的更加变本加厉了。催肥--扩展--催肥–扩展的若干个轮回(用户的需求是无穷无尽的,我不想在重复这句话了,但……)之后,项目的管理者和开发者们(可能还包括他们的东家)发现,项目的维护成本大的不可想象。
当然,遇到这个问题的第一反应就是加派更多的运维人员(里面甚至还得有人客串客服的角色,要去和客户沟通)。又当然,这往往收效甚微,不会有人认为当一辆车出现问题时只要有足够多的人扶着它,就能把它当成正常车开吧,那太可笑了。于是必须有一部分研发人员从需求的海洋里偶尔露个头,协助运维人员搞定那些运维人员解决不了的问题,然后再沉浸回去……这牵扯了太多研发人员的太多精力。
为了彻底的解决它(其实只是从现象上解决),越来越多的研发人员不得不投入更大的精力到运维性质的工作中,安排研发人员的专职运维日,设立值班制度,规定解决问题的deadline等手段都被迫(当然也有主动的)使用了出来,甚至有“不管问题解决情况怎么样,全组研发人员必须先通过海量加班,在公司管理者面前显示出足够努力的样子”的昏庸方针出现。
越来越多的项目组或开发公司有机会体会一个困境(当业务量与框架体制的承载力的比达到一定程度时才有机会触发)——成长-健康困境(Grow-Health Dilemma)。
成长-健康困境:是指$\color{red}{强制业务需要发展的前提下}$,项目的业务一定程度上的增加导致项目可维护性更大程度的降低,并且这种现象只能削弱不能根治的困境。
当业务成长本身被作为一种需求时,过量充盈的需求和有限的研发人员研发能力和研发时间会形成矛盾。为了达到目的,研发人员只能通过增加研发时间和加快研发进度来解决。
研发时间简单,通过加班就能达成。研发速度属于能力问题,能力的成长需要时间,等待研发人员开发的需求不可能给出这些时间。并且,即使能力提升,人类种族的物种限制也不太可能具有无瑕疵地完成这么多需求的研发(如果有人认为可以,那有两种可能:一些神确实可能这么牛,但更可能的是看到这里的同学对无瑕疵地这几个字存在误解)。
于是,在周身充斥了各种必须和不可抗力的环境下,处于这种状态的研发人员通常采用将将好实现业务的方式开展工作,并伴随着在代码中埋下隐患。埋下的隐患经过发酵之后成为线上问题,最终扔回给研发人员解决,当然不见得是当年挖坑的人解决,很有可能遗泽了后人(这又容易导致占用更多人员的更多精力解决)。同时,处理问题不可避免的增加了相关研发人员的工作量,因此他们很可能使用将将好修正现象的方式解决问题(这里说不准会不会又挖了个坑)。于是不完备研发——造成问题带来工作量——挤压其他工作的时间——不完备研发的 while(true) 出现了。
这个死循环其实并不是问题的全部。如果问题仅仅是如此,停下脚步一轮重构一轮规范就能解决问题,如果没有,那么就两轮,在配合规范、约定、审核其实还是可以解决的。但这基本是不可能的,前面强调了,业务发展本身就是一种需求,换句话说,不可能拿出专门的时间来去做这些工作。当一辆跑在路上的车出现了问题,不想停车就想修好车这种事靠谱吗,当然不。于是这个困境被不断酝酿,直到临近崩盘,才会不得已的开一个新的项目,并仅仅修复/更新一些框架体系的内容,业务照搬(有时也会做一些梳理归纳精简,不过这是在那些项目组真的下决心的前提下才可能发生,并且通常不会完全达成目的)。这相当于把一个鸡蛋换成鹅蛋,蛋式生态的承载力变大了,但就和前面的两次一样,再出问题只是时间远近而已。一般的,蛋式生态本身越健康,酝酿出成长-健康困境的时间越长,但困境的程度越深,能削弱的程度越小,也越容易导致相关研发人员破罐破摔。
这里稍微总结一下,需要将这个困境阐述清楚,以防认真的同学认为这只是文字游戏。
完整的成长-健康困境(GHD) 之所以被(笔者)称为困境,是因为其由多个问题环环相扣的组成:
- 首先,困境的主体是上述不完备开发、出坑、挤压时间、不完备开发的死循环,这个死循环输出的是源源不断的问题以及持续降低的项目(包括代码和业务)可维护性
- 然后,强制业务发展成为了困境的起因和阻碍。除开研发人员本身的原因(除开不意味不重要),确实是强制业务发展导致的工作量增加和工作时间没有同步增加最容易催生最初的不完备开发;而在此困境中,也确实是因为强制业务开发导致很难集中精力打破死循环,进而导致这个死循环不断酝酿。阅读到此处的相关同学先别头大,这并不意味着强制业务发展本身是不合理的
- 进而,在具有此循环的研发团队中的研发人员被迫(当然也有主动)加入这个死循环的工作模式中,成为困境的助力。这不全是此时研发人员的责任,毕竟在这种模式下,只有继续按这种模式开发才是性价比高的选择。毕竟工作量和工作时间可供伸缩的空间并不多,并且不按这种模式开发,往往意味着需要重构或整理一部分现有业务,这不仅带来更多工作量,也会带来更多风险。那么,很好理解的,越多人融入这种死循环工作模式,模式越不好打破,越容易促使更多人融入,最终造成的后果也越严重
- 最终盖棺的,是随着困境酝酿,一些前期可以解决的问题在质变后已经几乎无解。之前,问题可以处理,难度和资源消耗的问题而已;但质变后,或者已经无解,或者难度和资源消耗几何倍的提升。质变后的问题通常具备一些特质,这些特质中的几个交织并严重到一定程度都可能使一般问题演进成质变的(困境中几乎无解的)问题:
- 影响范围广,但一般情况下不会造成极端严重的后果(否则一旦发现必然被修复)
- 并不是什么技术含量很高的问题(通常比较low)
- 不好找出相对写法统一、操作简便的修复方案
- 修复工作的工作量大(尤为突出)
- 验证困难
- 修复方案会对现有数据和业务产生负面影响,
- ……
可能有人对最后一点抱有质疑,这里给一个容易理解的例子:
open service(可以理解成controller)层,约定了一种返回格式,比如叫 ChResult(call handle result),里面定义了状态码,异常信息,返回数据等。很明显,这玩意应该是open service的返回类型。但是,一些研发人员在前期(别管是什么原因吧)让manager层(可以理解为service)返回了这东西,此现象涉及一个完整的module。又因为种种原因,这种写法保留了下来,并且,项目已经出现了成长-健康困境。
随着时间推移(比如一整年的需求迭代),这个模块进化成了一个业务群:里面有一大堆业务,独立运行,多点部署,多个环境。
现在,有人(非团队要求)想调整这个没有常识的写法,将处理open service的返回从manager层解放到open service层中。提问,他敢真的动手修复这个问题吗?答案是不敢。提醒认为此时还可以由个人行为修复此问题的同学,千万别莽撞的动手。
首当其冲的,你可能对一整年需求迭代造成的业务复杂度扩展、代码量增长、分支增加、环境复杂程度有误解。除此之外,你有估算这里的工作量吗?有把握完成修复且不影响业务运转吗?能独立搞定n个分支、n个环境吗?能应对n个分支、n个环境下修复此问题时,具体修复方式的不同吗?能独立完成相关的验证工作吗?以及最关键的,就算你都做到了,你完成其他工作的工时还够吗?这并非教唆同志们没有担当的逃避,而是个人行为几乎不可能完整修复这种经过积累产生质变的问题。
可能有人会疑惑,笔者为什么要强调是个人行为。因为团队行为被锁死了,别忘了困境的大前提是强制业务发展,团队的整体行为必然是需求迭代。而在整体迭代需求的过程中,希望能在一定程度上对已有问题进行修复,那么只能靠研发人员的自发行为,即,当某研发人员意识到某个问题时,自主的找到修复方案并自主修复、验证。而困境的第四个因素,会导致即使整个团队中都是由这样有觉悟的研发人员组成,也会因为这些质变的问题导致个人行为收效甚微,甚至起反作用。
那么,可能还会有人反对。因为有少量团队,会从研发团队中专门分出一部分人,在一些特定的时候转职差缺补漏,为了表达方便,咱们暂时把这一部分同学统称纠察队。这种模式下,好像可以用少量人解决一些常见问题。但这里其实有那么几点需要说明:首先,绝大部分的纠察队在查明问题并确认一种简单易用的修复方式后,通常会进行小范围尝试,验证结果,验证通过后,他们通常会(在会议上)告知整个团队问题的所在、修复方式以及注意事项。是的,绝大多数情况下,不会由纠察队直接完整的修复问题。一方面是工作量直接压在他们身上不现实,指望有限的几个人把整个团队以及离开团队的前辈们造成的坑都填上怎么想都不靠谱;另一方面是因为效率,由他们不断发现问题并找出解决方案并推广到全队的效率远比让他们全权填坑更高。而一旦修复问题的工作在团队中推广,其实已经打破了困境,因为此时构成困境的第二点其实已经(至少暂时)不存在了。其次,那些由纠察队自行修复的问题,仔细观察,它们基本都没有到质变问题的标准,同时,也很难造成普遍且恶劣的影响,即使它们中的一部分可能在一些场景下回造成极端严重的后果。这里需要解释一下,这并不是文字游戏,而是限于表达能力限制。为了对比两种问题,下面分别列举一个例子,两个例子都忽略了不必要的信息,但都真实存在过
什么叫一些场景下会造成极端严重的后果?
用户发出的……姑且叫工单吧……下发至系统A,A下发系统B并生成工单b,B下发系统C并生成工单c,这是前提。系统C处理此工单时发生异常,重试无效,并在1个月后才暴露出来(笔者也想知道为啥一个月之后才暴露,所以不要计较了)。经过运维和后端运维分析,明显工单c的数据不正常。此时应该查找系统B中的工单b核对数据并在修复后重新推送,但系统B中没有任何相关信息。最后经过一个小组一天排查,在系统A中勉强找到了对应信息,但问题是,只有极少数情况才会由A将工单发送给系统B,且因为工单c数据异常无法直接判断其来源于系统A。肯定有人好奇系统B中的信息哪去了,因为工单b从系统B下发系统C后,还有一些逻辑,这些逻辑异常了并导致了回滚,仅剩的通过aop记录的接口日志还因为时间太长而被清理了,所以此工单在系统B中的痕迹消失的一干二净。为什么说是极端严重后果,后果是可能造成用户的操作没有响应并丢失操作信息。此类问题的难度通常在于发现、定位和修复方式且频率不高(高的话系统早就炸了),但一旦发现,必然不能不排除,而只要定位了问题并修复后,基本都能痊愈。
什么叫普遍而恶劣?
某项目日志中可以看到非常多空指针,但日志内容都是"xxx失败,原因: java.lang.NullPointerException",是的,后面都没了,顺带一提,项目已经上线了。为什么叫普遍,因为日志里都是一坨一坨的空指针;为什么叫恶劣,因为日志里的信息即使配合代码分析也不足以辅助研发解决。最后这个问题直接导致了全组加班重新修正日志,重新上线,等问题复现后再修复,进而再上线验证,耗时一周以上。此类问题的难度通常在于修复本身。
有意思的是,一些公司或项目组会在传统项目(蛋式生态前)阶段(通常认为,在业务扩展阶段的分布式/微服务项目才几乎不能避免困境出现,但有时候,尤其是研发团队不成熟时)提前陷入困境,并且他们几乎都通过将项目往蛋式生态演进的方式解决当前的问题,饮鸩止渴。
§2 蛋式编程(Egg-Style Programming)
§2.1 定义与诠释
ps:下面会出现一大坨似是而非的“概念”,请不要纠结“一个不是所云的人定义一堆似是而非的概念”/“啊,这个帖子风格好民科啊”这种自找纠结的问题,只把它们当成一个含义的名字或者一个长名字的缩写就好(要不表达起来真不方便)。另外,下文遇到的“概念”,都是笔者亲身体验或踩过的坑。
蛋式编程(Egg-Style Programming):即成长-健康困境(下文简称为 GHD)中研发人员的不完备研发的编程(研发)模式,同时若项目暂时未陷入困境,但随时间推移有能力导致出现的也可以定义为蛋式编程(下文简称为 ESP)。
§2.2 判定
判定蛋式编程(ESP)的最终依据是若项目发展(需求迭代)足够长,当前研发模式最终会导致GHD出现,或具备和GHD下的开发模式基本一致。
所谓最终依据,这里的意思是将视角置于结果反窥过程的定性,即若行为、状态、条件A是产生结果R的主要因素,且结果R已经出现或可以预见的肯定会出现,则基本可以判定行为、状态、条件A成立或存在。
ESP判断的本质是判定当项目中业务量的增加时,业务逻辑的条理性、清晰程度、可维护程度是否会随业务量的增加而出现更大程度的增加,对外表现为项目可维护性持续降低,并最终(有较大可能)导致项目基本不可维护。
虽然是赘述,但笔者希望澄清两个概念间的区别——ESP和GHD
ESP的本质是一种不完备开发的研发模式(由研发团队中所有人员的工作状态和整个项目甚至部门的运转模式共同组成),笔者不推荐在相当比例的项目中研发团队成员陷入此种模式。当然,实际上笔者不推荐在任何时候使团队中成员普遍使用ESP,但特定的一些项目中,确实用了也不会造成什么太大的影响,比如项目是个一锤子买卖,不用维护迭代更新扩展。GHD的本质是一种项目生存困境。它往往是项目存在了较长时间后,或经历一定迭代后陷入的一种棘手的状态。
相对于GHD,ESP是不强调强制业务发展,以致于ESP其实只相当于构成GHD的四点中 "主体" 的一部分。或者这么说比较容易理解,ESP是GHD之因;GHD是ESP下的项目发展到一定阶段的严重后果。
虽然ESP不强调强制业务发展,但ESP就是GHD的主体,而前置业务发展这一点太好凑了(笔者就没见过几个团队不强调的,尤其是在有个拎不清的上峰的时候)。这两点凑齐了,剩下的基本就是时间的问题了。这就是为什么笔者要强调这两个概念:首先,他们有本质区别;其次,一方几乎必然导致另一方,关联性太强了
从编码的角度,若服务层(主要承载业务逻辑的层)中的逻辑基本是面向过程的,或业务流程是面向过程的,或研发基本是面向需求翻译的编程时,基本可以判断属于 ESP。
面向过程:怎么叫面向过程,相信各位看官都清楚,就不多说了。只提醒一下还没反应过来的同学,面向过程的编程和不用对象是两回事,代码里逻辑是用各种对象写的和面向对象的编程也是两回事。
面向需求翻译的编程:假设业务描述中,当xx的值大于某界限时,需要做……。若实际开发中此处的代码和下面的伪代码类似,则可以称其为代码是对需求的翻译。这种现象是不可避免的,尤其是对比较简单的逻辑。但是,如果项目中代码基本都是对需求的翻译,就可以定义为面向需求翻译的编程,几乎无法避免出现各种问题(真的是各种问题,什么奇葩问题都能碰到)。
这是因为研发人员接到的需求的本质是一篇文章,或者直接称之为一份描述,无论其载体是prd还是ue图还是其他,都是为了将需求描述清楚。随着业务体量的逐步扩大,早晚有一天,不可能有一篇文章或一篇描述可以完整且正确的描述整个业务系统的功能,即时完成了,研发人员也没有精力去看并且完全掌握其中的信息(因为里面信息量和内部的联系会多到爆炸)。实事求是的讲,看官们可以回忆你们接到的需求(尤其是维护、迭代的需求),基本都是只描述了一部分业务的“剪影”,而不是“全景”,没错吧。
问题就在这里,面向需求编译的编程,因为是对着需求翻译的,所以只有在对需求的描述都是完整和正确的前提下,才能保证翻译出来的代码描述出来的业务逻辑也是完整和正确的。这怎么说么,联想一下同学们各自见过的需求,明显属于扯淡,对吧。if(xx.val()>limitValue){ //doBusiness }
从团队成员的工作模式看,若下面列举的内容符合多点,基本可以判断为 ESP:
- 产品同学将将好表达出自己需求
- 研发人员将将好实现需求
- 测试将将好测完黑盒正向逻辑
- 运维组无法处理相当比例的线上问题
- 容易催生值班制度
- 沟通频繁,甩锅频繁,人员流动频繁
从部门或公司的运作模式的角度,有若干种情况极易导致 ESP(虽然不是必然):
- 需求源源不断的由客户提出,或由业务方主动收集
- 客户可以提个性化需求,要求研发个性化系统
- 研发的输出量和部门/公司效益成正比(但不一定是线性)
§2.3 特性
ESP模式下的项目具有如下特性(其实都是DEBUFF):
- 业务混沌
- 设计忽略
- 边境效应
- 业务概念混乱
- 连坐触发
业务混沌
业务混沌是最直接的影响,其直接表现为团队中,没有人或者只有极少人清楚项目中各个业务的具体流程,并且几乎无法了解特别细节的业务。业务混沌并不是ESP下项目100%表现出来的特性,但它的出现也只是时间的问题。
设计忽略
设计忽略是指,研发人员在研发某需求前通常不会投入过多的精力进行程序设计。这导致业务仅仅是由代码堆砌而成,且这些代码在运行时,其执行的顺序恰好可以组成所需的业务流程。但实际上,比较良好的处理需求的方式,应该是程序设计的重要性高于编码本身。
我们当然可以说所有业务都是代码的堆砌。但是很多研发人员出于自己或环境的因素,往往完全忽略设计层面,秉承“废啥话,干就完事了”的总体思路强撸代码。这种做法当然可以实现功能,经过比较良好的测试后,也能使程序可以正常运行。但是,当业务迭代、变动、加强时,影响了相关人员透彻的理解和调整业务。也在平行需求、类似需求的研发过程中,很难被复用,并且通常会对相关的抽取优化起到阻塞的作用。
而实际上有效的设计环节可以帮助我们做很多工作,比如:
- 此功能是不是已经做过类似的了
- 是不是可以复用
- 老业务有没有参考价值
- 复用时需要做哪些调整、抽取、优化
- 新作时整个需求需要怎样的流程
- 流程涉及到哪些系统或服务
- 涉及什么数据
- 如何组织数据的结构和行为
- 完成需求需要这些系统或服务的哪些对象(功能)
- 现在已有哪些、没有哪些、需要调整哪些
- 对于没有的那些是由我自己实现还是请相关系统和服务的同学辅助
- 基于什么方式实现
- 我实现时哪些片段可以作为一个对象、工具、机制、配置存在
- 我实现时是否可以对一些成分进行抽取成为一个切面或者其他东西
- 对于流程中一个很小的点是不是可以定义成一个具有通用含义的接口,并由我提供默认的实现,这样别也能用了
当对一个需求完整的进行了分析后,我们往往可以确定一个可以完成开发目标、相对不影响现有业务的、易实施的研发实现方案。而对比这个方案,分析之前,你听到需求之后第一反应的方案往往只是“这么做能做出来”的水平。这个方案通常可以帮我们做到实现当前需求要求的功能,同时代码量不会显著增加(项目处于迭代维护阶段时),业务步骤度不会显著增加,业务阅读难度几乎不会增加。
边境效应
边境效应是另一种维度上的比较严重的问题,因为他是表现在开发人员本身的问题。ESP 环境下的研发人员会和出现类似国家边境线的特质。对某个开发人员而言,其工作范围或其负责的内容之内外,比较清晰的划分一条边境。认知边境,边境之外的部分基本超出认知;认领边境,会明确区分一个研发任务是否和自己有关,或应该由自己负责,并尝试拒绝认领边境外的任务;外交边境,和人交流沟通时,会不由自主的站在对自己境内有利的立场,并且往往沟通过程中本应该通过约定协商的部分,也会不由自主的跳过约定过程,直接按利于自己境内处理。边境效应尤其多发于有明确kpi或问责机制的ESP模式的团队中。
其更深层次的危害是容易造成甩锅和推诿的盛行,研发团队越是混乱,制度越是不合理就越容易招致这种后果。进而影响整个团队的氛围,降低整个团队的san值,这不是危言耸听,而是实事求是。毕竟谁都希望有个比较欢乐融洽(即使可能还是很忙)的队伍里,通常人的能动性和环境是有紧密关联的。
业务概念混乱
业务概念混乱通常在整体业务非常复杂、总体业务概念非常多且团队没有意识强调每一个业务概念的含义的情况下出现。它有两种表现形式,其一是业务概念弱边界,即不同业务中名称相同或类似的概念未做区分;另一种是业务概念的断链,即业务A中的概念a在业务B中有对应的概念b,但常规思路无法将它们联系到一起。业务概念混乱通常不会对结果造成什么直接的影响,因为在使用过程中通常可以对它们进行区分或纠正,但依然不提倡放任这种现象,因为这会使使用过程过于曲折。
笔者亲历的两个例子,一个是弱边界的,某单据系统把下发至此系统的订单统称为订单,其下游wms系统和更下游的配送系统也是这么个路数,于是分属单据系统和wms的两位同事坐在一起确认接口,讨论的都是订单,字段属性也基本都能对上(比如单据系统里有单号,wms里也有单号,两种订单的字段特别相似),但是各个字段怎么取值怎么聊也聊不到一起去,二人各自上火。直到对到配送信息时,发现对它的叫法各不相同,才想起确认一下二人说的订单概念是不是一回事,之后的过程才顺利起来。
另一个是断链的,某系统A有一个业务概念叫做合作伙伴,另一个概念叫做客户,有另外一个合同系统,里面有个概念叫做客户。但因为系统A中客户概念的存在,导致相当一批刚刚接触系统A的同学认为合同系统中的客户就是系统A的客户,但实际上签合同时的客户,因为签了合同所以称为了系统A的合作伙伴(因为一个概念,导致一波刚入职的萌新一周的工作量白费)。
需要声明的是,业务概念混乱是需求设计和程序设计时不可避免会出现的情况,我们不需要竭尽全力规避它,只需要在进行可能涉及到这个问题的交流之前提前进行各自概念的确认就能有效的无视其危害。
连坐触发
连坐触发可以说是阻止研发团队下决心打破GHD的最强大也是最后的守门员。连坐触发的意思是,当你试图优化GHD下的项目时,即时只是针对一类小问题,也有可能遇到各种不利于优化的场景。包括但不限于,试图优化一类小问题时:
- 因为某个地方实现方式的特异性,导致当前比较通用的优化方案行不通或无法完全解决问题。比如本来加个切面就能优化某问题,结果某处实现的对象是自己new出来的。
- 导致其他地方或自身出现新的问题。这往往是因为,准备优化的位置,就是以前需求迭代时打的补丁。当时做需求迭代时,应该由当时的实现方式,进化为另一种实现方式,而研发人员偷懒,仅仅打了个修补作用的补丁来达到了现象上正确的输入输出。比如,某接口从简单的处理一个业务场景扩展到处理两个业务场景甚至更多,良好的方式是开发一组handler或多个平行接口来应对这一迭代,但实际的实现是直接通过if硬开逻辑分支实现。此时优化这一问题,即时业务逻辑老调整没有技术难度,但同时还需要调整所有调用和处理接口返回的地方,但这些地方可能很多或者不容易进行优化。
- 出现对当前场景进行了优化,但出现了新的需要优化的地方。这是因为现在优化的位置是用于处理某问题的补丁,对补丁进行了优化后,原本的问题暴露出来了。更深层的问题是,谁也不能保证现在暴露出来的“原本的问题”是不是处理“另一个原本的问题”时打的补丁造成的
- 导致协作的研发人员出现问题。比如调整某功能,其上下游系统对应研发同学需要做较大调整,甚至前端同学需要调整。这种场景通常是对外接口设计不合理导致的。
- 造成一系列无法确认的场景。比如,(可能很多团队都进行过的)屏蔽调代码中所有魔法数。很有可能进行优化时,当时写这些魔法数的同学已经不再了,并且你完全不知道你现在看到的这个魔法数是啥意思。于是,笔者曾经见过这样的常量:public static final String STR_ONE="1"。就是因为团队希望替换业务代码里的魔法数,而其中的一些不好区分业务含义因此相关研发直接偷懒了(为什么说偷懒,因为常量应该是按业务含义和场景进行区分,而不是值),而这个替换其实又造成了一个新的坑:原本这些"1"作为魔法数是互相独立的,但是此时,它们成为了共同体,假如业务调整,其中一些1改成了枚举,我们依然不能通过直接修改常量实现(这里其实有另一个问题,关于目的和手段的区分),而干掉魔法数使用常量的其中一个目的其实就是这个
§3 总结
此文中,我们介绍了所谓的蛋式编程(ESP)是什么,它的最终危害和核心是什么(GHD),构成GHD的成分是什么以及它棘手在什么地方。并以ESP和GHD为核心,向上,我们介绍了ESP是怎么演化过来的(虽然离严谨的标准相去甚远,但也算尽力了);向下,我们梳理了ESP各方面判断的依据和特性(DEBUFF)。
到此,对于ESP是什么,从何而来,会导致什么,有什么危害的阐述告一段落。各位看官老爷可以回忆一下自己经历过的和自己"经理"过的项目,是不是发现其中全部或者绝大多数都属于ESP,甚至其中不乏已经出现GHD的。我期待您的反应是"是!",然后马上就会浮现出"但是没有什么方式规避啊","但是当时的场景只能那样啊","所有的项目基本都这样"的辩驳念头。
可以肯定的是ESP是非常不好避免和克服的,尤其是已经出现了ESP的团队。此文的下篇会介绍具有可能有效的预防ESP的方式,以及可能有效的一部分ESP场景下的破局思路(因为众多原因和阅历限制,笔者只能说:可能有效)。