
为什么模式很重要:来自超高速增长创业公司的教训
我听过一种说法:“模式是公司临近倒闭时才会谈论的东西”。这或许没错,但这也是公司在达到 PMF 后会谈论的东西。
别自欺欺人了,你真的需要规模化吗?
如果你没有经历过超高速增长的初创公司,很容易将可组合性、可扩展性、安全性、可测试性、冗余性、CI/CD 等事物视为不必要的浪费时间。确实,大多数初创公司过早地考虑这些问题。在你达到 PMF 之前,所有这些东西都纯粹是浪费时间,会分散你宝贵的资源,使其无法解决真正重要的问题。同样真实的是,大多数已经达到 PMF 的初创公司开始考虑这些事情又太晚了。他们以被动的方式处理这些问题,并在此过程中阻碍了自身的增长潜力。
来自超高速增长前线的经验教训
我很幸运地参与了两家增长极快的初创公司:Snowball 和 Cline。在 Snowball,我们遵循了大多数初创公司的模式——只有当扩展性、性能和效率问题明显拖慢我们的速度时,我们才开始着手解决它们。我们早期 PMF 的迹象之一是,我们的社区建立并托管了自己的 Web 应用来访问我们的智能合约(由于家庭紧急情况,我在发布时未能完成)。这种程度的社区参与和奉献精神是成功的巨大预兆,但不幸的是,社区构建的应用并非为了可组合性和规模化。很快,一团乱麻的代码开始阻止我们像竞争对手一样快速地发布产品。我和我的联合创始人每天工作 10-12 小时,每周 7 天,只进行前端功能开发,但我们仍然落后于竞争对手,并流失了用户!有一天,我决定受够了,花了一周时间重构了 Web 应用,最终使我们的功能吞吐量提高了 100 倍以上。随后对我们智能合约部署系统的改进又将产出提高了约 50 倍。这立即扭转了公司的局面,使我们从彻底的流失螺旋上升到三个月的新用户和收入增长(发布后 6 个月最终达到了 780 万美元的 ARR)。
尽管取得了所有这些成功,我们的增长在第二次可扩展性故障后再次停滞。这一次,是由于技术依赖性问题。2021 年 8 月底,由于一位主要以太坊生态系统影响者在社交媒体上发布的热门帖子,Avalanche 区块链的用户量出现了巨大的增长。用户量在一夜之间翻了三倍。尽管我们的系统可以处理这些流量(勉强),但该领域所有的 dapp 都面临着同样的弱点:由 Avalanche 托管的公共 RPC 端点。几乎所有在 Avalanche 上的项目都同时停止响应,高价值用户的涌入像他们来时一样迅速消失了。我们 Snowball 就像该领域的其他人一样,立即着手构建自己的 RPC 节点,以防止此类故障再次发生。不幸的是,为时已晚。以太坊移植用户浪潮消退得和来时一样快。Snowball 和整个 Avalanche 生态系统的交易量再也没有恢复。
归根结底,由于我们对扩展性问题采取被动而非主动的态度,本可以成为一代人的公司变成了一个昙花一现的案例。
最大气动压力
快进几年,Cline 正在经历更高水平的采用。我们被全球最著名公司中的许多顶尖工程师所使用。许多 Snowball 老兵和我在一起,我们已经确定,一旦数字开始向上和向右增长,就该开始考虑规模化了——在它影响到你的底线之前。所以问题变成了,如何优雅地进行规模化?
答案和以前一样:模式。没有理由重新发明轮子。那些经受住时间考验和数十亿 TPS 压力的行之有效的方法是已知、常见的,并且唾手可得——如果你足够明智去利用它们。需要经验才能确切地知道何时该使用某种技术,但有些事物是其他事物的先决条件,你永远不应低估其中一些改变可能带来的耗时和繁琐。一旦你有了 PMF,就该立即开始实施基础模式了!
第一步:为改变做准备
你的代码库必须设计为适应改变。为了实现任何可扩展性模式,你首先必须能够对你的系统进行改变。这听起来容易,但设计一个适应改变的系统实际上相当反直觉,对于不熟悉的人来说可能具有挑战性。代码库适应改变的能力称为模块化(Modularity)。要使代码库模块化,它不能紧密耦合。换句话说:系统应该彼此之间的依赖性最小。这可以通过几种方法实现,包括以下内容。
解耦通信
大多数简单的应用程序在发送方和接收方之间进行直接(因此紧密耦合)通信。例如,在 OOP 中,对象 A 可能会调用对象 B 中的方法,使 A 依赖于 B。在解耦通信系统中,你可能使用观察者模式、事件总线,甚至发布/订阅系统,具体取决于通信的范围。在这样的系统中,对象 A 会发出一个事件,对象 B 会事先订阅该事件,并通过回调响应。在这个系统中,对象 A 和对象 B 都将依赖于通信系统,甚至可能彼此完全不知情。
这样的系统允许你快速添加可能也响应或发出事件的新对象,而无需引入复杂的依赖模式或冒着依赖循环的风险(许多语言严格禁止)。

端口与适配器
许多应用程序将其业务逻辑与特定于外部依赖项的逻辑混合在一起。这种脆弱的模式使得采用替代方案、引入回退或切换到竞争产品变得非常具有挑战性。例如,在 Cline,我们的目标是将特定的推理提供商或模型与系统的业务逻辑解耦,以便添加新的提供商和新模型成为一项琐碎的任务,而不是一项耗时且繁琐的过程,只能由最高级的工程师完成。实现这一目标的方法是通过端口与适配器。
端口是接口模式的一种特定实现,其中多个独特的实现(称为适配器)能够遵循该接口。在 Cline 的案例中,我们可以创建一个名为 InferenceProvider 的端口,以及满足该端口的众多适配器,包括 Cline、Anthropic、Vertex 和 Bedrock。在我们所有的业务逻辑中,我们只会引用 InferenceProvider 端口,这样业务逻辑就与这些外部依赖项松散耦合,并且我们可以集中化添加新提供商所需的代码足迹。也就是说,我们可能只需要向一个文件(即新适配器的定义)添加数百行代码,而不是触及数十个文件中的数千行代码。

依赖注入
许多应用程序在使用点直接实例化其依赖项。以 UserService 为例。该服务可能在构建时创建自己的数据库连接,嵌入配置细节并直接绑定到特定的实现。UserService 可能包含一行代码,如:this.pg = new PostgresConnection(config)。这种方法的缺点在编写自动化测试时立即可见。如果 UserService 创建自己的数据库连接,你无法在没有运行数据库的情况下测试该服务。此外,如果你后来想切换数据库提供商或连接参数,你需要修改每个创建数据库连接的类。
在 Cline,我们利用依赖注入来反转这种控制流。组件不是创建其依赖项,而是从外部提供(注入)这些依赖项。UserService 现在可能声明:constructor(pg: PostgresConnection)。你可以通过结合端口与适配器走得更远:constructor(db: DBConnection),并且完全不用关心该连接是如何配置的,甚至它实际连接到哪种类型的数据库。这种方法集中了配置,允许在不修改业务逻辑的情况下对数据库交互进行实质性更改,最重要的是,使测试变得更加简单。自动化测试不再需要真实的数据库,我们可以注入模拟(stubs)、伪造(fakes)或仿真器(emulators),它们模仿数据库行为而没有开销或复杂性。
这种模式使我们能够迁移部分数据库基础设施,实现适当的规模化测试,并推出连接池优化,而无需触及我们绝大多数的业务逻辑。组件声明它们需要什么,而无需关心这些需求如何得到满足,使整个系统更具适应性。

拐点:理论与现实相遇之时
当你的初创公司开始经历真正的增长时,会有一个关键的拐点,理论上的可扩展性问题转变为紧迫、 immediate 的问题。这里讨论的模式是基础性的构建模块,使你的系统能够在不需要彻底重写的情况下发展。
这些模式不是学术练习或过度工程,它们是经过实战考验的方法,一旦你找到了产品-市场契合度,就会变得至关重要。关键在于时机:太早了,你是在浪费宝贵的资源来解决不存在的问题;太晚了,你是在飞机已经起飞时试图更换发动机。
教训很清楚:为你今天拥有的东西构建,但要考虑到明天的规模进行设计。当你公司的增长曲线开始变陡时,你将已经具备支持这种增长所需的架构基础,而不是被其压垮。在高速增长的初创公司世界中,这种准备工作往往决定了是成为市场领导者还是成为又一个警示故事之间的区别。