在这个系列里面,我会谈到前端架构的进化;它们解决了什么样的问题以及又是如何面临新的无法解决的问题的;最后这些架构背后常见的组件和模式。

我知道你们都太熟悉 Flux,Redux 和 Vuex 了,所以我不会对它们着墨太多甚至说刻意避免它们。相反,我会谈论到你们不熟悉和没有听说过的 Backbone.js,Mobx,NgRx 和 Akita 等等。我不会深入这些框架的使用细节,而是在必要时介绍它们框架内的概念和设计思路。最后你会发现其实所有框架背后其实都在用同一种方案解决问题,你也有能力创建自己的框架了。


作为系列的第一篇,在涉及到真正的技术内幕之前我们需要达成一些共识。这些共识是我们之后谈论所有技术方案的基础,关于什么是好的,如何在选项间抉择,关于框架演化的方向在哪。

关于这些原则你不必每一条都认同,你可以反对它们,也可以有讨论的空间。但你需要了解的是这些原则和共识决定了之后系列的文章中我偏好的一些内容。

满足非功能需求

框架是为了解决非功能性需求(Non-functional requirement),这一点非常重要,这是一切的前提。在我们公司内部更倾向称之为跨功能需求(Cross Functional Requirement)

所谓非功能需求就是我们老生常谈的可拓展性,可维护性,可测试性等等。之所以称它们是非功能性的是因为它们和我们的业务需求没有任何关系。例如产品经理要求你实现一个留言板功能,即使你在单个文件里使用了拥有 1) 一千行代码和 2) 20个形式参数以及 3) 数十种依赖注入的函数去实现这个功能他也并不在乎。因为他只关心功能能否上线,至于这部分代码的将来维护的成本有多高与他无关。

但是与我们有关,框架和模式的魔力恰恰是能够帮助我们在将来的开发中减少项目的维护成本。这里的成本不仅涵盖新功能的增加,旧功能的迭代,开发过程中的调试、部署,还能够让新加入团队的成员更快的上手融入团队。

之所以把这一条原则放在首要位置,是因为我知道你们中的大多数并不考虑非功能性需求。如果在之后的内容中我每列举出一种技术方案时,你们都在心里默默的念:

  • “为什么要这么麻烦,把功能实现不就得了”,
  • “反正两年后我也不在这个公司了,谁爱维护谁维护,快速上线要紧”。
  • “大不了换一波人重构”

那我们其实就聊不太下去了是不是。

实现功能需求不难,如果你已经是稍有几年的工作经验的话,想象一下如果现在让你和一个实习生去实现同一个功能,最后你和他最后的工作成果差别在哪?我相信单单从用户角度上看不大,因为你们都是根据同一份需求文档,同一份界面设计稿,同一份交互方案实现的。真正的区别在于程序的内部你是如何更优雅,更高效和更具前瞻性的解决这个问题的,这些都是非功能需求体现的地方

如何培养的这样的思维模式:想象你的项目要维护十年之久。在这十年里技术栈可能会发生天翻地覆的变化,可能 React 已经不再是最适合的表现层框架了,但是你的业务逻辑依然有效。如何保证业务逻辑与表现层的分离,如何在更换 React 的前提下不触碰核心业务逻辑的修改,这一系列文章也许会能给你答案。

“Falling Into The Pit of Success”

这个标题来自 codinghorror 博客上的一篇文章,标题就叫做 “Falling Into The Pit of Success”。这个网站的文章曾经集结成书出版,中文版译名为《高效能程序员的修炼》

这个原则翻译起来很奇怪:“掉进成功的陷阱”。但是如果你阅读完原标题的那篇文章之后,你会明白他想表达其实是:好的系统应该让开发变得容易,使得程序员很容易就能做正确的事情。

举个非常简单的例子,在早些年还在用 jQuery 或者是 Backbone 进行开发时,如果你本地想搭建一个应用的开发环境非常简单:1) 去官网下载 js 类库; 2) 创建 html 文件; 3) 在 html 中引用 js 类库。你甚至都不需要在本地启动 localhost 环境就能在本地使用 notepad 进行开发了

而现在你想要搭建一个 React 环境你需要

  • 保证 nodejs 和 npm 版本的正确性
  • 通过 npm 或者 yarn 安装 React 相关类库(这一步你可能会遇到你需要安装的包和 nodejs 不兼容的问题)
  • 编写 webpack 配置
  • 安装 webpack 所需的各种插件
  • 调试(虽然这一步骤看起来有些无厘头,但是我不相信各位能够一步到位的把前几步走通的,所有额外的工作的算在这一步骤当中)

没有人喜欢写 webpack 配置,光是这一步骤就会给人无限的挫败感。我不是在指责 Nodejs 或者 Webpack,它们的存在有它们的合理性,Webpack 配置文件里没有一行代码是多余的,它们帮助我们解决了开发中的很多问题。只是就“配置本地开发环境”这件事情而言,当下需要做的工作绝不算容易,也很难把这件事情做好。

为什么“容易把事情做对”对我们的项目非常重要:因为维护项目从来不是一个人而是一个团队的事情。

在一个团队中可能因为水平、经验等各种各样的原因导致不同人对框架理解不同,这种不同反馈在代码中就是大家都变得在用自己的方式,与众不同的方式做同一件事。但另一方面“程序”这种工业级的产品,我们要求的是稳定的输出,长期的可维护性。于是让每个人自己去思考问题,去实现一套自己的解决方案,无论对于效率还是质量而言都是有风险。

code review 的功能之一也是在消除这种变异性。一个好的团队的项目的代码库风格看上去因该是一致的,而不是迥异的。

在这里我想批评一下 Redux 框架,我不认为它式一个好的框架,问题不是出在技术方面,而是因为想让一个团队维护好一个 Redux 项目很难。假设 Redux 应用最好的状态是 10 分的话,那么代码层面的因素只能让你达到 5 分,另外的 5 分需要你学习所有代码以外的文档种的设计规范模式来达成。这对个人不是问题,但是想让团队种的每一个人都把这些吃透并处于同一水平太难,这就是难以轻易的做对的事情。

温伯格的咨询第二定律:不管一开始看起来什么样,它永远是人的问题。我们需要类似于 Typescript 或者 tslint 这类东西尽可能的移除 “人” 这样的干扰因素,通过硬技术手段保证项目的稳定产出。

最后话说回来,技术上的约束无论如何还是有限,就像你永远无法叫醒一个装睡的人一样,你也永远无法阻止一个想破坏项目的人,可能他不是有意的,而是无知造成的。最终找到优秀的人比一切都重要,找到他们,或者培养出他们。

活在当下

当我们希望前瞻性的解决问题时,我们究竟应该看多远?

我认可同时也是我学习到的建议是:1) 不要尝试去预测未来;2) 让程序足够灵活能够应对未来的变化即可

假设我们现在需要实现一个考试系统,需要设计试卷中“问题”与“答案”的数据库表。当前需求非常明确的一点是一个问题有且只有一个答案,且问题间不可以共享答案。于是想当然的我们可以设计一个 QuestionAnswerSet 表,将“问题”和他对应的“答案”都放在一张表中,存放在同一行中:

id Question Answer
1 The First Question The First Answer

但现在有人提出异议了:为了将来能更好的兼容一对多与多对多关系,不如提前将 Question 字段和 Answer 字段拆分为两张表,并且再用一张关系表将它们关联在一起。

于是吊诡的事情产生了:明明一张表能够解决的问题,现在我们要用三张表去解决。并且我们也不确定这三张表将来能不能用上。

你依然会反问:万一用上了呢?

那么我回答你:如果将来真的需要产生多对多的关联的,那么我们再做数据库表结构的更改和迁移也不迟。基于当前的设计做表的拆分并不是一件困难的事情。这就叫做“我们目前的设计有能力应对未来的变化”。而我们设计系统的目标就是让系统拥有足够的灵活性来应对变化。这种架构你也可以称之为演进式架构或者是持续性架构。在《演进式架构》一书中详细叙述了用来实施这样的架构的技巧,值得一读

“活在当下”有一个相近的说法你一定听说过——“过度设计”(over engineered)

“过度设计”的问题在于你认为你预测到了未来,但其实你并没有。你只是在你的视野范围里一厢情愿的相信某件事情可能会发生,但还有千万种可能你没有看到。但现阶段的代码和精力其实无法涵盖这所有的可能性。唯一不变的就是变化本身,你只需要让你的程序有能力应对未来的各种可能性即可

“过度设计”还会增加系统的复杂性(“简单和清晰”也是下面我们会谈到的另一个原则),给项目的维护带来困恼。想象一下另一个维护这个数据库的同学看到三张表的关系设计,他会下意识的去猜测问题和答案间属于多对多的关系,但很遗憾他永远也不会在代码里找到这样的逻辑。

简洁和清晰

程序员天生有一种把简单问题搞复杂的能力:

  • “单用 React 解决问题怎么能体现出我的水平? Redux 全家桶走起”
  • “我们最好能做一个工具或者平台来解决这个问题,然后把平台推广到整个公司”
  • “最近 X 技术很火?最好能在项目里用一用,将来好写在我简历上”

“简历驱动开发”和“造轮子晋升”我都非常理解。但我依然想说,如果可以的话,还是请站在项目和团队的角度上考虑如何满足项目的非功能需求:我们应该尽可能的降低项目维护和学习的门槛,降低它们的风险,而不是反向提高它们。退而求其次的,至少应该把复杂性和风险控制在一定的范围内。

如果你没法保证你的程序满足 SOLID 原则,没有套用各种模式和最佳实践,那么请至少保证系统的简洁和清晰。这依然可以提高代码的可维护性。例如把你的 React 单个组件保持在 300 行以内。

越简洁的代码维护起来就越轻松这一点是毋庸置疑的,你是在一个一千行的面向过程有三层循环的函数内调试 bug 容易还是在一个同样一千行但是拆分为五个每个文件不超过200行的组建代码内调试 bug 容易?这里的简洁是全方位的,小到变量的命名,函数的封装;大到框架的复杂程度,学习曲线。

简洁也并不可耻。如果你读过老马的《企业应用架构模式》(Patterns of Enterprise Application Architecture)的话,你会发现极其简单的 transaction scripts 模式也未尝不可。

你可能会喜欢

我做了一款发现播客的工具

我发现播客越来越成为我探索这个世界的重要渠道。为作为一名重度播客用户,自然希望听到更多不同的声音来帮助我理解周围正在发生的事情。于是我做了这个工具“播客广场”: [https://www.pcspy.net](https://www.pcspy.net/) 本文首发于[少数...… Continue reading

学习 Tensorflow 的困境与解药

发布于 2024年03月31日