东亚银行、岚图汽车带你解锁 AIGC 时代的数字化人才培养各赛道新模式! 了解详情
写点什么

拉动的力量:一种新的软件生命周期

  • 2009-11-25
  • 本文字数:8126 字

    阅读完需:约 27 分钟

IT 项目的起因很多,但是只有一部分软件能够带来真正的改变。软件开发是昂贵的,在预算紧张时,把钱花在刀刃上就变得异常重要。

在这篇文章中,我们将要看到如何把来自精益方法的看板,与来自行为驱动开发的特性注入模版结合在一起,来帮助我们识别最重要的软件,在开发的各个阶段中消除不必要的浪费,用最少的产出来完成目标。

下面的图显示了本文所讲述的新的软件生命周期。这些产出物给每个角色发出信号,促使他们创建更多的产出物,直到完成目标并获得商业价值。

最有价值的软件是还没有写出来的软件

Dan North 将软件与金属进行了比较。“生成报告”,他说,“就像铅。很容易得到,还很便宜。但同时也很重,没什么价值,还会拖你的后腿。开发人员耗费大量 时间编写报告软件。为什么不直接买一个、安装上、做一点修改,这样不是也行么?或者使用 Excel。除非你是一家销售这些软件的公司,否则不要花钱来构建 报告软件。

“还有些软件是比较难做的,需要配置并与第三方交互,但你还是可以购买。这就像铜”。他谈到了子域(subdomains)支持,这个词来自于 Eric Evans 的《领域驱动开发》。这就好比一个核心业务是寻找石油的公司,“他们需要 GPS 系统。虽然不是每个人都需要这样的系统,但是他们需要,而且还有 很多人也需要。那么你可以购买 GPS 系统,然后集成到你的系统中”。

“因此”,我建议,“如果你有类似的需求,而且这种需求对谁都是一样的,但是还没有人卖,那么你或许希望能有人帮你做一个这样的产品。”

“当然。”他在卡片上写道:

Dan 说:“这个是铅。我跟 CIO 们谈谈,问问他们花了多少钱在这上。太多了!”

“那么上面是什么?”我问道。

“上面(左上角)是能快速带来价值的东西。我们有一些软件,它们很容易构建,而且涉及到你的核心领域。这是 Agile 实验项目的理想目标,可视的、高价值的项目,而且没有人做过。本质上来说它们是差异化的。它们有价值,它们是黄金。”

“然后是右上角,这个象限是白金项目。它们很难做,是核心领域,充满风险而且可能需要和第三方进行交互,非常困难的交互,它们是有价值的差异。”

GOLD PLATINUM LEAD COPPERDavid Anderson 定义了四个商业关注点:商品、差异、节约成本和破坏者。“这是必然的”,我说,“总会有人破坏你的这些差异性-你的黄金和白金项目。他们 会盗用你的创意-不需要承担你所承担的风险,因为他们看到它确实可以使用-然后用更低的成本实现。这就是为什么我们需要敏捷,未来的某一点我们必然要改 变,来创建新的差异。”

Dan 笑着说:“Chris Matts 认为,构建软件只有三个原因:赚钱、省钱、捂钱,这第三个原因非常有意思。LastMinute.com 是一个典型的例子,他们通过与供应商签 署排它性协议赢得了所有的市场-剧院、休闲以及任何他们认为具有最后期限,而且人们可以在到期之前取消的东西。回想起来,LastMinute 的想法是显 而易见的,但当所有人都来分一杯羹时,逐渐缩小的投资回报导致只有很少一部分人能够坚持下来。

“当然,白金项目成本会比较高,因为他们很复杂,但从这个角度看,它比黄金项目更有价值。”

“好的”,我说,“因此,理想情况下,我们应该每周或每两周就发布新的软件版本,我们需要快速响应商业需求。当然,我们知道有些人会破坏我们的东西。因此 为了保护我们的钱,我们需要有一个最小商业特性集,它要能使复制的成本非常高昂。而且,必要的复杂度实际上是好事,它能使我们保持不同……至少在某段时间 内不同。创建一些差异,然后保持这些差异,这就是我们的工作范围。”

现在,我们知道了如何识别重要的目标和它的范围,下面我们该看看如何实现它了。

愿景促使干系人创建特性集

一旦识别出愿景,最初的干系人会通过思考所有需要参与的人,来确定项目其他的干系人。然后干系人会考虑他们需要哪些东西来实现愿景。我们通常是用这个模版:

作为……
我希望……
于是……

我记得 Guardian 的编辑们使用这种格式,业务分析师们将之称为“讨要功能”。Chris Matts 发现了这个问题,“你将干系人放在了最前面,所以他们会考虑那些能证明他们的东西。换一种方式,我们可以将愿景放在前面。这样人们就不会添加和 愿景无关的东西,有助于减少需求范围的蔓延。事情本来就应如此……”。我将他的建议记了下来:

为了……
作为……
我希望……

特性集对愿景的依赖与开发人员将依赖注入到代码中的方式很相似,并不是因为它是个好主意,而是因为它是必须的,因此有了这个词-“特性注入”。

有时我们发现,将愿景分割成一些小的目标很有用,它们定义了一个终点,而且总是可以回溯到大的愿景。

除此之外,那些提出 story 的干系人并不总是最后会使用它们的人。Captchas-使用难以识别的图形字母来做图灵测试-就是一个例子。“作为 一个用户,我希望有一个 captcha,于是……等等,我不想要 captcha,这是浪费我的时间”。我们使用模版来写这个 story:

为了……
作为……
我希望……

例如:

为了防止机器人给我的网站添乱
作为论坛的管理员
我希望用户必须要回答 captcha 才能写注释。

有些特性甚至跟软件没关系!可能我们的 story 会包含如培训、物流、网络管理、法律等东西。干系人通常都会有软件解决不了的,或者不用软件解决更好的东西,例如 LastMinute.com 的排他性合同。当我们考虑愿景时,也需要考虑这些。

这些故事通常会很大,以至于开发团队无法处理或估算。Mike Cohn 将之称为“themes”,而特性驱动团队称之为“特性集”,我们也这样称呼它们。

每个特性集的输出-例如,别让机器人来烦我-可能不能只依赖一个特性或者一个干系人来实现,我们会在过程中发现,其他的特性集可以实现或帮助同一个产出。当这种情况发生时,我们会再次审视一下我们的特性集。

特性集促使 BA(业务分析师)创建用户故事

Chris Matts 有一个理论。“你知道人们想要什么么?”

“想要什么?”我问道。

“他们向你要求的。你知道他们不想要什么么?”

“不想要什么?”

“你希望他们要的。你知道还有什么么?”

“还有什么?”

“一旦他们得到了他们想要的,他们就会要的更多。”

从多年尝试瀑布模型的经验中我们得出,我们很少能够事先预知所有的事,试图在一开始就做对是不完美的。干系人想要一些不同的东西,当我们终于将事情做对了,他们又开始要更多的东西了。

通常,我们在用户故事和特性集上做决定时所处的环境,会随着时间而改变,特别是在项目的生命周期内。唯一能让我们知道我们对项目的理解是否正确的方 式就是尽早得到反馈。得到反馈最好的方式,是给干系人一些他们可以实际操作的东西,他们可以体会用户或其他消费者的体验,并调整他们最初的想法。

为了达到这个目的,我们可以先拿掉与愿景实质上不想干的东西,即时它是发布商品或者保护差异的最小特性集的一部分。例如,对于购物车我们只有固定的送货费。修改送货选项或者大宗商品折扣可以放到以后的 story 中。

考虑大多数在线商店都会有类似的特性集:

为了能销售更多的商品
作为网络销售的头
我希望消费者能够在线订购商品,并能收到商品

它可以被分解为多个用户故事:

为了能销售更多的商品
作为网络销售的头
我希望消费者能够订购商品

为了能销售更多的商品
作为网络销售的头
我希望消费者能够将商品放在篮子里而且能够订购篮子(里的商品)

为了能销售更多的商品
作为网络销售的头
我希望消费者可以修改送货选项

为了能销售更多的商品
作为网络销售的头
我希望当(消费者的)订单超过一个最小值时,提供免费送货

当这些被实现出来时,第一个用户故事可以让我们从干系人那里得到美学方面的反馈,让他们考虑其他用户故事,例如放置相关商品的广告,这可以为项目愿景的实现添加一些不同点。其余的 story 可以晚一些再添加进来,我们可以从这些用户故事中分别得到反馈。

不是一定非要让 BA 来编写这些用户故事,不过通常他们对此比较在行。

用户故事促使 QA 创建场景

当 QA 测试一个特性时,他们通常会做三件事:

  • 设置应用程序运行的初始状态、数据等
  • 在测试环境中执行一些步骤
  • 检查输出结果

然后他们设置不同的环境,检查不同的输出。

当需要自动化这些场景时,我们可以使用行为驱动开发(BDD)和场景语言 Given、When、Then 来定义这些(场景)。

“领域驱动开发”的作者 Eric Evans 有一次被问到,场景和用例之间有什么分别。“你无法向业务人员要用例”,我回答,“除非他们懂技术,知道用例是什么。但是你可以管他们要例子, 你可以说‘给我一个场景’”。BDD 使用的语言,和 DDD 中的通用语言,都是为了能够让业务人员和开发人员可以进行交流。

“有一个满足免费送货标准的购物篮,当我结账时,屏幕上应该显示什么?”

“嗯……这个订单免费配送!”

从对话中,我们发现了新的信息,我们可以用这些信息来写一个例子:

有一个购物篮,里面的商品总价为 150 欧元
当我进入结账界面后
屏幕上应该显示“此订单免费配送”

QA 可以将它作为一个验收测试,QA 或开发人员可以将它自动化。

不是一定非要 QA 来写这些场景,不过根据我的经验,他们在这方面极为擅长。

所有这一切帮助我们确信,当我们开始实现时,我们的代码将有最大的机会为客户带来价值。

不是一定非要让 QA 来编写这些场景,不过通常他们对此比较在行。

场景促使 UI 专家设计 UI

在我主持的一次回顾中,开发人员识别出,UI 专家是阻碍他们完成故事的最大障碍。“从我们第一次见到这个故事,到我们可以开始工作,足足用了三天时间。”一个开发人员说道。

“定义页面的样子就需要这么长时间”,我们的 UI 专家回答道。

“嗯,”我沉思了一会,“有没有办法缩短这个时间?或许只做到开发人员可以工作就行了?”

“可能吧,你需要什么?”

一个开发人员拿起一张卡片,在上面画了一个草图。“就像这个样子”,他说,“只要能表达网站上显示什么就行了。一旦我们得到了内容,就可以开始写代码,使格式易于修改。”

“很好,”UI 专家笑道,“因为我们通常没法一次做对。”

这样,当 UI 专家忙于创建界面时,开发人员可以做一些简单但是可以工作的东西。界面的观感甚至可以作为一个单独的故事。那些内容-页面上应该显示哪些东西,用户应该点哪个按钮-定义了开发人员可以开始编写的第一块代码。

UI 不一定是图形化的。例如,一个应用程序可以被另一个系统使用,甚至没有人会直接查看它的输出。使用它的系统成为了用户,UI 专家需要知道什么样的数据才是消费系统需要的。

不一定非要让 UI 专家来设计 UI,不过通常他们对此比较在行。

UI 促使开发人员编写代码

一个刚毕业的开发人员,Jerry,问我:“我怎么才能知道我的代码是正确的?”

“你怎么知道它不正确呢?”我问道。

“会有人报告一个 bug,”他看着 QA 回答道,“可能是他们,也可能是用户。”

“他们怎么知道那是一个 bug 呢?”

“他们会使用系统,但是系统没有按着他们需要的那样去做。”

“是的。所以,我们判断你的代码是不是正确的唯一方式,就是通过用户界面。不管背后有什么,都是支持 UI 行为,并让我们可以在需要时易于修改这些行为。

”UI 是用户体验软件价值的唯一途径。创建代码可以使用的界面,是让代码有意义的本质。”

通过首先创建 UI,并将所有它需要的类打桩,我们可以快速获得 UI 是否满足商业需求的反馈。我们可以讨论可能需要修改的东西,我们有更大的机会来创建有价值的软件。如果是另一个系统使用这个 UI,我们可以检查两个系统是否能够通信。

代码促使开发人员编写更多的代码

Jerry 编写了界面的代码,他尽力让这一层很薄,并将界面使用的其他类 stub out 出去。然后,他思考下一步做什么。“我需要一个数据库表,表中包含三个列……”

“等一等,” 我建议他,“我们现在就需要数据库表么?我们有什么东西会使用数据库表么?”

“嗯,我们需要使用用户名、邮件地址和用户 id 来创建用户。”

“做什么呢?”

“这样用户就能通过注册界面在网站上注册了。”

“注册界面需要什么东西来帮助它完成注册新用户么?是不是要在界面里做所有的工作?”

Jerry 摇了摇头。“那意味着在一个地方放很多的代码,会让修改和维护变得异常艰难,而且 UI 的类也会难以测试,所以我们需要尽量让它短小。我知道我们需要使用其他的类,在设计模式中通常可以称之为 controller 或者 presenter。”

我们知道我们将来需要修改某些代码,因为 Chris 说过:“人们总是想要其他的东西”,或者因为业务上的差异性已经变化了。“也就是说,UI 需要将实际的工作分配给其他的类。我们现在能否知道我们的 Controller 将会使用一个数据库中的表?”

“不能……”

“好的,”我说,“让我们想想 controller 需要做些什么。”

我们将代码分解到不同的类或模块中。单一职责原则是一个好方法。我们可以问问自己,“这段代码应该做什么?不应该做什么?哪些东西是其他类的职责?我们将会如何使用这个类?”如果我们对每一行代码都考虑这些问题,那么所有的代码都会变得易于修改。

在单元级别,BDD 语言仍然有用。我们可以使用“应该(should)”来帮助我们将职责拉出来。

“现在,我们知道 controller 的行为是什么,它如何使用其他的类-user repository 和 user information,”Jerry 说,“我们还知道它应该响应 UI 的事件。”

我说,“我们可以使用我们的 controller 写一个 fake UI 类示例,来描述我们希望从 controller 中得到的东西。这可以帮助我们写出能给 UI 带来价值的最少的代码。作为一个有用的副作用,它还能为我们带来代码的单元测试和说明文档。”

首先,我们写了示例,或者叫单元测试。然后利用 IDE 来创建还未创建的类和方法,因为这样比较快。领域驱动开发(DDD)可以保持设计和代码 语言与业务一致,BDD 帮助我们关注行为、价值和如何使用类的示例。在发现某些新的、不寻常的或者遗漏的东西时,这帮助我们与业务建立有用的对话,而且由 于代码的设计和业务保持一致,对代码的修改难度,将与引起这些修改的业务流程修改的难度一致。

当我们完成了对每个类的编码后,我们继续对这个类的合作者编码,直到所有 stub 出来的类都有一个真正的实现。通过这种方式,我们可以保证我 们只做了那些可以为 UI 提供价值的事。我们不会为那些我们认为将来可能会用到的方法或数据库列来写代码,因为这在当时不是问题。这也帮助我们简化我们的设计。

好的设计可以在我们更深入理解业务流程时,简化代码修改,并为创建 UI 需要的价值提供更多的选择。就像 Chris 说的,“在金融领域,选项拥 有价值和过期时间。真正的选项-真实世界中的选项-也有价值,以及一个再没有选择余地的时间。有时,推迟决定并保持选项是值得的。”好的设计需要更长的时 间,但是这样是值得的,因为它让我们能够推迟决策,或当我们有更好理解时能够改变我们的决定。

这就是为什么我们使用好的设计实践,和像单元测试与系统测试这样的自动化反馈环,都是为了让修改变得容易。

不一定非要让开发人员来写这些代码,不过如果你正在写,那么你最终会成为一个开发人员。

我们为下一件最重要的事做准备

让我们看一个例子。

有一个简单的愿景:noughts and crosses 游戏。出于练习的目的,我们假装没人玩过这个游戏,也没有人看过战争游戏。我们有一个很大的特性集,叫做“游戏规则”。

我们可以为这个特性集写一个故事。

为了玩 Noughts and Crosses
作为一个玩家
我希望当有三个相同图形在一行时,有一个人获胜

下面是这个游戏的一个场景,以及对应的 UI,使用 JBehave 来自动化:

给定一个这样的网格
OO.
XX.
X…
当玩家点击 a3 时
网格将会变成
OOO
XX.
X…
并显示“O 赢了!”

下面是定义 GameModel 应该如何工作的示例代码:

复制代码
@Test
public void shouldNotifyObserverWhenTheCurrentPlayerWins() {
// 给定一个 X 将要获胜的布局
GameModel game = new GameModel();
game.playerActsAt(0, 0);
game.playerActsAt(1, 0);
game.playerActsAt(0, 1);
game.playerActsAt(2, 0);
MyGameObserver observer = new MyGameObserver();
game.addObserver(observer);
// 当 X 获胜时
game.playerActsAt(0, 2);
// X 应该是当前的玩家
// 而且 X 获胜了
ensureThat(observer.game.currentPlayer(), is(Player.X));
ensureThat(observer.game.message(), is(“Player X wins!”);
}

一旦我们完成这些示例,就可以开始编写代码了-GameModel 类-来帮助我们使这个示例运行通过。

在这个例子中,我们将 GUI 作为观察者 mock 出来,使用为这个例子创建的类-MyGameObserver。通过 mock 每个单元的协作者,我们可以保证示例与其他协作者没有耦合,包括那些还不存在的协作者。这让改变变得容易。

我们没有分数,我们不知道 X 是不是一直都先走,我们不知道有人获胜后应该如何-我们可能需要重启程序。我们只为这个场景、这个故事、这个特性集和这个愿景写了足够的代码。我们保持代码易于修改,这样就可以在以后为更多的场景和故事添加代码。

当我们完成时,我们就发布!

当所有的协作者都被真正实现时,我们就写完了这个场景的代码。

当一个故事中的所有场景完成时,我们就完成了这个故事。

当一个 theme 或特性集中的故事都完成时,我们就完成了这个特性集。

当涉众确认一个愿景的所有特性集都完成时,这个愿景就可以发布了。

在每个阶段,我们都努力尽快得到反馈

通过自动化示例和场景,我们能够发现代码是否能够工作。

通过自动化集成和部署到真实环境或相似的环境,我们能够发现是否有之前没有意识到的技术问题,或者没有考虑到的涉众。

通过尽快给涉众展示应用程序,我们能够发现我们是否可以完成愿景。

在敏捷和精益方法中,还有很多其他的反馈环。这意味着我们在开始工作时要假设可能会犯错误。我们不需要在一开始就保证每件事都是正确的-我们可以从“刚好够用”开始。

在每个阶段,我们都努力减少浪费

精益生产努力减少库存-那些还没有安装到可工作的汽车上的零件。用同样的方法,我们努力减少那些没有添加到可工作的特性集和愿景上的用户故事、场景、示例和代码。在看板系统中,信号被传递到每个阶段中,这样每个工作站就知道要为下一个工作站生产一些特定的东西。每个被生产出来的部分都是因为某个工 作站需要他们来满足客户的需要。这就是一个拉动系统。

在其他生产系统中,生产线上放着成盒的剩余零部件以备使用。人们需要围绕这些部件工作。这些部件需要维护、保护、空间等,如果商业需求的改变超过一定程度,这些部件就要被丢弃。

在一些软件项目中,我们也做了同样的事。我们创建特性集是因为我们有足够的预算。我们对详细分析那些我们相信会在 3 个月后需要的故事。我们为我们认为有趣的特性创建视图。我们创建数据库列和表来存储没有人会使用的信息,以及更多的视图来收集信息。所有这些都需要维护,需要和他们一起工作,而且在商业 需求改变时扔掉。

通过在需要使用时才生产需要的东西,精益生产线可以避免这些浪费。相似的,我们也可以在我们的项目中,通过只在需要时才创建来避免浪费。

这就是我们的拉动软件生命周期

最有价值的软件是还没有写出来的软件。
愿景促使涉众创建特性
特性促使 BA 创建用户故事
用户故事促使 QA 创建场景
场景促使 UI 专家设计 UI
UI 促使开发人员编写代码
代码促使开发人员编写更多的代码
我们做到能够支持下一件最重要的事
当我们完成时,就发布。
在每个阶段,我们都努力尽快得到反馈
在每个阶段,我们都努力减少浪费。

在生命周期中的每个角色都不需要一定是全职的-例如一个开发人员也可以做 BA 或 QA。在这种情况下,他会带上 BA 的帽子。如果一个人不考虑他将从下一个阶段中得到什么-例如代码-那么就几乎不可能得到他在下个阶段需要的东西-例如用户故事-因此让不同的人扮演这些角色还是值得的。

不管用什么方法论,总是有不止一种方式来实现同一个目标,不止一种方式来应用方法论,以及不止一种拉动系统适合你。

当 Dan North 发明 BDD 时,他说:“程序员想知道从何处开始,测试什么不测试什么,一次测试多少,如何为测试命名,以及理解测试失败的原因。”

这个概括应该能让你知道从何处入手,什么好用什么不好用,一次应该做多少,如何称呼正在做的事,以及如何在软件生命周期的每个层次获得反馈;这样不管编写什么软件,都是好的软件。

查看英文原文 Pulling Power: A New Software Lifespan


感谢刘申对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2009-11-25 00:113599
用户头像

发布了 63 篇内容, 共 23.4 次阅读, 收获喜欢 1 次。

关注

评论

发布
暂无评论
发现更多内容

maven打包,常用启动方式

秋天

mavne

会声会影最新版:会声会影2021中文版它来啦!

奈奈的杂社

视频剪辑 视频后期 自媒体 视频处理 会声会影

oktoken跟单社区系统开发|oktoken跟单社区APP软件开发

系统开发

源码解读:KubeVela 是如何将 appfile 转换为 K8s 特定资源对象的

阿里巴巴云原生

容器 云原生 k8s API 应用服务中间件

K8s 原生 Serverless 实践:ASK 与 Knative

阿里巴巴云原生

Serverless 容器 云原生 k8s 存储

MapReduce的运行机制详解

五分钟学大数据

hadoop 3月日更

引入单点登录,需要考虑哪些问题?

龙归科技

SSO 单点登录

oktoken对冲合约软件APP开发|oktoken对冲合约系统开发

系统开发

缓存为什么会被污染?

escray

redis 学习 极客时间 3月日更 Redis 核心技术与实战

【操作系统】存储器管理

学Java的猪猪侠

何止一个惨字形容!水滴Java面试一轮游,壮烈了,问啥啥不会,数据库血崩,我该怎么办?

钟奕礼

Java 学习 编程 程序员 面试

重点人员可视化研判分析系统搭建,可视化大屏系统

JVM 诊断之 jps 工具使用

hepingfly

JVM jvm调优 jvm诊断 jps

又是一些小细节!3面成功入职字节跳动:算法+数据库+中间件+JVM

Java架构之路

Java 程序员 架构 面试 编程语言

Flink程序优化及反压机制

大数据技术指南

flink 3月日更

硬件测试的思考和改进:有道词典笔的高效测试探索

有道技术团队

大前端

中国唯一入选 Forrester 领导者象限,阿里云 Serverless 全球领先

阿里巴巴云原生

阿里云 Serverless 容器 开发者 云原生

一年增加 1.2w 星,Dapr 能否引领云原生中间件的未来?

阿里巴巴云原生

容器 微服务 云原生 k8s 中间件

操作系统--虚拟存储器概述

学Java的猪猪侠

后端服务器网络编程之 IO 模型

Linux服务器开发

后端 网络编程 web服务器 Linux服务器开发 网络io

2021面试跳槽宝典:BATJ大厂核心面试解析600题

比伯

Java 架构 面试 程序人生 计算机

浅论指针(一)

Integer

c c++ 指针

Apache Iceberg学习日志

InfoQ_Springup

数据湖

关系数据理论是个什么牛马

学Java的猪猪侠

字节跳动5面喜提offer!分享给朋友们面试感受

Java架构之路

Java 程序员 架构 面试 编程语言

爆肝一周总结了一份Java学习/面试自测指南!200+道Java最常见面试题。

Java架构之路

Java 程序员 架构 面试 编程语言

2021版金三银四Java面试突击手册开源(涵盖p5-p8技术栈),“吊打”面试官的“葵花宝典”

Java 编程 程序员 架构 面试

「产品经理训练营」第八章作业

Sòrγy_じò ぴé

支付宝高级研发一二三面题目:CMS+CAS+线程锁+事务+雪崩+Docker

钟奕礼

Java 编程 程序员 架构 面试

怎样从零开始设计一个数据库

学Java的猪猪侠

技术中台之DevOps动态表单体系构建

EAWorld

拉动的力量:一种新的软件生命周期_研发效能_Elizabeth Keogh_InfoQ精选文章