【CSDN 编者按】本文深入探讨基于组件的前端架构是如何随着复杂度的增加而变得臃肿甚至难以维护,以及如何规避这种情况。
作者 | REM 译者 | 无阻我飞扬
出品 | CSDN(ID:CSDNnews)
构建高性能且易于更改的前端架构很难实现规模化。本文将探索解决这种复杂情况的主要方法,即众多开发人员和团队共同完成前端项目开发,并且可以快速而平滑地集成到前端项目中。
我们还将研究有效的方法来避免面对这种复杂情况时不知所措。无论是在问题出现之前还是之后,你会发现自己在被要求增加或改变一个功能时,会想“哦,天啦撸,这怎么会变得如此复杂?” 。
前端架构是一个涉及面很广的话题。本文将聚焦于组件代码结构,它能建立弹性的前端架构,从而轻松适应变化。
本文案例代码是基于React(用于构建用户界面的JavaScript库),其基本原理可以应用于任何基于组件的框架。
我们从编写代码之前开始谈起,看看代码结构是如何受到影响的。
常规心智模型的影响
我们的心智模型会影响我们的决定,进而影响代码库的整体结构。
在组建团队时,明确开发人员的心智模型很重要,最好大家都有共同的心智模型,但是每个个体又都会有自己潜在的心智模型。
这就是为什么团队在研发过程中需要共享样式指南和诸如prettier(前端代码格式化工具)之类的工具。作为一个团队,要有一个共同的模式,明确事情应该怎么保持一致性,大家要做哪些事情,应该怎么做。
这样就让研发工作变得更加轻松。具有共同心智模型的研发团队能够避免随着时间的推移,由于每个人都按自己的模式开发,从而让代码库沦为不可维护。
假如曾经经历过一个快速开发的项目,开发人员都急于发布程序,你可能已经发现,如果没有适当的指导方针,事情很快就会失控,随着时间的推移,代码的增加和代码运行性能的下降,前端会变得越来越慢。
在接下来的几节中,我们将探讨回答以下问题:
在使用基于组件的模型框架(如使用React)开发前端应用程序时,最常规的心智模型是什么?
它们如何影响构建组件的方式?
这些心智模型中隐含着程序员怎样的权衡,我们可以明确这些导致复杂度快速上升的权衡有哪些?
组件思维
React是最流行的基于组件的前端框架。“React思维”通常是前端框架开发人员读的第一篇文章。
它阐述了在以 "React方式 "构建前端应用程序时应如何思考的关键心智模型。这是一篇很好的文章,因为其中的建议也适用于任何基于组件的框架。
它所列出的主要原则让你在需要构建组件时提出以下问题。
该组件的责任是什么?好的组件API设计自然遵循单一责任原则,这对于组合模式非常重要。把简单的事情组合起来是很容易的,随着需求的不断增加和变化,组件保持简单往往是非常困难的,我们将在本文的后面部分进行探讨。
什么是其状态的绝对最小但完整的表示?这个想法是最好从最小但完整的状态真实来源开始,可以从中得出变化,这样灵活、简单,可以规避常见的数据同步错误,比如更新一个状态而不更新另一个状态。
状态应该定义在哪里?状态管理是一个广泛的话题,超出了本文探讨的范畴。但一般来说,如果一个状态可以成为一个组件的本地状态,那么它就应该是定义在本地的。组件在内部对全局状态依赖越多,它们的复用性就越低。提出这个问题有助于确定什么组件应该依赖于什么状态。
“React思维”这篇文章中还有一些智慧:
一个组件最好只做一件事,如果它变得庞大复杂了,就应该将它拆解为更小的子组件。
这里概述的原则是简单的,经过实践检验的,对于抑制组件复杂性很有效。它们构成了创建组件时最常规心智模型的基础。
不过,简单并不意味着容易。在有多个团队和开发人员的大型项目实践中,这一点说起来容易做起来难。
成功的项目往往来自于对基本原则的坚持,而且是持续的坚持,不要犯太多代价高昂的错误。
这就引出了我们将要探讨的两个问题。
是什么情况阻止了这些简单原则的应用?
如何才能尽可能地缓解这些情况?
下面将了解为什么随着时间的推移,保持简单性在实践中并不总是那么容易。
自上而下 vs. 自下而上
组件是React等现代框架中的核心抽象单元,可以考虑有两种主要的方法创建它们。以下是React中不得不说的内容:
可以自上而下或自下而上地构建。也就是说,可以从构建体系结构中更高层次的组件开始。在比较简单的项目中,通常自上而下更容易,而在较大的项目中,在构建时就编写测试代码,自下而上更容易。
更可靠的建议。乍一看,这听起来很简单,就像读到“单一责任很好”一样,很容易达成一致并继续向前。
但是,自上而下和自下而上的心智模型之间的区别,比表面上看起来要重要得多。应用于大规模研发时,当某一种心智模型作为构建组件的隐含方式被广泛分享时,这两种思维模式都会导致截然不同的结果。
自上而下的构建方式
在上面的引述中隐含着这样一种权衡,对于比较简单的项目,采取自上而下的方法,而对于大型的项目,则采取较慢、更具可扩展性的自下而上的方法,这样项目更容易取得进展。
自上而下通常是最直观、最直接的方法。根据我的经验,这也是从事功能开发的开发人员在构建组件时最常见的心智模型。
自上而下的方法是什么样的?当给出一个要构建的设计时,常规的建议是 "在UI周围绘制框,这些框将成为你的组件"。
这构成了最终创建的顶层组件的基础。采用这种方法,通常先创建一个粗颗粒度的组件,开始的时候似乎有一个正确的界限。
假设有一个新的管理仪表盘的设计,继续看设计需要构建哪些组件。
在设计中,它有一个新的侧边导航。在侧边导航周围画一个方框,并创建一个Story(在软件开发和项目管理中用日常语言或商务用语表达开发需求),告诉开发人员要创建新的
遵循自上而下的方法,可能会考虑它需要什么属性,以及如何渲染。假设从后端API获得导航项的列表,按照自上而下的模型,看到类似下面伪代码的初始设计也就不足为奇了:
// get list from API call somewhere up here
// and then transform into a list we pass to our nav component
const navItems = [
{ label: 'Home', to: '/home' },
{ label: 'Dashboards', to: '/dashboards' },
{ label: 'Settings', to: '/settings' },
]
...
到目前为止,自上而下的方法看起来相当直接易懂。我们的目的是让事情变得简单和可复用,用户只需要传递想要渲染的项目,交由SideNavigation来处理。
自上而下方法中常见的一些注意事项:
从最初确定为所需组件的顶层边界开始构建,从设计中画出的框开始;
它是一个单一的抽象,处理与侧边导航栏相关的所有事情;
它的API通常是“自上而下”的,即用户通过顶层向下传递它所需要的数据,并在后台处理一切事务。
通常情况下,组件直接从后端数据源渲染数据,这也符合将数据“向下”传递到组件中进行渲染的模型。
对于较小的项目来说,这种方法没有什么不对,但是对于众多开发人员试图快速发布的大型代码库来说,会看到自上而下的心智模型如何在大规模项目上很快出现问题。
自上而下错在哪了
自上而下的思维模式倾向于一开始就把自己固定在一个特定的抽象上,以解决眼前的问题。
这是直观易懂的。它常常被认为是构建组件最直接的方法,它也经常优化API,以实现组件最初的易用性。
这里有一个比较常见的场景。你所在的团队正在进行一个快速开发的项目,已经画出了框并创建了Story,完成了新组件合并,此时,一个新的需求出现了,要求更新侧边导航组件。
这个时候,事情可能会开始变得非常棘手。这是一种常见的情况,可能会导致创建大型单片组件。
开发人员拿起story进行更改,在现场准备编码,处在已经确定的抽象和API的项目背景下。
他们面临的选择:
考量一下这是否是正确的抽象。如果不是,在执行故事大纲之前先主动分解来撤销它;
增加一个附加属性。在一个简单的条件后面添加新的功能以检查该属性,编写一些测试传递给新属性,新功能通过测试并运行正常,这么做的好处在于完成的很快。
正如桑迪-梅茨所说:
现有代码发挥着强大的影响力。它存在的本身就证明了它的正确性和必要性。我们知道代码代表了所付出的努力,非常有动力去维持这种努力的价值。不幸的是,可悲的事实是,代码越复杂,越难以理解,也就是说,在创建代码方面投入越深,就越感到继续保留这些代码的压力("沉没成本谬论")
沉没成本谬论之所以存在,是因为人们天生就更热衷于规避损失。当再加上时间考量,要么是来自最后期限,要么只是简单的“1个story point”(story point是一个度量单位,用于表示完成一个产品待办项或者其他任何某项工作所需的所有工作量的估算结果),开发人员选择A的可能性不大。
从规模上看,正是这些快速做出的小决定迅速累积起来,开始增加组件的复杂性。
不幸的是,我们现在已经违背了 "用React思考 "中概述的一个基本原则。简单的事情往往不会简单化,与其它选择相比,引导我们走向简单并不容易做到。
警告
让我们将这种常见的场景应用到简单的导航侧栏例子中。
第一个设计变更需求出现了。需要增加对导航项的要求,使其具有图标以及不同大小的文本,并使其中一些成为链接而不是SPA页面过渡。
在实践中,UI拥有大量的视觉状态。我们还希望有分隔符,在新选项卡中打开链接,被点击过的链接的默认状态,等等诸如此类。
因为把导航项列表作为数组传递给侧栏组件,对于这些新的要求,需要在这些对象上添加一些附加属性,以区分新类型的导航项目及其不同的状态。
所以现在的类型可能看起来像是这样,类型对应于它是链接还是常规导航项:{id, to, label, icon, size, type, separator, isSelected}等等。
然后在
这里的问题是,具有这样的API的自上而下组件,必须通过添加到API来响应需求的变化,并基于传入的内容在内部逻辑分叉。
从小事到大事的发展
几周后,要求提供一个新的功能,需要能够点击一个导航项目,转换到该项目下的一个嵌套子导航,并有一个返回按钮回到主导航列表,还希望管理员能够通过拖放对导航项进行重新排序。
现在需要有嵌套列表的概念,并将子列表与父列表关联起来,确认有些导航项目是否可以拖动。
一些需求发生了变化,可以看到事情是如何开始变得复杂的。
一开始是一个相对简单的组件,有一个简单的API,经过几次快速的迭代,迅速发展成为其他组件。比方说,开发人员及时设法使事情顺利进行。
至此,下一个需要使用或改编这个组件的开发人员或团队要面对的是一个需要复杂配置的单片组件,而且(说实话)很可能根本就没有什么好的开发说明文档。
最初的意图是“只传递列表,剩下的就由组件来处理”,但现在却事与愿违,对组件进行更改既缓慢又有风险。
此时,一个常见的场景是考虑废弃所有东西,从头开始重写组件。现在我们已经了解了第一轮迭代中需要解决的问题和用例。
单片组件的逐渐增长
除了第一次,一切都应该自上而下地构建。
正如我们所看到的,单片组件是试图做太多事情的组件。它们通过属性接收太多数据或配置选项,管理太多状态,输出太多UI。
它们通常从简单的组件开始,通过上述更常见的复杂性的逐渐增长,随着时间的推移,最终一个单片组件要做得太多了。
一开始只是一个简单的组件,在实现新的功能时,经过几次迭代过程(甚至在同一个sprint中,sprint是项目开发过程中最小迭代周期),就会变成一个庞大的单片组件。
当团队在快速开发的情况下,使用同一个代码库,如果多个组件发生这种情况,前端很快就会变得更难更改,用户的终端速度也会变得更慢。
下面是单片组件可能导致前端突然崩溃的一些其它方式。
它们通过过早的抽象而产生。还有一个微妙的陷阱导致了单片组件的出现。这与作为软件开发者早期被灌输的一些常见模型有关,特别是对DRY(不要重复自己)的坚持。
事实上,DRY早就根深蒂固了,我们在组成组件的地方看到了少量的重复现象,很容易想当然地认为“这个重复的东西很多,如果把它抽象成一个单一的组件就好了”,于是仓促地进行了过早的抽象。
一切都是一种权衡,但从没有抽象中恢复要比从错误的抽象中恢复要容易得多。正如我们将在下面进一步讨论的那样,从一个自下而上的模型开始,可以逐渐地得出这些抽象,避免过早地创建它们。
它们防止跨团队复用代码。经常会发现另一个团队已经实现了或正在开发与你的团队所需要的类似的东西。
在大多数情况下,它能做到90%你想要的,但你想要一些轻微的变化,或者只是想重复使用它功能的特定部分,而不需要把整个东西都拿过来。
如果像
它们使代码膨胀。怎样才能只允许在正确的时间加载、解析和运行需要的代码?
有一些更重要的组件需要优先展示。对于大型应用程序来说,一个关键的性能策略是根据优先级在 "phases "中协调异步加载的代码。
除了让组件能够选择在服务器上的渲染与否之外(因为理想情况下,只对那些真正会被用户在第一时间看到的组件尽可能快地执行服务器端的渲染。),这里的想法是在可能的情况下延迟渲染。
单片组件阻止了这些意图的发生,因为必须将所有内容作为一个大块组件加载,而不是拥有可以优化的独立组件,这些组件只在用户真正需要的时候加载,用户只需付出实际使用的性能代价。
它们导致运行时性能低下。像React这样的框架,有一个简单的状态—〉UI功能模型,其效率令人难以置信。但是,为了查看虚拟DOM中发生了什么变化而进行的协调过程在大规模开发中代价是相当昂贵的。单片组件很难保证在状态发生变化时只重新渲染最少的内容。
在像React这样的虚拟DOM框架中,实现更好渲染性能的最简单方法之一是将更改的组件与已经更改的组件分开。
因此,当状态更改时,只需要重新渲染严格意义上必要的内容。如果使用像Relay这样的声明式数据获取框架,那么这种技术会变得越来越重要,它可以规避数据更新时对子树进行代价昂贵的重新渲染。
一般来说,在单片组件和自上而下的方法中,很难找到这种分割,容易出错,而且常常导致过度使用memo()。
自下而上的构建方式
与自上而下的方法相比,自下而上的方法通常不那么直观,而且最初可能会比较慢。它会产生多个较小的组件,这些组件的API是可复用的,而不是庞大的单片组件。
当试图快速发布软件时,这是一种不直观的方法,因为在实践中并非每个组件都需要可复用。
然而,创建API可以复用的组件(即使它们不能复用)通常会形成更可读、可测试、可更改和可删除的组件结构。
关于事情应该被分解到什么程度,没有一个标准的答案。解决这个问题的关键是使用单一责任原则作为一般准则。
自下而上与自上而下的心智模型有何不同?
回到上面的例子。使用自下而上的方法,仍然有可能创造一个顶层的
确定了顶层的
而是首先对构成
总的复杂性分布在许多较小的单一责任组件中,而不是在单个的单片组件中。
自下而上的方法是什么样子的?
回到侧边导航的例子。下面是一个简单案例代码示例:
Home
Settings
在这个简单的例子中没什么可说的。支持嵌套组的API是什么样子的?
Home
Projects
Settings
Foo
Project 1
Project 2
Project 3
See documentation
自下而上方法的结果是直观的。它需要做更多的前期工作,因为更简单的API的复杂性被封装在各个组件中,但这正是它成为一种更具易用性和可变性长期方法的原因。
与自上而下的方法相比,它的优势有很多:
使用组件的不同团队只为实际导入和使用的组件付诸努力;
也可以轻松地进行代码拆分和异步加载那些对用户来说不具有直接优先级的元素;
渲染性能更好且更易于管理,因为只有因更新而更改的子树需要重新渲染;
可以创建和优化在导航中具有特定责任的单个组件。从代码结构的角度来看,它的可扩展性更强,因为每个组件都可以单独工作和优化。
有什么问题?
自下而上的方法一开始会比较慢,但从长远来看会更快,因为它的适应性更强。可以更容易地避免仓促过早的抽象,随着时间的推移,驾驭变化的浪潮,直到正确的抽象变得明晰,这是防止单片组件扩散的最佳方法。
如果是像侧边导航这样在整个代码库中使用的共享组件,自下而上的构建往往需要团队花费更多的精力来组装这些部件,但正如我们所看到的,在具有许多共享组件的大型项目中,这是一种值得做出的权衡。
自下而上方法的强大之处在于,模型以“我可以将哪些简单的基本单元组合在一起以实现我想要的东西 "的前提开始,而不是从脑海中已经存在的特定抽象开始。
敏捷软件开发最重要的经验之一是迭代的价值;这适用于所有级别的软件开发,包括架构”
从长远来看,自下而上的方法可以更好地进行迭代。
接下来,回顾一下一些有用的原则,记住这些原则可以让自下而上的构建方式变得更容易:
避免使用单片组件策略
平衡单一责任与DRY
自下而上的思维通常意味着接受组合模型。这往往意味着在开发上可能会有一些重复。
DRY是开发人员学习的第一件事,对代码进行DRY的感觉很好,但是把所有东西都DRY之前,最好还是等一等,看看是否有必要。
但是这种方法可以让开发人员随着项目的增长和需求的变化而“驾驭复杂的浪潮”,并且允许在有必要的时候更容易地使用抽象的东西。
反转控制
理解这一原则的一个简单例子是callbacks 和Promises(异步编程的解决方案)之间的区别。
使用回调函数,不一定知道该函数的去向,它将被调用多少次,或者用什么来调用。
Promises将控制权反转回用户,这样就可以开始编写逻辑,并假装值已经存在。
// may not know what onLoaded will do with the callback we pass it
onLoaded((stuff) => {
doSomething(stuff);
})
// control stays with us to start composing logic as if the
// value was already there
onLoaded.then(stuff => {
doSomething(stuff);
})
在React的中,这是通过组件API设计实现的。
可以通过子元素来展示“slots”,或者渲染样式属性,以保持用户方的反转控制。
在这方面,有时用户会有一种对反转控制的厌恶,因为有一种不得不做更多工作的感觉。但这既是意味着放弃可以预测未来的想法,也是为了选择赋予用户以灵活性。
// A "top down" approach to a simple button API
// with inversion of control
// provide a slot consumers can utilize how they see fit
: null} />
第二个例子更灵活地满足不断变化的需求,性能也更高,因为
可以在这里看到自上而下和自下而上的细微差别。在第一个例子中,传递数据并让组件处理。在第二个例子中,需要做更多的工作,但最终这是一种更灵活、更高效的方法。
有趣的是,本身可以由后台更小的基本元素组成。有时候,一个特定的抽象背后有许多不同的子行为元素,这些元素可以显式表示。
例如,可以将其进一步分解为应用于两个按钮的Pressable和Link组件,它们可以组合起来创建类似LinkButton之类的东西。这种细颗粒度的分解通常留给设计系统库领域,但作为以产品为中心的工程师,这一点值得铭记于心。
开放扩展
即使是在使用组合模式自下而上构建时,仍然希望导出具有可使用API的专用组件,但这些组件是由较小的基本元素构建而成,为了灵活起见,也可以从包中公开那些组成专用组件的较小构建模块。
理想情况下,组件只做一件事。因此,在预定抽象的情况下,用户可以将需要的东西包装起来,用其自己的功能进行扩展,或者,也可以只提取一些构成现有抽象的基本元素,构建所需要的东西。
利用storybook驱动开发
通常有大量的离散状态最终会在组件中得到管理。状态机库变得越来越流行是有原因的。
当用storybook独立地构建UI组件时,可以采用他们想法背后的模型,并为组件可能处于的每一种类型的状态编写story。
像这样预先做,可以避免在生产中意识到忘记了实现一个好的error状态。
这有助于确定所有的子组件,这些组件需要建立在正在开发的组件之上。
在独立构建UI组件时,问自己一些可以形成弹性组件的问题:
它是否可以访问?
它在加载时是什么样子的?
它依赖于哪些数据?
它是如何处理错误的?
当只有部分数据可用时,会发生什么情况?
如果多次加载这个组件,会发生什么情况?换句话说,它有什么样的副作用,如果它管理内部状态,会期望这种状态是一致的吗?
它是如何处理“不可能的状态”以及这些状态之间转换的。例如,它有一个'加载'和'错误'属性,如果它们都是'真',会怎么样?(在这个例子中,这可能是一个重新思考组件API的机会)
它的可组合性如何?考量一下它的API。
有啥让人高兴的吗?比如,精致的动画效果。
下面是一些更常见需要规避的情况,会阻碍构建弹性组件:
根据组件的实际功能命名组件。这又回到了单一责任原则。不要害怕给组件取长名字,如果它们有意义的话。
把组件命名为比其实际功能更通用的名称也很容易。当组件的命名比它们实际做的更通用时,就是向其他开发人员表明,它是处理与X(未知)相关的一切的抽象。
因此,当出现新的需求时,它自然会作为进行更改的明显位置而脱颖而出,即使这样做可能没有意义。
避免使用包含实现细节的属性名称。尤其是UI样式的“leaf”组件。尽量避免添加类似isSomething这样的属性,因为这些属性与内部状态或特定领域的东西有关,然后让该组件在传递该属性时执行不同的操作。
如果需要这样做,属性的名字能反映出它在使用它的那个组件中的实际作用,那就更清楚了。
举例来说,如果isSomething属性最终控制了诸如padding之类的东西,那么属性名称就应该反映出这一点,而不是让组件知道一些看起来不相关的东西。
谨慎使用 props 进行配置。这又回到了反转控制。
如果知道只会有一种类型的子类(并且肯定这一点不会改变!),像〈SideNavigation navItems={items}/〉这样的组件可以很好地工作,因为它们也可以被安全地分类。
但是正如我们所看到的,这种模式很难在不同的团队和试图快速发布的开发人员之间进行扩展,而且在实践中,它对变化的适应性较差,复杂性反而增长较快。
因为经常会希望扩展组件,使其拥有不同的或额外的子类型,这意味着将会在这些配置选项(或属性)中添加更多内容,并添加分支逻辑。
与其让用户安排和传递对象,更灵活的方法是把内部的子组件导出,让用户组合和传递组件。
避免在渲染方法中定义组件。有时候,在一个组件中拥有“helper”组件可能是很常见的,这些组件最终会在每次渲染时被重新加载,并可能产生一些奇怪的bug(错误)。
此外,有多个内部的renderX、renderY方法往往就会导致那种情况的发生,这些通常是一个组件变成单片组件的标志,是组件分解的良好候选者。
分解单片组件
如果可能的话,请尽早进行组件重构。识别可能发生变化的组件并积极地分解这是一个很好的策略,可以将其融入到开发评估中。
当发现前端变得过于复杂时,你会怎么做?
通常有两种选择:
重写内容并逐步迁移到新的组件上
逐步分解组件
探讨组件重构策略暂时不在本文的范围之内,但是可以利用现有的一些经过实践检验的重构模式。
在像React这样的框架中,“组件”实际上只是伪装的函数。可以在所有现有的行之有效的重构技术中用组件来代替“函数”这个词。
举几个相关的例子:
删除标志参数
用多态性取代条件性
字段上移
重命名变量
内联函数
总结
本文讲了很多,让我们回顾一下要点。
心智模型影响了我们在设计和构建前端组件时做出的许多微观决策。明确这些信息是非常有用的,因为它们累积得相当快。这些决策的累积最终决定了什么是可能的——要么增加或减少添加新功能的摩擦,要么采用使我们能够进一步扩展新架构(对此不确定或将合并到下面)。
在构建组件时,自上而下与自下而上在大规模开发中可能会导致截然不同的结果。构建组件时,自上向下的心智模型通常是最直观的。当涉及到分解UI时,最常见的模型是在功能区域周围绘制方框,然后这些方框就成为组件。这种功能分解过程是自上而下的,通常会直接导致创建具有特定抽象的专用组件。需求会发生变化,在几次迭代过程中,这些组件很容易快速成为单片组件。
自上而下的设计和构建会导致单片组件的产生。一个充满了单片组件的代码库,会导致前端架构缓慢且无法适应变化,单片组件不好,因为:
它们的更改和维护成本很高
它们的更改是有风险的
很难跨团队软件复用
它们的性能很差
在采用面向未来的技术和架构时,它们会增加摩擦,而这些技术和架构对于继续扩展前端非常重要,比如有效的代码拆分、跨团队的代码复用、加载阶段、渲染性能等。
可以通过了解底层模型和环境来避免创建单片组件,这些底层模型和环境通常会导致过早的创建抽象或对其进行持续扩展。
React在设计组件时更有效地支持自下而上模型,这样可以更有效地避免过早的抽象,就可以“驾驭复杂的浪潮”,在时机成熟时进行抽象。这种构建方式为组件组合模式的实现提供了更多可能性。意识到单片组件的真正成本有多高,就可以应用标准的重构实践来定期分解它们,将其作为日常产品开发的一部分。
原文链接:https://frontendmastery.com/posts/building-future-facing-frontend-architectures/
本文为CSDN编译整理,未经授权,禁止转载!
参考链接:
https://www.geeksforgeeks.org/difference-between-bottom-up-model-and-top-down-model/
https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
https://kentcdodds.com/blog/inversion-of-control
https://kentcdodds.com/blog/aha-programming
— 推荐阅读 —
《新程序员004》已全面上市
48 位程序员出身、在各自领域取得卓越成就的高手
以自己的阅历和洞见来解答程序员们所面临的普遍疑惑。