可扩展架构取舍
引言:“架构”是前端开发中一直以来都缺少的。由于近几年Web应用日趋复杂,前端架构开始流行起来。成熟的工具使得开发人员可以针对要解决的问题设计出可扩展的架构。
构建可扩展的软件,可以从很多角度来思考软件架构。但是如果每个角度都去考虑,根本不可能做出想要的软件。这就是为什么需要从架构的角度对设计进行取舍:取我们最需要的,舍次要的。
本文选自《大型JavaScript应用最佳实践指南》。
确定不可变内容
在做出取舍之前有一点很重要:列出那些不能舍弃的需求——我们的设计的哪些方面对实现可扩展是至关重要的、不能改变的。比如,被渲染页面中的实体个数或者函数间接调用的最大深度就不能改变。虽然不可变的内容不会太多,但是它们确实存在。最好的办法是缩小这些内容的作用范围,减少它们的数量。如果有太多严格的设计原则不能被打破或改变以迎合需求,就不能更好地适应不断变化的可扩展性影响因素。
考虑到可扩展性影响因素的不可预测性,无法改变的设计原则是否还有意义?答案是肯定的,但只有在它们浮现出来,并且显而易见时才成立。所以这可能不是前期的原则,虽然我们经常要遵守至少一两个前期原则。这些原则可能是从早期代码重构或后期软件的成功中总结出来的。在任何情况下,这些不可改变的内容必须明确下来,并和所有相关人员达成一致。
从开发的便捷性考虑性能
性能上的瓶颈需要在一开始就被修复或避免。一些性能瓶颈是显而易见的,会对用户体验造成明显的影响,这些就需要立即修复。因为这些瓶颈意味着我们的代码由于某些原因无法扩展,并且可能引出设计上更大的问题。
其他性能问题相对较小,通常是开发者为了通过各种手段提高性能,对代码进行基准测试时发现的。这些无法很好地扩展,因为这些小的性能瓶颈无法被用户察觉,但是修复起来非常耗时。即使应用程序大小合理,又有不少开发人员,但是每个开发人员都在修复这类小问题,就无法继续开发新的功能。
一方面,这种细微的优化会引入对特殊情况进行处理的代码,而这类代码对于其他开发者来说就没那么容易理解了。另一方面,如果不进行这种细微的优化,代码就会相对简洁,容易维护。在必要时,需舍弃性能优化来保证更好的代码质量。这样才能增强我们在其他方面提高可扩展性的能力。
性能的可配置性
如果有几乎每个方面都可配置的通用组件自然是极好的。然而,设计通用组件的代价需要牺牲性能。这在一开始只有少量组件时是无法察觉的,但是当软件功能、组件数量、组件配置项开始增加时,问题就显现出来了。随着每个组件尺寸(复杂度、可配置项的数量等)的增长,组件的性能就会呈指数递减,如下图所示。
只要性能问题没有影响到用户,就可以保留配置选项。不过需要注意的是,可能会在消除某个性能瓶颈时不得不删除某些可配置项,不过可配置项不太可能成为性能瓶颈的主要来源。在不断扩大和增加功能的过程中还容易过于投入,回顾起来你会发现,在设计时创建的自认为有用的可配置项,最终并没有什么用,反而加大了开销。所以,当配置项没有带来确凿的好处时,为了保证性能,应该果断舍弃。
从可替换性考虑性能
一个与可配置性相关的问题是可替换性。现在我们的用户界面运行良好,但是随着用户数量和功能的增加,我们发现某些组件无法轻易地被另一个组件替换。这可能是一个软件成长问题:想设计一个新的组件用来替换已有组件,或者可能需要在运行时替换某些组件。
替换组件的能力基本上由组件通信模型来决定。如果新的组件可以像已有组件那样发送/接收消息/事件,那么替换起来就相当简单了。但是并不是软件的所有方面都需要可以替换,为了保障性能,可能根本没有可替换的组件。
但当扩展应用时,可能需要考虑将大组件重构为较小的可替换组件。但是这样做会引入新的间接层,从而影响性能。不过牺牲一点点性能换来可替换性,可以帮助我们在其他方面获得架构的可扩展性。
可寻址性的开发便捷性
为应用程序中的资源分配可寻址的URI 必然会增加功能实现的难度。真的需要为应用暴露的每个资源分配URI 吗?也许不需要。从保持一致的角度看,为每个资源分配URI是合理的。但是如果针对某些资源,没有路由以及统一、易于遵循的URI 生成方案,那么更倾向于不为这种资源分配URI。
为应用中的每个资源分配URI 带来的负担是值得的,因为不支持可寻址资源更糟糕。URI 让我们的应用和用户熟悉的其他页面表现一致。也许URI 的生成和路由在应用中是不可改变的、不可舍弃的,所以几乎总是牺牲开发的便捷性来换取可寻址性。相较于URI,开发的便捷性可以随着软件的成熟更深入地解决。
性能的可维护性
功能开发的难易程度最终取决于开发团队和可扩展性的影响因素。例如,可能出于预算的压力不得不招聘初级开发人员。这样是否能适应后期要求,则取决于代码。当需要考虑代码性能时,很可能会引入一些对于缺乏经验的开发者难以理解的代码。很显然,这会阻碍新功能的开发。如果新功能开发难度比较大,那就会花费更长的时间,这显然不能适应用户的需求变化。
开发人员并不是总需要费力理解这些用来解决特定领域性能瓶颈的晦涩代码。当然可以通过编写高质量、易于理解的代码,甚至编写文档来缓解这个问题。但这一切都是有代价的,随着团队的壮大,需要短期内完成对开发者的培训、指导,这些都是在生产效率方面需要付出的代价。
在关键代码上经常是优先考虑性能而不是开发的便捷性。不能总是逃避代码性能带来的代码丑陋,但是如果这些丑陋的代码能很好的被隐藏,就可以得到更易理解和自解释的代码。比如,底层JavaScript 库性能良好,API 紧凑易用,但是如果你看一下底层的源码,就会发现并不是那么优美。这就是我们的收获——让别人维护出于性能原因而看起来丑陋的代码。
减少功能以提高可维护性
当所有其他手段都失败时,需要退后一步,全面审视应用中的所有功能。架构是否能够支持所有的这些功能?把投入了大量时间来开发的架构废弃掉是毫无道理的,但也确实会发生。大多数时候,会被要求实现一组颇具挑战的与我们结构相悖的功能。
这种情况发生时,实际上是在打乱现有的稳定功能,或者在应用中加入一些低质量的东西。两种情况皆无益处,但值得投入时间,花费精力,与投资人沟通以决定哪些必须被删除,尽管这个过程并不愉快。
如果已经做出折中的选择并且花时间理清了我们的架构,那么应该有一个合理的理由来解释为什么我们的软件不能支持上百个功能。
利用框架
框架的存在是为了通过采用一套紧密聚合的模式,帮助我们实现架构。框架具有极大的多样性,选择哪个框架是由个人偏好以及与设计的契合度来决定的。例如,一个JavaScript应用框架可以实现更多创造性,而另一个框架拥有更多功能,但其中大部分功能是我们不需要的。
JavaScript 应用框架有不同的大小和成熟度。有些内置了很多套件,有些则倾向于机制而非政策。没有一个框架是专为我们的应用设计的,对于每个框架所声称的功能要持有存疑态度。框架标榜的功能只适用于简单的一般情况,而在我们架构中的应用则完全是另外一回事。
尽管如此,当然可以用一个自己喜欢的框架作为设计过程的输入。如果我们真的喜欢这个工具,团队也有使用经验,可以让它来影响决策者。只要我们明白,框架不会自动地响应扩展影响因素,因为这个部分是由我们来负责的。
本文选自《大型JavaScript应用最佳实践指南》,点此链接可在博文视点官网查看此书。
想及时获得更多精彩文章,可在微信中搜索“博文视点”或者扫描下方二维码并关注。