“前端”架构真的有必要存在吗?
声明
本篇文章是infoQ公众号发布的,我觉得说的很好,就摘录过来了。 如有侵权请联系我删除。
架构到底要不要分前端和后端?
多年来,前端一直被视为后端过程的附属品,可能因为最初的系统主要是围绕自动化流程、文件工作流或计算展开的。然而,如今有更多面向用户的应用程序,流畅的用户体验和美观的用户界面已成为独立的价值所在。单页应用(SPA)随 React、Angular、Vue 等框架的出现而兴起,改变了系统的格局。越来越多的责任从后端转移到了前端,前端应用不再是系统的“冰山一角”,而需要被标准化管理。这也就是为什么现在很多人会谈论“前端架构”。
但也有许多前端开发者将前端架构视为文件布局和工具选择。架构师、 Angular Devtools 贡献者 Tomasz Ducin 对此特地进行了反驳,他认为文件布局从来都不是架构,它只是实现最终目标的一种方式,并不重要到足以被视为架构。
目录结构无法体现诸如模块隔离、状态管理、依赖倒置等重要的架构概念,这些方面更关乎系统的实际架构质量和业务需求的支持能力。他认为我们需要构建对前端架构的正确理解:架构是通过沟通、分析和推理的结果,而非简单的工具选择。架构师的角色在于理解产品需求、业务目标、组织结构等,并据此形成高层次的设计决策。
在此背景下,Tomasz Ducin 与“架构周刊”深入探讨了设计前端架构的实际意义——以及它是否真的需要“前端”这个前缀。
我们是否有必要将前端和后端架构分开,还是说只需一个通用的架构团队即可?其中一个关键在于建立跨职能团队来弥合前后端之间的隔阂。Tomasz Ducin 认为,将前后端团队整合到一起,可以创造更流畅、更以用户为中心的工作流程。
前后端一体化的团队能够更快速地交付功能,提升沟通效率,进而降低“沟通成本”。这也使团队更好地契合业务目标和康威定律——系统结构会自然反映团队的沟通方式。
此外,尽管前后端需求不同,两个“阵营”可以相互借鉴。例如,前端开发人员可以从领域驱动设计等后端原则中获益,以更好地理解业务问题;而后端开发人员则可以学习前端高效的原型设计方法以及将应用程序构建为组件的思维方式。
前后端架构的边界:是否需要分离,还是一体化更优?
架构周刊:之所以会有这期节目,就是因为 Tomasz 应该是我能想到的最适合讨论前端架构议题的人选之一,至少在我接触和关注的圈子里是这样。所以今天我们将一同探讨前端架构还有总体架构,聊聊其中到底有哪些差异,以及前端和后端之间为什么会存在这样的差异。Tomasz,能不能向我们的观众介绍一下自己吗?
Tomasz Ducin: 没问题,很荣幸能跟你面对面交流。我先简单介绍一下,我主要从事金融、银行、实时数据分析等行业的大规模产品。没错,我曾经担任过开发人员、团队负责人、架构师等各种岗位。
大约在八年之前,我开始转型担任独立顾问。所以我现在的主要工作,基本上就是帮助其他团队或者产品解决跟性能、测试还有架构相关的障碍。现在有些人会考虑要不要投身于微前端,又疑虑于过渡之后可能遇到的问题。
其实完全没有必要,这也是我转向这个领域的原因。除此之外,我还参与跟前端和架构相关的培训工作。
架构周刊:咱们进入正题,你之前写了一篇关于前端架构定义的文章,里面还专门把“前端”用括号括了起来。我第一次读的时候,感觉你在文章里好像对前端这个概念有意见。真是这样吗?
Tomasz Ducin: 如果说我们这次对话是由那篇文章启发而来,那我的文章则是由另一场对话启发而来。我忘记到底是在培训课上还是在社交媒体的讨论里,但有个人确实提到,一切具有目录结构的东西,比如文件、文件目录之类,基本都属于前端架构。
当时我气得都上头了。大家都说,一定要把话在脑袋里转 3 遍再说出口。不开玩笑,当时我在脑袋里可能足足转了 50 遍。
最终,我还是决定出言回应,驳斥这种典型的错误观点。我承认,用纯抽象的方式理解架构这种概念确实比较困难,但我认为确实有太多的前端开发人员乃至前端工程师对于架构的理解还相当模糊。
按我的理解来讲,最大的两个问题就是人们过于关注具体工具,过于关注能够直接从代码、文件或者相应的结构当中看到的东西。
相反,架构的真正意义在于那些不太容易快速感受到的东西。所以我才对此提出了质疑——前端绝对不只是目录结构。别怀疑,我为观点准备了十几个相应的例子。
当我们做出架构的选择决策时,往往需要从多个不同角度来看待它。在考虑应用程序或者系统架构时,最重要的永远你需要哪些特性、设定哪些限制,还有引入了怎样的机会空间。
更具体地讲,最重要的不是工具,而是开发者选择的惯例或者结构。这些要素往往相互之间关联不强,视开发者需要的特性而定,甚至可能因为某些限制而根本无法实现。从长远来看,这才是最重要,也是最难以把握的。
架构周刊:是的,没错。我很喜欢这篇文章,但理解起来也着实有点困难。要说我对文章的理解,那就是它解释了什么是架构,强调真正关注的是决策过程,还有哪些决策环节是最关键的。确实,这个过程肯定很难把握。但好在你在文中列举了不少例子,比如要选择建立前端、要不要使用微前端等等,还有使用哪些工具,比如 Webpack Module Federation 或者其他一些人们普遍觉得在架构中不那么重要的要素。所以对我来说,最困难的部分就是真正理解我们为什么要划分出所谓前端架构?你对这个问题怎么看?为什么架构会分成前端和后端?
注:在文章中,Tomasz Ducin给出了以下架构定义:
架构的定义通常体现在核心设计决策上,而非具体的技术实现。例如,是否采用微前端架构(MicroFrontends, MFE)是关键,而使用何种模块联邦工具(如webpack module federation)则相对次要。同样,模块的可重用性与隔离性的优先级比是否实现barrel文件(如index.js/ts)更为重要。在数据模型管理方面,决定模型是否跨模块共享或通过访问控制(ACL)实现隔离是核心设计,而采用面向对象(OOP)还是函数式编程(FP)则影响较小。
此外,架构设计还取决于状态管理的集中或分布式特性,使用哪种状态管理工具(如redux)则是实现选择。同样,系统是基于PULL还是PUSH机制的决定更为关键,而选择promise、async/await或rxjs等异步处理方式属于实现细节。在需要实时数据的场景下,支持实时数据处理的能力非常重要,而具体使用Firebase、Supabase等后端服务则因需求而异。
客户端与服务器的API契约变更权限及核心架构驱动因素也是架构定义的重要部分,选择GraphQL、REST或SSE等协议则是实现方式上的选择。系统对关键渲染性能(如LCP)的优化需求和单租户或多租户架构的选择更为关键,而UI组件的代码行数以及认证数据的存储位置(如Redux、context或useState)则是次要考量。此外,如何为UI提供容错机制是系统设计的核心,而CI管道的代码覆盖率要求则是质量控制方面的细节。
Tomasz Ducin: 不同的人对这个问题肯定有不同的看法。我觉得架构选择主要涉及决策、权衡、影响、后果还有从长远来看难以改变的因素等等。
我们当然可以给出自己的定义,人们也可以对此有自己的看法。我个人的理解是,前端开发和后端开发在现实层面已经分成了两个群体,各自生活在自己的世界当中。而且这两个群体间太缺乏知识和经验交流了。
我还发现,至少在跟我共事过的群体里面,开发者不怎么爱看书,尤其是那些经典的编程书籍。书中就工程领域和行业应用讨论的内容都经过深思熟虑,辅以严谨的分析论证,哪怕在不同的平台上也完全能够适用。
举例来说,所有消息传递解决方案都有了明确的设计思路,直接从书里找答案就行。实际变化的基本就只有应用程序的规模,再加上云计算这一后来才出现的重要因素。虽然这些在 20 或者 40 年前还不存在,但其中的模式仍然是共通的。
但好笑但又有点可悲的是,我们在前端世界中总是在重新发明已经存在的模式,甚至可以说就是在重新发明轮子。
这种情况之所以长期存在,就因为前端首先是以界面的形式呈现,所以不同岗位的开发者在工作时根本不会考虑持久性的问题,或者在绝大多数情况下,也由不得他们考虑持久性。我倒认为在软件开发领域,最好不要采用边界清晰的领域逻辑。
虽然有些情况适合做出区分,例如前端中的有界上下文,但聚合和实体之类内容则跟领域驱动设计(DDD)类似,根本没必要专门围绕前端做出区分。
前端确实有自己关注的焦点,比如用户交互,因此会有相应的模式和独特的问题。我承认某些模式在前端体现得更加突出,最典型的例子就是状态机,这肯定应当跟后端区分开来。
在客户端侧,状态管理可以说是开发者最应该考虑的优先事项之一。在这方面我们会采用最激进、效果最好的工具,也就是 Redux。这个例子也很典型,因为 Redux 在理念上相当激进。
举例来说,其中既没有事件、也没有命令,唯一存在的就是 action。我认为这是一种象征性的方式,代表着我们对于系统的思考方式存在差异。因为其中完全不涉及事件、命令、查询逻辑的区分,也不考虑系统之间要如何集成和相互通信。
唯一重要的就是 action。我们当然可以理性讨论 Redux 中的 action 到底算是事件还是命令,但你会发现很难它指向过去还是未来。唯一能够确定的是,每当我们分派一个 action,真正关心的就是它的执行结果如何。
从这个角度看,这肯定更像是命令。但在另一方面,一旦用户点击了某个元素,这时过去的状态又是确定的,那么前后变化就很明显的。界面由此发生的变化是确定存在的。
架构周刊:说起 Redux,我个人倾向于称之为命令统筹,因此它更像是特定的 action 或者命令集。无论怎么称呼,它都假设我们总会得到相同的结果。毕竟在理论上,就如你所说,其中不涉及存储。所以我们做的就是跑通状态机,并保证它永远给我们相同的结果。所以说,我觉得这是个能够确切表现相似点与差异点的有趣用例。我觉得对于很多专注于后端开发的朋友来说,这种状态管理、特别是多种不同的状态管理技术才是最让人难以理解、难以掌握的部分。
所以说回之前提到的前端跟后端之间的脱节,我觉得这也是最糟糕的情况之一,因为当前很多所谓最佳实践都是由后端开发者提出的,而不知何故他们压根就没考虑到前端开发的需求。
可能在他们看来,前端那边的事情并不重要吧。更极端地讲,有些后端开发者甚至宁愿没有前端,最好让整个系统保持静态,然后就万事大吉了。当然,我觉得这些想法至少在当下根本就不可能,所以没有任何意义。我觉得前端和后端这两个阵营可能相互学习,你同不同意?如果你也同意,那能不能举几个例子来说明如果双方能够紧密合作,那可以从对方身上学到哪些长处?
Tomasz Ducin: 我说的很有道理。所以在回答你的问题之前,我想先强调一点。至少在我自己的圈子里,我经常看到一些动图或者表情包,讲的就是前端开发比后端更复杂、更困难,又或者后端开发比前端更难。
但我觉得身为软件工程师,我们一定要有基本的问题解决习惯——就是先尝试理解问题,再努力找到正确的解决方案。前端和后端架构之间的区别,还有我们考虑的所有其他问题,都应该遵循这样的处理思路。
问题落回到前端和后端上,我们其实面对的是相同的物理规律。我们需要理解基本相同的开发原则,不仅仅是那些最最基本的条件,还应当考虑这种前、后端互通的利弊是什么。
比如说共享数据、共享模型、共享基础设施、共享一切。在这些方面,前后端的诉求基本是完全一样的。我们只需要将它应用到我们正在开发的代码、产品或者系统当中。这似乎并不困难。
就个人经历,我发现对于富有经验的前端开发者,他们非常清楚自己需要学习多少东西。对于这样的优秀人才,他们往往对自己不了解或者经验不足的问题缺少足够的敬畏……因为他们自信满满,总觉得自己能够克服一切难题。
这就是所谓“无知者无畏”。反过来也是一样,对于经验丰富的后端开发者,我们可能根本不清楚前端开发者需要面对哪些复杂性。
所以我们会自然而然地认为那边的工作比较轻松,因为我这的现实问题很难、需要为其付出很多努力。所以一旦出了问题,那肯定是对面能力不足或者脑袋不清楚。
人们之所以坚信后端比前端更难,或者相反,就是因为他们在其中一方面很有经验、清楚自己在专业方面还有多少不知道的东西,但对另一派的难处则理解不深。这也是我个人非常关注的领域之一。那么,前端和后端开发者能不能从彼此的领域里学到些东西呢?当然可以,而且相当不少……我说上几个小时估计也说不完。
但总的来讲,只要认真想想有哪些架构样式,它们各自有着怎样的优缺点,再深入思考它们要解决哪些问题、又会引入哪些新的问题,就会意识到前端绝不像很多人想象中那么简单。通过这种形式的孽情,我们就能体会到另一派的模式、风格、范式和思维方式。
对于前端开发者也是一样,孽情思维能帮你获得全新的思维方式,在引入任何特性前都认真考虑需要为此付出的代价——这在常规前端开发中并不常见。正如你所说,每周都会有全新的 JavaScript 框架出现。
虽然变化速度比十年之前已经好了很多,但新工具仍然不断涌现。所以我想说的是,前端的一大挑战就是我们更倾向于对项目进行微优化,倾向于考虑 API 本身。
比如说,你在 API 层次上使用的抽象是什么?或者说得更具体一点……你打算怎么使用自己选择的工具?具体形式如何?工作流程是怎样的?你打算构建怎样的结构?当然,我这里说的结构绝不是代码和文件之类。
我指的是各模块之间的关系,还有开发产品的各团队之间的关系。例如,最经典的一点在于,当初我做咨询的时候,就接触过单体式还有微服务形式的各种系统。但这并不重要。
真正重要的,是我们能清楚地看到有多个团队在高度模块化的代码库上工作,而且每支团队都对于一个具有不同优先级的特定产品所有者。这在本质上就是一种独立的有界上下文。从这个角度理解,前端开发者在网站上建立的就是一套由功能实体作为分隔的目录。
以电子商务为例,我们的每款产品都有相应的完整模块。这种设计被称为模块化。可以想见,前端开发者确实能从后端开发者那里学到很多东西,不仅仅是模式或者架构样式,甚至是整个思维方式。
就像之前提到的,这不一定非得是在战术和战略上做出明确区分的领域驱动设计,毕竟有些设计在剥离之后就没有意义了。但至少我们要能够理解对方的表达以及特定语言在系统中特定领域的语义。
以此为基础,我们就可以尝试从完全不同的角度思考。例如,大多数高级前端工程师都知道,god class 永远是最大的敌人,但他们还是会根据实体来划分自己的模块。这明显是言行不一致了。
架构周刊:根据我的经验,在我们的团队中,很难说服前端开发者去考虑后端开发方面的问题,或者说业务领域的问题。对他们来说,自己手头构建的组件就是工作的全部,但一定得想办法让他们关心这个组件背后的逻辑,而不能仅仅根据直观看到的情况做判断。
所以光是知道选择了哪种方案或者工具还不够,前端开发者要能够理解这背后的商业意图是什么。当然,在后端方面,也建议大家多多关注当前技术细节。类似的讨论还可以有很多,包括领域驱动设计以及如何将领域部署到代码当中。
好的一面是,我个人在 Angular 团队工作过几年,Angular 的新版本已经吸纳了很多经验。我非常讨厌旧版本,新版本已经好多了,我也在工作中学到了不少。我意识到在 Angular 当中,任何组件都摆脱不了相应的样式或者说风格。
但如果能进一步探究组件内部,就能把一切串连起来。所以才会有单元测试、有视力,有 JavaScript 或者 TypeScript 支持代码。在花了一段时间研究 Angular 时,我已经拥有比较丰富的后端开发经验了,所以我才能意识到这其实是种非常符合人体工学的设计方法。
因为我会把所有变更的东西都放在同一个地方。所以我觉得这也是前端和后端团队实现良好协同的绝佳案例。而且我很同意你的说法,我觉得前端开发者在战术层面其实更加擅长。
比如说,有些实践是纯功能性的。如果从功能入手咨询他们的意见,他们完全可以对各种奇怪的样式命名做出全面的解释,特别是那些经常使用 React 的开发者。但也必须承认,很多问题确实找不到合适的关联路径。
这倒不是说开发者自己的水平不够,而是后端团队总是以某些特定方式收集信息,所以自然会形成相应的思维定式。总是关注自己需要关注的事,自然难以理解其他人怎么想。
Tomasz Ducin: 在拥有了足够很丰富的项目参与经验之后,就会发现无论选择哪种框架,包括现代框架或者各种主流框架,基本采用的都是相同的方法,所说的组件、前端可视化组件或者前端组件之类也都是一回事。
而后端组件则是完全不同的东西。很遗憾,因为在前、后端开发者口中这些都是“组件”,所以沟通难度会直线上升。总之,前端组件之间看起来更加相似,因为它们有自己的可视化层、有自己的状态、有自己的行为、也有自己的测试。
而这一切,我都想不到后端有哪些等价的东西可以与之对应。就像你之前提到的,前端开发者在战术方面更加娴熟,所以哪怕面对的是一个包含成千上万个组件的项目,那么其中一般也只有 50 个真正重要、与领域无关且高度重复使用的组件。
其余的组件则基本上都是特定、甚至一次性使用的。在这种情况下,我们可以用一种非常高效的方式将其归类为“即抛”型资产。
这些开发工作在几分钟之内就能完成。比如说我们可以快速搞定与业务、营销、客户等进行沟通的渠道,而这些才是我们自己开发和拥有的组件。对于任何后续出现的需求,我们也都可以自下而上快速开发相应的组件。
这当然很酷,因为我们可以自主处理组件、设计各种测试等等。所有工作都尽在掌握,还可以整理一份始终有效的说明文档。我们甚至能够使用 Storybook 设计集成测试。
这种自由度至少在测试方面真的非常非常好。
架构周刊:抱歉打断一句,后端开发者应该也有类似的体会,也就是原型设计。后端原型设计跟前端比较类似,哪怕是像购物车这样体量不大的功能,也可以跟其他功能对接并在各类场景下重复使用。
Tomasz Ducin: 是像服务或者模型那种可复用的资产吗?
架构周刊:我不知道,或者说很遗憾,这种作法在后端开发中没那么常见。肯定有那么一段时间,比较流行基于模型的开发形式,也就是基于模型设计。理论上我们可以自由选择流行的原型,比如预订功能等等。我们可以在此基础之上构建自己的功能,比如说预订功能。或者说直接使用现成的预订样式或者原型,让它的设计走向更容易掌握。
Tomasz Ducin: 但我必须得承认,整个过程还是没办法像前端开发那么直观和明确。相较于前端那种着眼于极高级别的领域原型或者复杂性,后端开发面对的往往都是非常底层的东西,所以如果非要对应起来,那我觉得可能是实体、值对象或者聚合。
我们可以通过特定的方式来记录如何使用它们,比如有新人刚刚加入项目,我们希望新人能在第一天就高效参与工作。这时候,肯定就先得让他们看看项目中的现有组件都有哪些。
顺带说一句,你是怎样对 Storybook 进行分组的?根据不同业务规模和所处领域的复杂性,肯定会存在一系列与业务相关的模块。所以其中可能会体现康威定律,即组织运作方式必然体现为产品的开发方式。
比如说其中有多少技术含量、跟多少个领域相关等等。但 咱们回到之前的问题,后端开发者能从前端开发这边学到什么?就个人来讲,概括起来的答案就是怎么以速度更快、成本更低的方式完成工作。
但注意,不能以松散凌乱的方式完成,也不能搞得太过激。比如说,java.net 还有很多其他平台的构建方式决定,当我们需要服务之间相互通信时,就必须采取分布式设计。
这就要求我们建立序列化数据传输对象(DTO),之后还需要对其进行反序列化。如果有一套完整的中继对象树结构,且各个对象之间彼此相关,则需要一整套抽象流程等等。
这就是有时候我们不得不选择的操作方式,毕竟我们总是需要把代码部署在特定的场景下。而如果不采取上面这样的软件设计方法,不提前考虑逻辑的实现方式,那几乎肯定会搞出各种各样的泄漏问题。
正因为如此,我们才会总结出那么多经典设计模式。前端开发者总爱嘲笑这些模式,但它们确实非常有效。而前端之所以能够“幸免于难”,是因为他们有浏览器——浏览器能够非常有效地解析 JSON,所以只要提供相应的 TypeScript 接口就行了。
虽然还有服务器值不值得信任的问题,但前端开发确实不需要任何的运行时类型检查。哪怕不能 100% 信任,我们也可以先把这事放在一边,稍后再引入运行时类型检查。
总之,我们真正需要的就是从 Swagger、Pact 或者其他解决方案等契约中自动生成接口。就这么简单,就这么直观。前端开发者可以快速制作原型、快速实现功能,使用基于 Storybook 或者其他同类方案实现自下而上设计,并快速交付最终产品。
当然,我们不可能完全照搬经典设计模式中的一切。但就经验来看,哪怕对模式的具体认识有所偏差,但从长远来看,直觉还是会引导我们走向完全相同的解决思路。
举例来说,在后端我们可以设置一个使用多个类来实现的接口,这些类名称中涉及相应的 factory 或者策略。而在前端,我们需要编写的就只有函数。
虽然这里也涉及策略或者 factory,但我们不会这么称呼它,因为其本质上仍然是一个函数。我们只关注抽象设计,而并不关心具体接口。
另外,如果我们只有一个 TypeScript 函数签名,我指的是类型、而非实现,而且知道同一个类型可能有多个函数实现,那这本质上就是一种设计模式。无论我知不知道其名称,它的作用一般都是消除大量不执行任何操作的代码。
可能我有点吹毛求疵了,但我在后端站点上见过很大体量夸张的代码库,其中只有单一接口和单一实现。它们的名称基本是一一对应的,这颇有点为了面向对象而面向对象的意味。
或者是在.NET 当中,比如产品实现的 iProduct。为什么要保留这样的东西?我当然知道,这是为了遵循依赖倒置原则。但既然我们总要使用唯一的实现,而且严格来说要遵循的是依赖倒置,那提取接口的意义又在哪里?
我的意思是,这其实有点跑偏了。总之在前端开发中,我们对于最佳实践的关注最好不要过度,这样能够大大减少死抽象和僵尸抽象的数量。
架构周刊:没错,没错。所以我也同意,非要揪着前端的命名方式不放是种无知的表现。这里我无意冒犯任何人,但这确实是种无知。我们真正需要关注的应该是它在做什么,而不是它为什么被这样命名。这样我们才能更多关注结果,而不是纠结于特定模式。当然,多了解理论也是好的,总之不要偏废。这也是我最近几年越来越倾向使用 TypeScript 等语言的原因,这让我感觉自己开发效率更高了。当然,这是个非常个人而且主观的判断,这里就不过多赘述了。
那让我们回到最初关于重要决策和所谓前端架构的讨论。我们已经确定常规架构和前端架构之间确实存在一些差异,虽然没有确切数字,但我猜大多数读者和观众在听到“架构”时,首先想到的都是后端架构。
_所以你能不能论证一下,在审查或者评估现有项目时,各种前端架构决策之间到底有哪些显著不同?您认为在其中起到决定性作用的因素是什么,特别是关于项目及其架构的决定性因素?_
Tomasz Ducin: 这个问题我得好好想想。这肯定取决于我们从哪个角度看待问题,比如说我们要如何做项目部署、有多少个团队、总共有多少开发者参与其中、是否有必要采用微前端等等。
还有部署的频率如何等等。比如说你的业务动态程度很高,需要跟最终用户合作,而最终用户可能是任何人,那我们就得更多关注基础设施层面的交付和回滚能力。
从这个角度来看,我觉得我们首先需要考虑所有跟可观察性相关的因素。这也是前端开发者经常忽略的问题,也就是完整的 DevOps 文化。没错,我认识的很多前端开发者认为 DevOps 就是 Kubernetes,或者是其他一些他们不熟悉的 Docker 之类。
实际情况当然不是这样。从更合乎逻辑的角度来看,DevOps 关注的是如何拆分模块。如果说你的团队只在冲刺结束后才整体部署,或者说很多基本特征都保持不变,那怎么选择都不打紧。
在这种情况下,大家甚至可以根据实体对业务逻辑进行划分。当然,随着开发团队复杂性或者规模的增加,这样的方案也将不起作用。
但只要项目一共只有四、五个人,而且大家做的都是同一件事,那么哪怕几年之后具体人员有所变动,这样的方案也仍然有效。所以我要再次强调,系统的规模究竟多大?
根据个人经验,后端开发者的数量通常要比前端开发者更多,而且需要在持久性、一致性、事务、后端内部通信等方面承担更多的责任。所以并不是说前端开发更简单,而是有很多主题并不需要劳烦前端开发者去考虑。
所以说,后端开发者肩上的担子要更重一些。但如果前端应用程序的挑战性比常规情况更高,那前、后端团队之间的沟通就非常重要了。比如说你需要分享哪些模型、分享怎样的状态,又该根据当前状态提出怎样的问题?
更进一步讲,如果需要拆分某些要素,又该如何操作?假设我们在前端使用的大多是规范性的模型,基本上整个应用程序都共享同一个实体接口,那我们不妨考虑这样一个简单问题。
如果系统中又引入了另外一项功能或者特性,该如何处理?你会让接口变得越来越大吗?同样的,如果我们选择使用状态管理解决方案,那么这类方案大多不会强制要求任何架构特征,但像 Redux 这样的特殊案例除外。
具体来讲,Redux 会强制实施集中式的状态解决方案。我认为这也是 Redux 最大的缺点。如果我们采取的是集中式的开发方法,那么 Redux 就毫无瑕疵,种种优势将如星星般闪光。
但如果不是,我们也可以考虑打破规则,选择使用 Redux 的其他竞争性方案。因为这时候 Redux 的硬性假设不但不是优点,反而会成为额外的成本来源。这就是实事求是规划开发方式的意义所在。
或者举个例子,正如你所说,有人认为 Redux 之于前端也是一种因为技术力不足而勉强为之的统筹方案。但我更喜欢你提的“命令统筹”的说法,我觉得 action 更偏向于命令、而非事件。但没错,这在前端开发来讲并不是特别重要。
那你分享了多少东西?另外考虑到前端的可视化特性,在代码库的划分方面,你会在项目采用怎样的分类方法?比方说要复用什么,对哪些组件进行复用?
我也不确定后端开发者是什么心态,但前端这边大家对于可复用有点过份痴迷了。
前端总在想办法实现复用,只要有可能就得复用起来。比如发明了一种设计风格,包括应用程序的外观和使用感受,那就把它用在一切能用的地方。
这些东西跟模块完全无关,跟领域也无关,比如按钮、单选键、复选框等等。但换到后端这边,会有人想要在数据表、列表或者网格表上复用高级排序、高级过滤、高级分页之类的功能吗?
有经验的后端开发者都知道,这些功能越是参数化、可复用性越强,它就越不实用。因为它已经变得太抽象了。或者说,可能会有人创建多个更简单的定制版本。
总之新的问题又来了:可复用的边界在哪里?它到底要多灵活、多通用?这个问题其实很难找到确切的答案。前端的另一种常见现象,就是企业早晚希望把自己应用程序的外观和使用体验统一起来,因此需要引入组件库。
可复用组件库应该由谁来维护,工作流程又是怎样的?比如在有界上下文这个背景下,库就是供应商,领域模块则代表客户。
所以这不是随便指定某个人就能解决的,基本上只能建立新的团队对组件库进行维护,再由其他团队告诉他们该做什么。而这就在组织中形成了单点故障源。
而且哪怕不单独建立团队,责任的分配也是个大麻烦。比如组件库应该有多大,应该在多大程度上由领域驱动,其中又该包含多少参数等等。
总结来讲,应用程序的实际部署和监控是一回事,如何共享状态、共享模型又是另一回事。可复用组件、特别是组件库的使用方式非常重要。这是我能想到的三个关键因素,而且肯定还有其他考虑不够周全的部分。但根据个人经验,大多数问题都来自这三个方面。
架构周刊:让我来梳理一下,你的意思是跟前端相关的特定因素,比如 UI 组件、状态管理等客观存在。但总的来说,我们在前端和后端遇到的问题并没有那么大区别。
拿组件来说,负责维护组件的团队跟关注后端的核心或者平台团队也有很多相似之处。我甚至自己就经历过之前提到的组件库的情况。在当时那个项目里,我们决定在特定阶段内建立一支专门的组件维护团队,从头开始构建自己的组件库。当时我们就想得很清楚,这支团队在几个月内就会解散,至少绝对不会比项目周期延续得更长。而且从长远来看,这件事的优先级其实很低。遗憾的是我确实猜对了,到了特定阶段组件库团队已经跟不上节奏了,而且就是你提到的理由,比如团队间的沟通效率太低之类。
所以是的,说了这么多,人们居然还是坚信前端和后端不是一回事,这真的非常有趣。实际上,虽然二者在具体挑战上略有不同,但总体规模和关注的议题还是非常相似的。
Tomasz Ducin: 确实非常相似,甚至连相关的误解也很相似。比如有个著名的亚马逊 Prime 流媒体案例,他们从微服务架构转回了单体架构,当时就有云还是后端社区的人将微服务大加批判了一番。我们对这个问题也很有共鸣。
因为不少前端开发者一直说微前端纯粹是个错误,完全就是技术债。但这么说就是忽视了康威定律一商业运作的基本原理。很多人似乎忘了,我们开发者属于成本部分,只是手段、而不是项目存在的目的。
我们有价值,但我们并不是目的。我们的存在是为了解决某些问题,但无法决定组织是什么样子。我们自己就是另一种形式的微前端,所以批判微前端其实就是在批判我们自己。
先澄清一点,我自己既不是微前端的支持者、也不是反对者。但 在实际开发当中,我常常会提醒团队尽量别用,因为微前端跟微服务架构一样,也会带来某些难以回避的成本和问题。
所以如果好处不够明确——其实在大多数情况下好处都不明确,那我实在不鼓励团队朝着过于昂贵、过于复杂的方向探索。我觉得这才是合理的思维方式。
在我看来,微服务和微前端虽然存在差异,但从宏观角度来看定位又非常类似。总之,二者有很多共通之处。
架构周刊:没错,我也同意。而且出乎很多人的意料,到底是采用单体方式还是更加分布的方式(比如使用微前端或者微服务)其实跟技术没多大关系,更多体现的是康威定律,即如何组建团队而又不至于相互干扰。
Tomasz Ducin: 当然,也有人强调这是一种能够解决人的问题的技术性方法。我记得有句名言,我忘了是谁说的了,大意就是无论发生了什么,归根结底都是人的问题。
哦对,好像是 Gerald Weinberg,就是。那如果把情况推向极端,那一切都是人的问题。毕竟我们都是人,我们讨论的一切都跟人有关。所以围绕人建立的一切都抽象于人自身。软件开发造的可不是机械,而是对人类思维的抽象体现。
所以我承认,一切最终都可以归结于人的问题。但另一方面,如果我们忽视或者想要对抗康威定律,那又完全是另一个问题的。
我们可以把组织结构理解成一个星系,其中的星体又怎么能对抗引力呢?
所以说,我们可以认为技术债或者架构失误永远都会存在。想要反抗这一切属于一种基本的认知偏见,大家对于某种架构的好恶,就像是对于某种问题解决方式的好恶一样,其实没有什么太大的意义。
在思考抽象事物时,我总会试图从现实生活中找到恰当的类比。所以每当我们讨论模式,特别是架构模式时,我都会尝试从现实世界中寻找答案。比如关于系统架构,我喜欢思考建造建筑物的各种方式,这是种很自然的联想方式。
比如在波兰,我们曾经有种叫做“板楼”的建筑。在苏联时代,政府会以这种方式用非常低的成本建造起大量住宅楼,建设速度很快,但质量就一言难尽了。
现在回头来看,这一切也有其合理性。板楼既不美观、也不吸引人,同时质量一般,最大的亮点应该就是位置了。但那个时候,我们的国家非常贫穷、也没有更好的建造技术和资源供给。
我们没有足够的技术人才,也拿不出充裕的时间。那时候国内很多人根本没有住的地方,毕竟二战才刚刚结束。所以要想正确评判板楼的意义,也要权衡这些利弊因素。
虽然在决定建造板楼建筑群的时候,成品质量很差,但容纳能力却非常强大,可以快速让很多人居有定所。如果要权衡利弊,那就是用比较差的质量换来更快的速度和更低的建造成本。所以我觉得这是个非常好的主意。毕竟它有效解决了当时人们急需住宅的现实问题。
这个项目本身怎么样?糟糕,相当糟糕。但凡住过那种板楼的,都不会有太好的印象。但相信大家也明白了,我想强调的是它确实达成了目的,也表明架构的演进性质非常重要。
在特定时间点上,我们可能会面对某些优先事项。这就像是在产品开发的过程中,商业环境也可能发生巨大变化,导致架构的选择或者驱动因素发生很大变化。所以再次强调,如果大家不喜欢某种架构,并且认为微服务完全就是个笑话,我想说那只是因为你不太了解这个专业。这就是权衡的意义所在。
架构周刊:说得好,我完全同意,特别是在各种不同的变化和影响因素之下,不仅仅是前端和后端,还有其他一些关于技术的讨论。
比如 API 优先或者 GraphQL 那种面向前端的后端。对于所有这些问题,团队往往需要通过合同、技术以及如何简化工作流程的方式实现相互协作。说到这里,我感觉我们终于慢慢接近了最初讨论的原点。
让我好奇的是,好像很多人都一直理不清这个问题,往往会表达出非常强烈的分歧和争吵。在你看来,到底有没有划分前端和后端团队,还是说最好把它们整理成更多业务垂直团队。我知道这是个陷阱问题,但我也相信你肯定有自己的明确意见。
Tomasz Ducin: 好吧,根据我自己的经验观察,他们是把很多现实因素给抽象了出来。这一切都是以产品的构建为起点。也就是说,他们是先考虑解决方案,而不是从打算解决的问题起步。
这明显是错的。所以如果你要构建的产品是那种需要经常重新部署、尽快交付价值,而且需要将上市速度尽量提高的类型,那问题就是达成目标的最快方式应该是什么?
在这种情况下,如果这款产品、或者说产品中的某些部分和模块需要由后端和前端分别交付,而且大多数时候都是这样,那么即使前端和后端规模各有不同,也必须考虑到双方之间的沟通成本。
两个部门之间的距离越远,相应的沟通成本也就越高。
所以我们接下来的目标就是努力降低沟通成本。这时候的答案一般就是建立全栈团队,甚至可以考虑 DevOps 团队。虽然我自己并不是 DevOps 工程师,但在日常工作中基本会按照 DevOps 的理念工作,比如端到端负责。
比如每个人都要清楚我们的数据库架构采用怎样的前端和后端、匹配怎样的监控和回滚机制、要测量哪些 DORA 指标、多久发布一次、需要多长时间执行一次故障回滚、故障发生的频率等等。
总而言之,团队成员们关注的目标越是统一,工作起来就越是简单。当然,对于其他一些任务需求,可能相反的规划思路才更高效。
我自己就在观察中发现,出于技术原因,移动团队往往是独立存在的。但我觉得这更像是主流需求之外的特殊情况。
通行的基本原则应该是,根据所在企业、产品和系统的规模,如果希望实现某个目标,就把追求同一目标的人们汇聚起来,这基本上就组成了一支团队。
有人说这是在为他们赋能,我不太喜欢这种说法,这是为他们赋予达成目标所需要的信誉、工具和资质,更简单地说就是让他们能放手做自己的工作。
所以在我看来,把团队拆分开来其实是种特例,是游离于常态之外的情况,毕竟沟通成本永远越低越好。毕竟如果能把沟通控制在团队之内,那即使大家在做完全不同的事情,沟通一般也能顺畅推进。
所以如果能把关注同样目标的人们安排进同一支团队,我们就能大大降低沟通成本,而且效果相当显著。我想说,这是缩短产品上市时间的一种有效手段。
最后强调一次,一定要先看业务、再看组织,别跟康威定律作对。
架构周刊:没错,我的实践经验也差不多。我曾经在多个团队工作过,这些团队会以不同的方式工作,包括技术层面还有垂直层面。那些紧密协同的团队总是能更轻松、更快地交付高质量的产品。
可如果我们以技术为理由将团队拆散,那其实就是带偏了他们的关注重点。我觉得团队永远不应该只关注纯技术,而应该更多关注商业价值。另外对我来说,如果可以的话,保持团队统一会有很好的激励作用。这并不是我自己的感受,而是作为一个团队,如果能够从头到尾交付功能、看到自己的成果在其他人手中发挥作用,那绝对会非常激励人心。
Tomasz Ducin: 另外我还有两点想要补充。即使我们已经组建了团队,比如三位后端开发者和一位前端开发者,那么哪怕事实证明让精通 Java 或者.NET 的人去编写 TypeScript,实际效果比不上资深 TypeScript 开发者自己包办,从长远来看影响也并不大。
毕竟这个世界上根本就不存在极端优雅或者干净的代码。那种东西只存在于理想当中。顺便说一句,我讨厌所谓干净的代码。
因为干不干净纯粹是一种主观判断,根本不是能够量化评估的客观特征。所以即使开端开发者在处理简单后端任务时质量差一点、或者没那么稳定,其实也并不重要。毕竟我们最终需要的是团队的整体产出,这也是缩短产品上市时间的关键所在。
回到我们之前讨论的内容,如果我们将跨学科技术人员纳入同一支团队,那么大家就能彼此交流知识和经验,并且从不同角度看待某些似乎已经拥有共识的问题。团队肯定能从中受益匪浅。
举例来说,我们之前已经提到,前端开发者可以向后端开发者证明,他们长期遵循的实践其实还有更轻量化的解决方案。另一方面,后端开发者也可以向前端开发者证明,在不同前端模块之间共享模型未必是个好主意。
所以只要引入一个映射器,就可以防止模型泄露。据我观察,前端开发者通常不清楚这一点。总之,我坚持认为组建多学科团队能够让每位成员都从中获益。
架构周刊:是的,我觉得这很好地总结了我们的整个讨论过程,也回答了我们对于架构要不要分前端和后端的问题。再次感谢 Tomasz 的到来。