敏捷开发 – LigaAI 团队博客 https://ligai.cn/blog 以人工智能,赋能项目管理 Tue, 05 Mar 2024 06:53:02 +0000 zh-CN hourly 1 https://wordpress.org/?v=5.8.4 https://ligai.cn/blog/wp-content/uploads/2021/02/logo_图形-150x150.png 敏捷开发 – LigaAI 团队博客 https://ligai.cn/blog 32 32 产品管理经验分享:删掉 500 个产品待办事项后,我逃离了「假敏捷」 https://ligai.cn/blog/alige/1272.html Thu, 10 Aug 2023 03:28:30 +0000 https://ligai.cn/blog/?p=1272 阅读更多]]> 文章开始之前,我想先请大家思考几个问题:

  • 你的产品待办列表中有多少项工作?
  • 其中最早的待办事项是什么时候创建的?
  • 你和 Scrum 团队多久会维护一次列表中那些从没进过迭代的「钉子户」事项?

我第一次问自己时,得到的答案是这样的:

  • 产品待办列表中有 450 个待办事项;
  • 最早的一项在三年零七个月前创建;
  • 至少有 100 个事项被完善和评估,却从未被规划进迭代。

我开始反思产品待办列表(Product Backlog)和产品待办事项(Product Backlog Item)的奇怪现象,随后确定了一件事情:我没有理解「敏捷」的真正含义

现在我将与你分享,为什么清理(甚至删除)产品待办列表可能让你拥抱更自由的敏捷。

01 笨重的产品待办列表是敏捷的劲敌

你能立刻说出「敏捷」的含义吗?

一千个人眼中有一千个哈姆雷特,而我的理解是:敏捷要更快地向用户和业务提供价值

对于抽象的「价值」,大家或许也会有不同的解读。于我而言,「提供价值」意味着在帮助用户解决问题的同时,为业务带来回报

从容地面对未知是践行敏捷的关键。

追本溯源,敏捷强调拥抱变化,在变化中学习。我们应该简单地创建假设、验证假设、学习、检查并调整。这听上去并不复杂,但不知何故,很多人都把它变得无比复杂,包括我自己。

庞大的、笨重的产品待办列表恰恰是敏捷的反面。我猜你可能会反驳说自己很敏捷,但你是不是

  • 让事项在产品待办列表中呆了很多年?
  • 不敢删除任何待办事项,唯恐惹恼干系人?
  • 同开发人员一起浪费大量时间处理一些永远不被排进迭代的需求?

我认为,任何有超过三个迭代工作量的产品待办列表都是笨重的。如果产品待办事项的数量比 Scrum 团队几个迭代的工作量还要多,那就说明团队当前「拥抱计划 > 拥抱变化」。那这到底是敏捷呢?还是瀑布呢?

要想实现价值,就必须维护一个精益的产品待办列表。不要被计划的假象所迷惑。

02 大胆地删除产品待办事项

作为一名产品负责人,再没有什么比笨重的产品待办列表更能让我恐慌了。无条件地向利益相关者承诺交付,美其名曰「客户至上」;但事实上,在现存的所有产品待办事项中,有近乎一半的承诺难以兑现——它们始终在待办列表中占有一席之地,被「计划中」完美掩护。产品负责人换了一个又一个,它们却一直没有被交付和满足。

一个无限制的、庞大而笨重的待办清单让我们永远无法兑现所有的承诺。这也是一种无法持续管理产品待办事项的坏方法。

不要用「把需求放进产品待办列表」的方式,愚弄利益相关者。

我先后在多个组织担任过产品负责人,在很长的一段时间里,我都在以一种低效的、甚至可以说是毫无意义的方式适应新工作。我之前的做法是:

  • 阅读整个产品待办列表;
  • 接触关键干系人,了解每个事项背后的需求;
  • 结合交流结果,丰富产品待办事项;
  • 确定事项优先级,为产品待办列表排序。

这样做的结果是,我浪费了大量的时间,还给自己带来了更多来自不同干系人的压力。每个人都急切地想要一些东西,但没人愿意把自己的需求从产品待办列表中删除。

这是一个很常见的错误:让利益相关者掌握主动权,而不是自己把控产品方向。

现在,我会先做这些事:

  • 理解产品战略;
  • 清理/删掉产品待办列表;
  • 定义要验证的假设;
  • 创建与战略相关的事项,重建列表。

你一定在想:把产品待办列表删掉也太激进了!

是的,你说得对。但是,为了更快地交付价值,我们必须采用非常规的,乃至极端的办法。除非能消除所有干扰,否则你没有时间去做最重要的事。

我删掉了利益相关者想要的需求,他们会生气吗?肯定会啦,但是这跟他们发现产品无法达到预期而发的脾气可没法比。

再说一个秘密吧:我曾经一次性删掉了大约 500 个产品待办事项,最后只有 2 位利益相关者向我提出了疑问,而其他人没有任何反馈。我的经验是,如果你申请删除某个事项,大概率会被拒绝;但如果愿意冒一次险,那你可能会收获意外之喜。

03 没有冲突,敏捷就枯萎了

做对产品有利的事情很难不惹人生气。因为我们无法通过取悦所有人,更快地交付价值。正确地做产品一定需要面对冲突和压力,而处理冲突的能力又将决定我们是否是合格的产品负责人。

同生活中的任何事情一样,短期利好很可能是靠牺牲长期利益实现的。

如果产品待办列表能完美地符合利益相关者的期望,他们会在一开始时非常高兴,但久而久之逐渐失望,因为团队无法实现他们的预期。如若在最开始就选择那条艰难的路,选择拥抱冲突来实现承诺,那你将可以带领 Scrum 团队交付价值,而不是陷入 WaterScrumFall 的错误模式。

为了确保自己不会陷入「有效性错觉」,请每 3 个月清理一次产品待办列表,为新事物腾出空间,让噪音消失;也为 Scrum 团队留出时间复盘学习成果,评估当前的目标和意义,重新开始。

最后,有效性错觉和技能错觉是由一种强大的专业文化来支撑的。我们知道,在任何情况下,当身边的人都跟自己持同样的想法时,不论这种想法有多么荒唐,人们都能保持一种不可动摇的信念。——《思考,快与慢》,丹尼尔·卡尼曼

# LigaAI 总结

敏捷强调要拥抱变化,拥抱学习。无限制的、笨重的产品待办列表会使组织无法快速响应变化,而无法如约交付承诺也会让利益相关者越行越远。

定期清理产品待办列表,维护组织价值交付的敏捷性,始终关注最重要的事情,才能让企业和组织保持活力,一往无前。

(原文作者为 David Pereira,内容经 LigaAI 翻译整理。)


>> LigaAI 往期精彩阅读 <<

如何用 NPS 确定研发优先级,打破技术与业务的次元壁?

这 4 个系统可靠性评估指标,可能比 MTTR 更靠谱!

LigaAI:从效率、度量和价值维度,成为研发团队的智能医生

高绩效团队的 5 个优秀习惯,看看你占了几个?

了解更多开发者提效、研发效能管理、前沿技术等消息,欢迎关注 LigaAI。欢迎体验我们的产品,期待与你一路同行!

]]>
Liga译文 | 管理研发团队后,我发现用「速率」做度量错得离谱…… https://ligai.cn/blog/%e7%a0%94%e5%8f%91%e7%ae%a1%e7%90%86/1192.html Mon, 27 Mar 2023 02:26:58 +0000 https://ligai.cn/blog/?p=1192 阅读更多]]> 一旦你开始了解敏捷开发和 Scrum 方法,就一定会碰到「速率 Velocity」。它表示研发团队在一个迭代周期内,能完成的所有故事点数之和;常用作度量基准,辅助长期的工作估算和迭代规划。

几年后,当我在一个优秀的软件工程师团队担任管理者,我才意识到「速率」在实际度量时存在很大的缺陷。也正因如此,我才得以找到真正正确的研发效能度量指标。

01 为什么「速率」不好用?

让我们从速率的计算公式开始:

  • 实际速率 = 完成的总点数 / 迭代次数
  • 预期速率 = 估算产生的总点数 / 迭代次数(估算故事点数即被添加到迭代中的故事点数)

在管理实践中,大多数团队会选用「实际速率」,所以本文也围绕它展开说明。那么,实际速率在使用中具体存在哪些缺点?

1. 无法展示浮动空间

实际速率在数值上无法展示研发团队实际完成工作量的浮动情况。 下面是点数定义相同的两个团队在四个迭代内分别完成的故事点数统计:

从结果上看,两个团队的速率值都是 20, 但我们能说「两个团队都能在一个迭代内完成 20 个点的工作」吗?

对第二个团队或许可行,因为它的迭代点数浮动很小(± 2 个点),但第一个团队的变化就大得多(± 18 个点)。如果只看实际速率值,管理者其实无法了解和掌握研发团队的稳定性。

更进一步地,也无法准确地估算待开发的故事和任务的工作量,或者为史诗(Epic)拟定一个预计发布日期。

2. 不能灵活适应变化

研发团队和需求经常会发生变化,成员出勤率、人员变动、紧急 Bug 修复、企业培训等等都会影响实际可用资源。

但是,实际速率是基于理想的团队平均运转能力计算的。如果迭代期间有成员休假外出,那团队能否完成所有的故事?这对速率又会产生怎样的影响?团队还能准确地估算开发容量吗?

同样的,如果团队迎来一名新成员,那实际速率会变大吗?还是由于我们需要为新成员提供培训,该迭代的研发速度其实会变慢?需不需要重新评估待办列表?这些我们都毫无头绪。

3. 估算本身不准确

用速率管理研发效能难有成效的原因还在于,它依赖于故事点数——一个被人为定义的、很难在团队内部达成统一共识的估值。 同时,研发团队也很难保证跨迭代的点数衡量标准一致,这也是当前工作估算的头号难题。

如果不能用相对标准和准确的方式估算研发工作,就很难维持稳定的开发速率。这不止会影响后续迭代的管理,也限制了估算精度的检验和改进。

4. 成员会精疲力竭

最后,基于速率值设定团队的迭代目标不可避免地会让成员倍感疲惫。

相信很多团队都出现过追赶截止日期,紧急交付的情况。在临近交期的短时间内,成员们超负荷工作,每天工作 15 个小时再加上周六、周日无休,尽可能完成所有的待办事项,以达成迭代目标。

我们都不希望类似事件发生,但不可否认的是,「极限挑战」状态下的团队速率确实得到了提高。那么,在下一个迭代规划时,研发团队是否可以接受比现在更多的工作量?长此以往,工作量内卷一定会让成员们疲惫不堪。

速率不该被用来设定团队目标,而应该被管理者用来设定绩效先例并预判未来价值。

既然速率不可行,那应该用什么指标代替它度量和管理呢?

02 正确的管理指标:承诺方差

使用承诺方差(Commitment Variance,即 CV) ,它有助于增强团队自组织和提升自驱力。其计算公式如下:

  • PointsCompleted-完成总点数:上一个迭代中,研发团队成功交付的故事点数。
  • PointsCommitted-承诺总点数:团队在迭代计划中承诺能完成的故事点数,可用被添加到迭代待办列表中的点数之和表示。

1. 指标管理目标

使用承诺方差时,研发团队要尽可能准确地估算研发工作量,并使用相同的标准承诺一个完成目标。

而优化承诺方差的目标是尽可能将其绝对值降为 0;团队要努力完成所有任务,并使燃尽图在迭代结束时变为 0。

2. 结果解读

如果承诺方差的值

  • 大于 0,说明超额完成目标。 团队可以根据承诺值和迭代过程,决定是否提高下一迭代的承诺值;或者结合迭代复盘,分析超额交付的具体原因,例如故事点数被高估、有计划外的人手增加等等。
  • 小于 0,说明承诺预期过高。 基于当前的数值基准,剖析过度承诺的原因,在下一个迭代中重新调整。
  • 等于 0,意味着团队能够准确地估算研发任务并评估交付能力。 保持这个节奏,向前冲吧!

3. 优势分析

用承诺方差代替速率管理研发交付能力,团队会得到以下收获:

  • 结合每个迭代的实际情况,灵活地制定迭代承诺和目标。
  • 正确定义故事点数,专注准确的工作估算。
  • 正确地评估团队交付力,有的放矢地设立承诺和目标。
  • 围绕自设定的承诺,调整工作状态,减少迭代冲刺中疲劳的风险。

对团队管理者而言,承诺方差也同样意义非凡,主要体现在:

  • 有机会与团队合作,共同完成新功能和/或产品复杂性的估算,获得安全感和信心。
  • 向利益相关者提供更准确的预计交付期限。
  • 放心授权成员自组织,激励团队自驱成长。

4. 潜在风险

当然,使用承诺方差管理也存在一些潜在风险。

  • 自我施压和内卷/内耗。 一旦成员(们)将交付工作量与绩效评估等联系起来,就可能产生过大的内部/个人压力,过度承诺和过度交付。管理者需要创造一个充满安全感的环境,避免成员们内耗;也要敏锐识别过度承诺,以维护团队长期健康的稳定发展。
  • 故意减少承诺。 有些团队可能会人为地减小承诺值或只完成承诺部分的工作。管理者需要甄别伪模式的存在,避免资源浪费。

一个小建议:可以先使用承诺方差管理工作,在建立相对准确的估算标准后,再尝试用速率设立能力基准,在承诺方差的基础上建立提速缓冲区。

03 案例分析

下面我们通过示例,进一步讲解承诺方差的实际应用。还是开头提到的两个团队,假设他们现在使用承诺方差来完成任务估算和能力评估。

上表中,团队「迭代实际完成的平均工作量」就是实际速率。两个团队「平均承诺的故事点数」都非常接近 22(± 0.25)个点,但实际完成量却非常不同。

第一个团队在使用承诺方差后发现,当前定义的「点数」无法支持正确的工作量估算和能力评估。

于是在第四个迭代中,他们调整了「一点数工作」的定义并在内部达成共识。由于度量颗粒度变细,他们给出 28 个点的承诺值(尽管数据显示,他们在上个迭代中只完成了 12 个点)。

通过绘制两个团队的承诺方差趋势图,可以看到,优化故事点数也是一个持续改进的过程。

04 LigaAI 总结

基于相同的点数标准,承诺方差将工作量估算与能力评估有机结合,解决了速率管理中存在的灵活性和准确性不足的问题。

承诺方差的管理目标是使其绝对值尽可能降为 0。既不过度承诺,让团队耗费心力,也要避免承诺低估,造成浪费资源。

(原文作者:Michel C;文章出处:Medium)


>> LigaAI 往期精彩阅读 <<

前端进阶:如何在 Web 中使用 C++?

如何科学管理技术团队的研发交付速率?

从 Netflix 传奇看,结果导向的产品路线图如何制定?(上篇)

Outcome VS. Output:研发效能提升中,谁会更胜一筹?

击碎增长瓶颈,LigaAI 将持续分享研发效能度量体系的搭建经验,以及科学的度量指标管理方法。了解更多效能优化与增长干货,欢迎关注我们的团队博客或点击 LigaAI-智能研发协作平台,在线申请体验我们的产品。

]]>
2022年度总结 | 这一年,我们写了10万字 https://ligai.cn/blog/team/1158.html Fri, 06 Jan 2023 04:06:55 +0000 https://ligai.cn/blog/?p=1158 阅读更多]]> 大开大合中,2022 年正式落下帷幕。

2022年,LigaAI 一共发布了 62 篇文章,累计超过 10 万字 。2023 年的第一篇文章,我们希望与你一起回溯、共享过去一年的收获与成长。

01 敏捷,是进步的价值观

LigaAI 坚信当互联网红利散去,只有坚定而灵活地拥抱变化、适应变化的组织,才能在风云变幻中走得长远。这也是敏捷价值观赋予团队的生命力。

可惜的是,在全员卷王的干扰和影响下,许多研发团队错将「快速开发」当做「敏捷开发」,误把「增量瀑布」视作「迭代开发」,才让「实践中进步」的敏捷之道饱受唱衰之词。

过去一年中,LigaAI 围绕敏捷方法论和实践分享,整理、编译了许多文章,也帮助了很多团队逐渐探索出组织的「正大光明路」。其中,「敏捷四会」系列文章更是受到了大家的一致好评。

以正确的、进步的敏捷价值观,赋能更多研发团队,LigaAI 一直在努力。

#正确的敏捷

《敏捷真的是开发者的绊脚石吗?》

《敏捷开发真的过时了么?》

#敏捷四会

《高效的「迭代计划会议」怎么开?》

《每日站会能不能取消?》

《分布式团队的高效站立会说明书》

《Sprint Review是不是功能演示会?》

《迭代评审会的七宗罪,你知道吗?》

《如何在迭代回顾会上「知无不言」?》

02 一月一度,妙谈相见

在实践和分享中成长。2022 年 6 月,LigaAI 正式启动了 #大咖妙谈# 专栏。借此机会,我们有幸与许多研发管理大佬、敏捷实战专家展开交流,并将他们宝贵的可复制经验分享给大家。

过去一年,我们曾聊过敏捷团队构建、程序员成长经历、研发效能提升,AI 技术生产力,以及如何更好地响应用户反馈。希望这些内容能让正在勇敢践行敏捷的你,充满底气与信心。

#Liga妙谈

第一期:《自组织的敏捷团队如何搭建?》

第二期:《程序员如何实现个人能力提升?》

第三期:《研发效能管理的关键是什么?》

第四期:《AIGC火了,人类又得到了什么?》

第五期:《如何高效甄别、快速响应用户反馈?》

P.S.:新的一年,如果你有任何研发效能相关的问题想让 LigaAI 和各路开发大佬一起聊聊,欢迎随时给我们留言建议!

03 技术成长,扎根于热爱与分享

作为一个开发成员占比超过九成的团队,LigaAI 深刻地感受到技术伙伴的热爱,不止存在一行行斑斓的代码之间,也浸透在一篇篇记录和分享当中。

2022 年,LigaAI 的小伙伴们将学习、收获、成长沉淀在字里行间,优化、提升、进步体现在方方面面。在与志同道合的朋友们交流心得、共同进步的同时,LigaAI 的技术分享文章也很幸运地获得了许多开发者朋友的认可。在这里再次将技术文章分享给大家。

#组织效率

《影响研发效能的7个常见场景解读》

《可伸缩的研发流程管理方案分享》

《多测试环境的动态伸缩实践》

#分享成长

《为什么我的 ORDER BY create_time ASC 变成了 order by ASC?》

《领域驱动设计入门与实践[上]》

《领域驱动设计入门与实践[下]》

《Javaer 如何做单元测试?》

《多个服务器如何跨命名空间,访问公共服务?》

04 优秀的PO,可以成就敏捷

产品负责人(Product Owner)、Scrum Master 和研发团队,是构成敏捷团队不可或缺的三个角色。其中,产品负责人更是承担了极为重要的职能:需求分析、价值排序、规划排期、沟通调节……

好的产品负责人,是成功践行敏捷的开始。围绕产品管理和需求管理,LigaAI 打造了全方位、多维度的内容体系,让指导概念和工作方法真正落地下来,服务更多组织和团队轻松践行敏捷。

#产品管理

《浅析「产品思维」》

《产品管理全流程大揭秘》

《没有决策权,产品经理如何做好产品管理?》

《一文讲清「敏捷路线图」》

#需求分析

《为什么我们总是说不清「需求是什么」》

《怎样简洁明了地说清楚产品需求?》

《如何串连「语言工具」描述简洁清晰的需求?》

#优先级排序

《产品经理该如何确定优先级?》

《做优先级排序时使用最多的三个模型》

《分享一个优先级系数计算公式》

05 走一条「正确但艰难」的路

2022年,对 LigaAI 而言,意义非凡。

✨ 3月,我们公布了 A 轮融资消息,很荣幸我们的理念、产品、团队和赛道都得到投资人的认可;

✨ 4月,我们参加「PLG公司的机遇与挑战」圆桌会议,与实力同行共同探讨了 PLG 的更多可能;

✨ 7月,LigaAI 迎来 2 周年生日。在热闹非凡中,我们与越来越多的小伙伴携手迈向未来—— 2 The Future;

✨ 10月,LigaAI 受邀参加 2022 亚马逊云科技中国峰会,与更多关注效能的朋友们分享「智能协作」的魅力;

✨ 12月,我们正式启动「客户案例」计划,未来也将与更多优秀客户一起,探索更多 LigaAI 的缤纷打开方式。

致力于成为颠覆性的新一代智能研发协作平台,LigaAI 希望能让每一位开发者回归专注的价值创造,为每一个技术团队打造稳固的敏捷壁垒。这不容易,却也是一定要做的、正确的事。

#成长里程碑

《LigaAI完成A轮融资,加速打造全新的智能研发协作平台》

《如何借助「新一代智能协作」提升研发效能?》

《两周上线一个新产品,猴子无限怎么做的?》

#品牌故事

《对话LigaAI创始人周然:在研发SaaS赛道,「颠覆」Jira》

《PLG公司的机遇和挑战》

# 写在最后

变化是复杂而难测的。过去的一年,LigaAI 在内容分享上做了一些新的尝试,希望为正在尝试敏捷的朋友们带去一些确定性。

未来,我们希望能够打造智能化研发协作生态,在提升组织研发效能的同时,为企业增长创造更多确定性。

LigaAI 新一代智能研发协作平台,一直在路上。

了解更多敏捷开发、项目管理、行业动态等消息,关注我们的团队博客或点击 LigaAI-智能研发协作平台,在线申请体验我们的产品。

]]>
Liga译文 | 一文讲清「敏捷路线图」,不再掉入瀑布陷阱 https://ligai.cn/blog/pmo/1147.html Fri, 30 Dec 2022 06:50:54 +0000 https://ligai.cn/blog/?p=1147 阅读更多]]> 尽管许多组织和团队声称自己非常敏捷,但他们仍在使用瀑布的方式规划产品。为什么会这样?我们该如何改变这种「错误敏捷」?

原则上,践行敏捷开发很简单:构建一个增量测试这个增量了解需要改变的信息将信息反馈到第一步中,并重复步骤

整个过程可以从两个方面,将敏捷开发与瀑布开发彻底区分开:第一,尽早且频繁地交付小批量的可工作的产品第二,根据(一)得到的新变化和信息,对产品进行恰当的调整。

如下图所示的敏捷路线图很常见。时长一年的甘特图上堆满了功能,并提前完成了任务分配。研发团队只能在一个个不间断的迭代中,实现所有的功能;他们还没有机会总结已交付的工作并作出调整,就必须立刻进入下一个功能开发。

图1. 这样的甘特图怎么可能敏捷?

如果前两个产品功能交付后被证明是错误的(这非常有可能发生),难道我们还要按原计划迭代下去吗?继续遵循上面的甘特图,可能会一错到底。因为计划好的路线图无法帮助我们评估交付效果,识别问题并及时调整。

敏捷开发刻意只关注下一次迭代的即时计划,但许多团队构建了一年甚至更长时间的甘特图,并且罗列了无数个待开发功能和完成截止日期。这样怎么会敏捷呢?

01 前瞻性计划:敏捷刻意忽略的部分

除了当前迭代正在构建的产品外,敏捷方法论不太关注前瞻性的计划。因为只有这样才能做到「实时计划」——我们应该看到什么是可行/不可行的,并根据反馈的数据决定下一步该怎么做。

敏捷意味着「能够快速轻松地行动」,而敏捷开发需要被持续引导,逐步提供价值,再根据市场反馈的情况快速调整策略和方向。这是一个建立在洞察与反应几乎同步的快速反馈周期,而不是基于前期的大计划。

但是,组织(尤其是大型或传统组织)通常不喜欢没有计划地工作。没有计划的组织会陷入「计划缺失焦虑症」;更准确地说,组织的领导者会很焦虑。因此,他们会制定一个功能列表,提前做好分配,以确保每个人都能按预定的方式有序地工作。

敏捷的组织应当正面解决计划缺失的焦虑,但许多团队没有这样做。反而是领导者们认为,过去路线图和甘特图很有用,那就应该继续使用它们。慢慢地,敏捷就变形成「Water-Scrum-Fall」,就像下图这样。

图2. 迭代中的「敏捷式开发」仅是瀑布开发中很小的部分

不幸的是,敏捷的核心——灵活自由地根据新信息进行调整——被完全忽略了。许多团队实践的敏捷并不是真的敏捷,而过去的瀑布式任务列表也演变成了「用户故事」列表。

02 SAFe:敏捷适配器

敏捷框架没有提供任何当前迭代以外的具体规划建议,而扩展框架正填补了这一空白。SAFe 有一个优点:作为从前期瀑布式大计划到团队敏捷执行的适配器,它可以很容易地被理解。

使用 SAFe(或其他扩展框架),产品管理团队提出功能,设计团队绘制 UI 模型,再发送给管理层审批。当工程师开始编写代码时,管理层会很放心。因为他们已经做好了充分的计划,而成百上千名程序员都会尽职尽责地工作。通过敏捷扩展框架,管理人员可以看到所有计划中的功能,而开发人员可以在迭代中专注地写代码。

这也是敏捷扩展框架受欢迎的原因:它们将领导者熟悉的大型计划,转换为研发团队可执行的敏捷迭代。在一些高级管理者看来,这就是最重要的。事实上,工程团队已沦为「功能工厂」,他们几乎失去了所有学习、快速调整和改变的能力。管理者却真诚地相信,团队已经完成「敏捷转型」。

03 敏捷性需要空间来操作

回到前面提到的两个决定性敏捷特征:尽早且频繁地交付小批量的可工作的产品根据(一)得到的新变化和信息,对产品进行恰当的调整。

第一点比较好理解,几乎所有资料也都集中在这上面;能够正确掌握第二点的人要少很多。如果团队没有从早期部署的迭代中学习,也没有将洞察力融入后续的迭代优化,那么就没有正确地践行敏捷——这其实只是「增量瀑布」

正确的敏捷要求组织多次发布和重新发布相同的功能,并且每次都要从早期的经验中吸取教训,使该功能更易于使用、更强大、性能更好或在某些方面更好。这需要时间,而且这些时间无法在前期被充分估计和预先承诺。因此,要想成功地洞悉变化,完成迭代优化,团队需要在计划中做到以下三点。

  1. 计划实现的路径必须是可塑的。定义一个愿景或最终目标,但允许具体的功能和内容在执行时可以有所变化。我们无法准确预测一个功能会如何运转,所以需要测试它。敏捷团队要允许每个功能能被反复加工、打磨或扩展几次,直到真正实现它。
  2. 计划需要为调整和优化留出弹性空间。如果时间已经全部按计划分配完,就需要删掉一些东西。正确的策略是在一开始就不要填满所有时间,让它保持开放状态;可以为新想法整理一个新队列,直到时间允许,再进入迭代分配。后文的 「Now-Next-Later」也会详细讲解这一方法。
  3. 领导的支持。这通常是三点中最难实现的。

04 领导力是敏捷性的关键

最具影响力的成功因素是组织展示和激励的领导范式。—— Agile 2

很多领导层都认为敏捷是团队内部的事情。这种假设肯定是错误的,而领导们也需要以敏捷的方式行事。在项目过程中,如果要为团队提供调整的空间,领导者就要接受临时的计划。「可塑性强的路径」和「允许调整的弹性空间」印证了这点。

管理者要有足够的勇气和决心,才能对团队说:“我们不打算在这个迭代将你们的工作填满;你们需要跟着数据走,看看自己的工作成果。” 如果领导者要真正拥抱敏捷,他们就需要做出根本性的改变。这遵循精益创业的精神:建立一个项目、测试它、从数据中学习、并将这些经验直接反馈到后续的项目中

同时,领导者也要有勇气,接受这些事实:团队不会在外部干扰下提前确定工作,也不会一轮接一轮无缝地进行功能开发。团队需要更多实验性、创造性和跨职能的工作。

这同样意味着,领导者需要快速响应、批准通过更多碎片化的需求变更。他们要探索出更灵活地工作方法,以便为团队提供及时审批。这无疑会打破官僚主义的枷锁,而由领导者们组成的小团队则能更快、更独立地监督和批准自己团队的工作。

05 Now-Next-Later 模型

更常用的敏捷路线图工具是「Now-Next-Later」模型。甘特图中层层叠叠的功能集可以归纳总结为三个模块。

  • Now:我们正在积极工作的事情。
  • Next:「Now」完成后,即将开始的工作。
  • Later:其他所有未分配和未承诺的功能。

将新的需求、功能和工作按模块规划好,比挤进拥挤的甘特图要容易得多。我们可以很轻松地在每个模块中留出弹性容量空间,以便后续根据最新信息做出调整。

图3. 一个典型的「Now-Next-Later」路线图。

可以看到,「Now」中的项目定义得比「Next」更加详细,「Later」是定义和描述最少的。每个模块的时间跨度可以自由决定,但最好跨度大一些,尽量覆盖多个迭代和优化周期;建议的时间周期是每个模块 6 周或者一个季度

正确地使用「Now-Next-Later」,既能让我们适应短期变化,为后续工作和 Bug 修复留出空间,也能适应长期变化,改变我们对哪些功能要从模块中取出的想法。

但它不会消除干系人要求尽快交付功能的压力。这也是为什么领导者需要自己拥抱敏捷,才能让团队成功。领导者需要通过自己的努力,倡导敏捷的工作方式,并以同样的标准要求自己。

06 结论

许多自称敏捷的组织,他们提前计划了大量功能列表——通常提前一整年——并告诉团队要以小批量的方式交付。这不是敏捷,这是增量瀑布。

敏捷开发的核心特征是小批量地交付可工作的产品,从早期迭代中获得洞察力,并在后续迭代中进一步完善和优化相同的功能。其中的关键在于我们无法预知和确定什么是可行的,什么是行不通的;这需要洞察和跟踪数据。打造一款优秀的产品,我们要一边走,一边制定计划。这与一年长的甘特图完全不同。

「Now-Next-Later」只有与团队自主权相结合时才有意义。这样才能使团队发现计划的调整空间,及时做出应对。想要灵活地工作,在做计划时要注意建立高度灵活的路线图留出充足的弹性空间用于响应调整,以及获得领导层的积极支持。利用好这三点,就可以持续地引导项目,而不是在前期就将它固定下来。这就是敏捷真正的意义所在。

原文作者:Raj Nagappan

文章出处:Medium


>> LigaAI 往期精彩阅读 <<

多个服务器如何跨命名空间,访问公共服务?

半个月上线一个新产品,猴子无限是怎么做的?

如何基于GitHub Pages+Hexo,搭建个人博客?

多测试环境的动态伸缩实践

了解更多敏捷开发、项目管理、行业动态等消息,关注我们的团队博客或点击 LigaAI-智能研发协作平台,在线申请体验我们的产品。

]]>
技术分享 | 多个服务器如何跨命名空间,访问公共服务? https://ligai.cn/blog/sharing/1136.html Fri, 16 Dec 2022 03:57:35 +0000 https://ligai.cn/blog/?p=1136 阅读更多]]> 一、问题背景

在开发某个公共应用时,笔者发现该公共应用的数据是所有测试环境(假设存在 dev/dev2/dev3)通用的。

这就意味着只需部署一个应用,就能满足所有测试环境的需求;也意味着所有测试环境都需要调用该公共应用,而不同测试环境的应用注册在不同的 Nacos 命名空间。

二、两种解决方案

如果所有测试环境都需要调用该公共应用,有两种可行的方案。第一种,将该公共服务同时注册到不同的测试环境所对应的命名空间中。

第二种,将公共应用注册到单独的命名空间,不同的测试环境能够跨命名空间访问该应用。

三、详细的问题解决过程

先行交代笔者的版本号配置。Nacos 客户端版本号为 NACOS 1.4.1;Java 项目的 Nacos 版本号如下。

最初想法是将该公共应用同时注册到多个命名空间下。在查找资料的过程中,团队成员在 GitHub 上发现了一篇类似问题的博客分享:Registration Center: Can services in different namespaces be called from each other? #1176

01 注册多个命名空间

从该博客中,我们看到其他程序员朋友也遇到了类似的公共服务的需求。在本篇文章中,笔者将进一步分享实现思路以及示例代码。

说明:以下代码内容来自用户 chuntaojun 的分享。

shareNamespace={namespaceId[:group]},{namespaceId[:group]} 
@RunWith(SpringRunner.class)
@SpringBootTest(classes = NamingApp.class, properties = {"server.servlet.context-path=/nacos"},
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SelectServiceInShareNamespace_ITCase {

    private NamingService naming1;
    private NamingService naming2;
    @LocalServerPort
    private int port;
    @Before
    public void init() throws Exception{
        NamingBase.prepareServer(port);
        if (naming1 == null) {
            Properties properties = new Properties();
            properties.setProperty(PropertyKeyConst.SERVER_ADDR, "127.0.0.1"+":"+port);
            properties.setProperty(PropertyKeyConst.SHARE_NAMESPACE, "57425802-3058-4507-9a73-3229b9f00a36");
            naming1 = NamingFactory.createNamingService(properties);

            Properties properties2 = new Properties();
            properties2.setProperty(PropertyKeyConst.SERVER_ADDR, "127.0.0.1"+":"+port);
            properties2.setProperty(PropertyKeyConst.NAMESPACE, "57425802-3058-4507-9a73-3229b9f00a36");
            naming2 = NamingFactory.createNamingService(properties2);
        }
        while (true) {
            if (!"UP".equals(naming1.getServerStatus())) {
                Thread.sleep(1000L);
                continue;
            }
            break;
        }
    }

    @Test
    public void testSelectInstanceInShareNamespaceNoGroup() throws NacosException, InterruptedException {
        String service1 = randomDomainName();
        String service2 = randomDomainName();
        naming1.registerInstance(service1, "127.0.0.1", 90);
        naming2.registerInstance(service2, "127.0.0.2", 90);

        Thread.sleep(1000);

        List<Instance> instances = naming1.getAllInstances(service2);
        Assert.assertEquals(1, instances.size());
        Assert.assertEquals(service2, NamingUtils.getServiceName(instances.get(0).getServiceName()));
    }

    @Test
    public void testSelectInstanceInShareNamespaceWithGroup() throws NacosException, InterruptedException {
        String service1 = randomDomainName();
        String service2 = randomDomainName();
        naming2.registerInstance(service1, groupName, "127.0.0.1", 90);
        naming3.registerInstance(service2, "127.0.0.2", 90);

        Thread.sleep(1000);

        List<Instance> instances = naming3.getAllInstances(service1);
        Assert.assertEquals(1, instances.size());
        Assert.assertEquals(service1, NamingUtils.getServiceName(instances.get(0).getServiceName()));
        Assert.assertEquals(groupName, NamingUtils.getServiceName(NamingUtils.getGroupName(instances.get(0).getServiceName())));
    }

}

进一步考虑后发现该解决方案可能不太契合当前遇到的问题。公司目前的开发测试环境有很多个,并且不确定以后会不会继续增加。

如果每增加一个环境,都需要修改一次公共服务的配置,并且重启一次公共服务,着实太麻烦了。倒不如反其道而行,让其他的服务器实现跨命名空间访问公共服务。

02 跨命名空间访问

针对实际问题查找资料时,我们找到了类似的参考分享《重写 Nacos 服务发现逻辑动态修改远程服务IP地址》

跟着博客思路看代码,笔者了解到服务发现的主要相关类是 NacosNamingService, NacosDiscoveryProperties, NacosDiscoveryAutoConfiguration

然后,笔者将博客的示例代码复制过来,试着进行如下调试:

@Slf4j
@Configuration
@ConditionalOnNacosDiscoveryEnabled
@ConditionalOnProperty(
        name = {"spring.profiles.active"},
        havingValue = "dev"
)
@AutoConfigureBefore({NacosDiscoveryClientAutoConfiguration.class})
public class DevEnvironmentNacosDiscoveryClient {

    @Bean
    @ConditionalOnMissingBean
    public NacosDiscoveryProperties nacosProperties() {
        return new DevEnvironmentNacosDiscoveryProperties();
    }

    static class DevEnvironmentNacosDiscoveryProperties extends NacosDiscoveryProperties {

        private NamingService namingService;

        @Override
        public NamingService namingServiceInstance() {
            if (null != this.namingService) {
                return this.namingService;
            } else {
                Properties properties = new Properties();
                properties.put("serverAddr", super.getServerAddr());
                properties.put("namespace", super.getNamespace());
                properties.put("com.alibaba.nacos.naming.log.filename", super.getLogName());
                if (super.getEndpoint().contains(":")) {
                    int index = super.getEndpoint().indexOf(":");
                    properties.put("endpoint", super.getEndpoint().substring(0, index));
                    properties.put("endpointPort", super.getEndpoint().substring(index + 1));
                } else {
                    properties.put("endpoint", super.getEndpoint());
                }

                properties.put("accessKey", super.getAccessKey());
                properties.put("secretKey", super.getSecretKey());
                properties.put("clusterName", super.getClusterName());
                properties.put("namingLoadCacheAtStart", super.getNamingLoadCacheAtStart());

                try {
                    this.namingService = new DevEnvironmentNacosNamingService(properties);
                } catch (Exception var3) {
                    log.error("create naming service error!properties={},e=,", this, var3);
                    return null;
                }

                return this.namingService;
            }
        }

    }

    static class DevEnvironmentNacosNamingService extends NacosNamingService {

        public DevEnvironmentNacosNamingService(Properties properties) {
            super(properties);
        }

        @Override
        public List<Instance> selectInstances(String serviceName, List<String> clusters, boolean healthy) throws NacosException {
            List<Instance> instances = super.selectInstances(serviceName, clusters, healthy);
            instances.stream().forEach(instance -> instance.setIp("10.101.232.24"));
            return instances;
        }
    }

}

调试后发现博客提供的代码并不能满足笔者的需求,还得进一步深入探索。

但幸运的是,调试过程发现 Nacos 服务发现的关键类是 com.alibaba.cloud.nacos.discovery.NacosServiceDiscovery,其中的关键方法是 getInstances()getServices(),即「返回指定服务 ID 的所有服务实例」和「获取所有服务的名称」

也就是说,getInstances() 方法进行重写肯定能实现本次目标——跨命名空间访问公共服务

/**
 * Return all instances for the given service.
 * @param serviceId id of service
 * @return list of instances
 * @throws NacosException nacosException
 */
public List<ServiceInstance> getInstances(String serviceId) throws NacosException {
        String group = discoveryProperties.getGroup();
        List<Instance> instances = discoveryProperties.namingServiceInstance()
                        .selectInstances(serviceId, group, true);
        return hostToServiceInstanceList(instances, serviceId);
}

/**
 * Return the names of all services.
 * @return list of service names
 * @throws NacosException nacosException
 */
public List<String> getServices() throws NacosException {
        String group = discoveryProperties.getGroup();
        ListView<String> services = discoveryProperties.namingServiceInstance()
                        .getServicesOfServer(1, Integer.MAX_VALUE, group);
        return services.getData();
}

03 最终解决思路及代码示例

具体的解决方案思路大致如下:

1. 生成一个共享配置类NacosShareProperties,用来配置共享公共服务的 namespacegroup

2. 重写配置类 NacosDiscoveryProperties (新:NacosDiscoveryPropertiesV2),将新增的共享配置类作为属性放进该配置类,后续会用到;

3. 重写服务发现类 NacosServiceDiscovery (新:NacosServiceDiscoveryV2),这是最关键的逻辑;

4. 重写自动配置类 NacosDiscoveryAutoConfiguration,将自定义相关类比 Nacos 原生类更早的注入容器。

最终代码中用到了一些工具类,可以自行补充完整。

/**
 * <pre>
 *  @description: 共享nacos属性
 *  @author: rookie0peng
 *  @date: 2022/8/29 15:22
 *  </pre>
 */
@Configuration
@ConfigurationProperties(prefix = "nacos.share")
public class NacosShareProperties {

    private final Map<String, Set<String>> NAMESPACE_TO_GROUP_NAME_MAP = new ConcurrentHashMap<>();

    /**
     * 共享nacos实体列表
     */
    private List<NacosShareEntity> entities;

    public List<NacosShareEntity> getEntities() {
        return entities;
    }

    public void setEntities(List<NacosShareEntity> entities) {
        this.entities = entities;
    }

    public Map<String, Set<String>> getNamespaceGroupMap() {
        safeStream(entities).filter(entity -> nonNull(entity) && nonNull(entity.getNamespace()))
                .forEach(entity -> {
                    Set<String> groupNames = NAMESPACE_TO_GROUP_NAME_MAP.computeIfAbsent(entity.getNamespace(), k -> new HashSet<>());
                    if (nonNull(entity.getGroupNames()))
                        groupNames.addAll(entity.getGroupNames());
                });
        return new HashMap<>(NAMESPACE_TO_GROUP_NAME_MAP);
    }

    @Override
    public String toString() {
        return "NacosShareProperties{" +
                "entities=" + entities +
                '}';
    }

    /**
     * 共享nacos实体
     */
    public static final class NacosShareEntity {

        /**
         * 命名空间
         */
        private String namespace;

        /**
         * 分组
         */
        private List<String> groupNames;

        public String getNamespace() {
            return namespace;
        }

        public void setNamespace(String namespace) {
            this.namespace = namespace;
        }

        public List<String> getGroupNames() {
            return groupNames;
        }

        public void setGroupNames(List<String> groupNames) {
            this.groupNames = groupNames;
        }

        @Override
        public String toString() {
            return "NacosShareEntity{" +
                    "namespace='" + namespace + '\'' +
                    ", groupNames=" + groupNames +
                    '}';
        }
    }
}
/**
 * @description: naocs服务发现属性重写
 * @author: rookie0peng
 * @date: 2022/8/30 1:19
 */
public class NacosDiscoveryPropertiesV2 extends NacosDiscoveryProperties {

    private static final Logger log = LoggerFactory.getLogger(NacosDiscoveryPropertiesV2.class);

    private final NacosShareProperties nacosShareProperties;

    private static final Map<String, NamingService> NAMESPACE_TO_NAMING_SERVICE_MAP = new ConcurrentHashMap<>();

    public NacosDiscoveryPropertiesV2(NacosShareProperties nacosShareProperties) {
        super();
        this.nacosShareProperties = nacosShareProperties;
    }

    public Map<String, NamingService> shareNamingServiceInstances() {
        if (!NAMESPACE_TO_NAMING_SERVICE_MAP.isEmpty()) {
            return new HashMap<>(NAMESPACE_TO_NAMING_SERVICE_MAP);
        }
        List<NacosShareProperties.NacosShareEntity> entities = Optional.ofNullable(nacosShareProperties)
                .map(NacosShareProperties::getEntities).orElse(Collections.emptyList());
        entities.stream().filter(entity -> nonNull(entity) && nonNull(entity.getNamespace()))
                .filter(PredicateUtil.distinctByKey(NacosShareProperties.NacosShareEntity::getNamespace))
                .forEach(entity -> {
                    try {
                        NamingService namingService = NacosFactory.createNamingService(getNacosProperties(entity.getNamespace()));
                        if (namingService != null) {
                            NAMESPACE_TO_NAMING_SERVICE_MAP.put(entity.getNamespace(), namingService);
                        }
                    } catch (Exception e) {
                        log.error("create naming service error! properties={}, e=", this, e);
                    }
                });
        return new HashMap<>(NAMESPACE_TO_NAMING_SERVICE_MAP);
    }

    private Properties getNacosProperties(String namespace) {
        Properties properties = new Properties();
        properties.put(SERVER_ADDR, getServerAddr());
        properties.put(USERNAME, Objects.toString(getUsername(), ""));
        properties.put(PASSWORD, Objects.toString(getPassword(), ""));
        properties.put(NAMESPACE, namespace);
        properties.put(UtilAndComs.NACOS_NAMING_LOG_NAME, getLogName());
        String endpoint = getEndpoint();
        if (endpoint.contains(":")) {
            int index = endpoint.indexOf(":");
            properties.put(ENDPOINT, endpoint.substring(0, index));
            properties.put(ENDPOINT_PORT, endpoint.substring(index + 1));
        }
        else {
            properties.put(ENDPOINT, endpoint);
        }

        properties.put(ACCESS_KEY, getAccessKey());
        properties.put(SECRET_KEY, getSecretKey());
        properties.put(CLUSTER_NAME, getClusterName());
        properties.put(NAMING_LOAD_CACHE_AT_START, getNamingLoadCacheAtStart());

//        enrichNacosDiscoveryProperties(properties);
        return properties;
    }
}
/**
 * @description: naocs服务发现重写
 * @author: rookie0peng
 * @date: 2022/8/30 1:10
 */
public class NacosServiceDiscoveryV2 extends NacosServiceDiscovery {

    private final NacosDiscoveryPropertiesV2 discoveryProperties;

    private final NacosShareProperties nacosShareProperties;

    private final NacosServiceManager nacosServiceManager;

    public NacosServiceDiscoveryV2(NacosDiscoveryPropertiesV2 discoveryProperties, NacosShareProperties nacosShareProperties, NacosServiceManager nacosServiceManager) {
        super(discoveryProperties, nacosServiceManager);
        this.discoveryProperties = discoveryProperties;
        this.nacosShareProperties = nacosShareProperties;
        this.nacosServiceManager = nacosServiceManager;
    }

    /**
     * Return all instances for the given service.
     * @param serviceId id of service
     * @return list of instances
     * @throws NacosException nacosException
     */
    public List<ServiceInstance> getInstances(String serviceId) throws NacosException {
        String group = discoveryProperties.getGroup();
        List<Instance> instances = discoveryProperties.namingServiceInstance()
                .selectInstances(serviceId, group, true);
        if (isEmpty(instances)) {
            Map<String, Set<String>> namespaceGroupMap = nacosShareProperties.getNamespaceGroupMap();
            Map<String, NamingService> namespace2NamingServiceMap = discoveryProperties.shareNamingServiceInstances();
            for (Map.Entry<String, NamingService> entry : namespace2NamingServiceMap.entrySet()) {
                String namespace;
                NamingService namingService;
                if (isNull(namespace = entry.getKey()) || isNull(namingService = entry.getValue()))
                    continue;
                Set<String> groupNames = namespaceGroupMap.get(namespace);
                List<Instance> shareInstances;
                if (isEmpty(groupNames)) {
                    shareInstances = namingService.selectInstances(serviceId, group, true);
                    if (nonEmpty(shareInstances))
                        break;
                } else {
                    shareInstances = new ArrayList<>();
                    for (String groupName : groupNames) {
                        List<Instance> subShareInstances = namingService.selectInstances(serviceId, groupName, true);
                        if (nonEmpty(subShareInstances)) {
                            shareInstances.addAll(subShareInstances);
                        }
                    }
                }
                if (nonEmpty(shareInstances)) {
                    instances = shareInstances;
                    break;
                }
            }
        }
        return hostToServiceInstanceList(instances, serviceId);
    }

    /**
     * Return the names of all services.
     * @return list of service names
     * @throws NacosException nacosException
     */
    public List<String> getServices() throws NacosException {
        String group = discoveryProperties.getGroup();
        ListView<String> services = discoveryProperties.namingServiceInstance()
                .getServicesOfServer(1, Integer.MAX_VALUE, group);
        return services.getData();
    }

    public static List<ServiceInstance> hostToServiceInstanceList(
            List<Instance> instances, String serviceId) {
        List<ServiceInstance> result = new ArrayList<>(instances.size());
        for (Instance instance : instances) {
            ServiceInstance serviceInstance = hostToServiceInstance(instance, serviceId);
            if (serviceInstance != null) {
                result.add(serviceInstance);
            }
        }
        return result;
    }

    public static ServiceInstance hostToServiceInstance(Instance instance,
                                                        String serviceId) {
        if (instance == null || !instance.isEnabled() || !instance.isHealthy()) {
            return null;
        }
        NacosServiceInstance nacosServiceInstance = new NacosServiceInstance();
        nacosServiceInstance.setHost(instance.getIp());
        nacosServiceInstance.setPort(instance.getPort());
        nacosServiceInstance.setServiceId(serviceId);

        Map<String, String> metadata = new HashMap<>();
        metadata.put("nacos.instanceId", instance.getInstanceId());
        metadata.put("nacos.weight", instance.getWeight() + "");
        metadata.put("nacos.healthy", instance.isHealthy() + "");
        metadata.put("nacos.cluster", instance.getClusterName() + "");
        metadata.putAll(instance.getMetadata());
        nacosServiceInstance.setMetadata(metadata);

        if (metadata.containsKey("secure")) {
            boolean secure = Boolean.parseBoolean(metadata.get("secure"));
            nacosServiceInstance.setSecure(secure);
        }
        return nacosServiceInstance;
    }

    private NamingService namingService() {
        return nacosServiceManager
                .getNamingService(discoveryProperties.getNacosProperties());
    }
}
/**
 * @description: 重写nacos服务发现的自动配置
 * @author: rookie0peng
 * @date: 2022/8/30 1:08
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
@ConditionalOnNacosDiscoveryEnabled
@AutoConfigureBefore({NacosDiscoveryAutoConfiguration.class})
public class NacosDiscoveryAutoConfigurationV2 {

    @Bean
    @ConditionalOnMissingBean
    public NacosDiscoveryPropertiesV2 nacosProperties(NacosShareProperties nacosShareProperties) {
        return new NacosDiscoveryPropertiesV2(nacosShareProperties);
    }

    @Bean
    @ConditionalOnMissingBean
    public NacosServiceDiscovery nacosServiceDiscovery(
            NacosDiscoveryPropertiesV2 discoveryPropertiesV2, NacosShareProperties nacosShareProperties, NacosServiceManager nacosServiceManager
    ) {
        return new NacosServiceDiscoveryV2(discoveryPropertiesV2, nacosShareProperties, nacosServiceManager);
    }
}

本以为问题到这就结束了,但最后自测时发现程序根本不走 Nacos 的服务发现逻辑,而是执行 Ribbon 的负载均衡逻辑com.netflix.loadbalancer.AbstractLoadBalancerRule

不过实现类是 com.alibaba.cloud.nacos.ribbon.NacosRule,继续基于 NacosRule 重写负载均衡。

/**
 * @description: 共享nacos命名空间规则
 * @author: rookie0peng
 * @date: 2022/8/31 2:04
 */
public class ShareNacosNamespaceRule extends AbstractLoadBalancerRule {

    private static final Logger LOGGER = LoggerFactory.getLogger(ShareNacosNamespaceRule.class);

    @Autowired
    private NacosDiscoveryPropertiesV2 nacosDiscoveryPropertiesV2;
    @Autowired
    private NacosShareProperties nacosShareProperties;

    /**
     * 重写choose方法
     *
     * @param key
     * @return
     */
    @SneakyThrows
    @Override
    public Server choose(Object key) {
        try {
            String clusterName = this.nacosDiscoveryPropertiesV2.getClusterName();
            DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
            String name = loadBalancer.getName();

            NamingService namingService = nacosDiscoveryPropertiesV2
                    .namingServiceInstance();
            List<Instance> instances = namingService.selectInstances(name, true);
            if (CollectionUtils.isEmpty(instances)) {
                LOGGER.warn("no instance in service {}, then to get share service's instance", name);
                List<Instance> shareNamingService = this.getShareNamingService(name);
                if (nonEmpty(shareNamingService))
                    instances = shareNamingService;
                else
                    return null;
            }
            List<Instance> instancesToChoose = instances;
            if (org.apache.commons.lang3.StringUtils.isNotBlank(clusterName)) {
                List<Instance> sameClusterInstances = instances.stream()
                        .filter(instance -> Objects.equals(clusterName,
                                instance.getClusterName()))
                        .collect(Collectors.toList());
                if (!CollectionUtils.isEmpty(sameClusterInstances)) {
                    instancesToChoose = sameClusterInstances;
                }
                else {
                    LOGGER.warn(
                            "A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}",
                            name, clusterName, instances);
                }
            }

            Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose);

            return new NacosServer(instance);
        }
        catch (Exception e) {
            LOGGER.warn("NacosRule error", e);
            return null;
        }
    }


    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }

    private List<Instance> getShareNamingService(String serviceId) throws NacosException {
        List<Instance> instances = Collections.emptyList();
        Map<String, Set<String>> namespaceGroupMap = nacosShareProperties.getNamespaceGroupMap();
        Map<String, NamingService> namespace2NamingServiceMap = nacosDiscoveryPropertiesV2.shareNamingServiceInstances();
        for (Map.Entry<String, NamingService> entry : namespace2NamingServiceMap.entrySet()) {
            String namespace;
            NamingService namingService;
            if (isNull(namespace = entry.getKey()) || isNull(namingService = entry.getValue()))
                continue;
            Set<String> groupNames = namespaceGroupMap.get(namespace);
            List<Instance> shareInstances;
            if (isEmpty(groupNames)) {
                shareInstances = namingService.selectInstances(serviceId, true);
                if (nonEmpty(shareInstances))
                    break;
            } else {
                shareInstances = new ArrayList<>();
                for (String groupName : groupNames) {
                    List<Instance> subShareInstances = namingService.selectInstances(serviceId, groupName, true);
                    if (nonEmpty(subShareInstances)) {
                        shareInstances.addAll(subShareInstances);
                    }
                }
            }
            if (nonEmpty(shareInstances)) {
                instances = shareInstances;
                break;
            }
        }
        return instances;
    }
}

至此问题得以解决。

Nacos 上配置好共享 namespacegroup 后,就能够进行跨命名空间访问了。

# nacos共享命名空间配置 示例
nacos.share.entities[0].namespace=e6ed2017-3ed6-4d9b-824a-db626424fc7b
nacos.share.entities[0].groupNames[0]=DEFAULT_GROUP
# 指定服务使用共享的负载均衡规则,service-id是注册到nacos上的服务id,ShareNacosNamespaceRule需要写全限定名
service-id.ribbon.NFLoadBalancerRuleClassName=***.***.***.ShareNacosNamespaceRule

注意:如果 Java 项目的 nacos discovery 版本用的是 2021.1,则不需要重写 Ribbon 的负载均衡类,因为该版本的 Nacos 不依赖 Ribbon。

2.2.1.RELEASE 版本nacos discovery 依赖 Ribbon.

2021.1 版本nacos discovery 不依赖 Ribbon。

、总结

为了达到共享命名空间的预期,构思、查找资料、实现逻辑、调试,前后一共花费 4 天时间。成就感满满的同时,笔者也发现该功能仍存在共享服务缓存等可优化空间,留待后续实现。

五、参考文献

[1] Registration Center: Can services in different namespaces be called from each other? [EB/OL]. https://github.com/alibaba/nacos/issues/1176, 2019-05-07/2022-11-29.

[2] 重写Nacos服务发现逻辑动态修改远程服务IP地址 [EB/OL]. https://www.cnblogs.com/changxy-codest/p/14632574.html, 2021-04-08/2022-11-29.


>> LigaAI 往期精彩阅读 <<

半个月上线一个新产品,猴子无限是怎么做的?

如何基于GitHub Pages+Hexo,搭建个人博客?

多测试环境的动态伸缩实践

被忽悠入坑后,我如何让产品「起死回生」?

了解更多敏捷开发、项目管理、行业动态等消息,关注我们的团队博客或点击 LigaAI-智能研发协作平台,在线申请体验我们的产品。

]]>
客户案例 | 半个月上线一个新产品,猴子无限是怎么做的? https://ligai.cn/blog/sharing/1126.html Thu, 08 Dec 2022 09:57:15 +0000 https://ligai.cn/blog/?p=1126 阅读更多]]> 5~10 人的小微型创业团队,需不需要专业的研发协作工具?

随着生产力工具的价值获得更广泛的认可,越来越多观点认为,组织结构精简、业务尚未成熟的小微型团队应该尽早引入专业研发协作工具,完成核心竞争力的蜕变

猴子无限也是如此。自创业第一天起,追求高效和敏捷的猴子无限就与LigaAI建立了深度合作关系。

这个不足 10 人却全员远程的研发团队,如何打造高效迭代的加速引擎?今天就随LigaAI一起了解。

01 LigaAI兼具高效研发和便捷协作

猴子无限是典型的小微远程团队:成员不足 10 人,各自分散在北京、美国和加拿大等地区。身处天南海北,大家只能通过线上沟通完成协作,而跨国家、跨时差的异步协同对工具提出了极高要求。

几经对比,猴子无限很快确定了兼具高效研发和便捷协同的LigaAI。

与多维表格类的产品相比,LigaAI聚焦开发者场景,提供更符合研发习惯的面板视图和功能模块,更有智能助理、IDE插件、Git集成等释放开发者精力,为研发协作全面提效;

同Jira类的传统研发协作工具相比,LigaAI以轻负担的产品体验优势突出重围。高度重视交互体验,采用超灵活的aPaaS级自定义组件,深度贴合团队需求,开箱即用,轻量易上手。

猴子无限CEO尹伯昊告诉我们,选对生产力工具会给团队带来「基因层」的提升和变化,而LigaAI能为小微创业团队注入价值驱动的敏捷核心。

02 信息透明研发协作才能事半功倍

规模精简的团队中,成员大多身兼数职。多项目、多模块的研发需求并行乃家常便饭。

为了保证研发协作不乱套,项目信息和进度状态一定要实时同步;对远程团队来说,提升信息透明度更是组织稳定运转的基本要求。

LigaAI连通研发全流程信息流,提供全员可见的【看板】视图,消除异步信息差。猴子无限将研发工作共享在看板之中,每位成员在管理研发任务时,都能及时掌握项目最新的整体进展;通过执行人分组还能查看其他伙伴的任务情况,进一步保证信息的透明与共享。

基于目标共识,随时拉齐进度共识才能更好地创造业务价值。猴子无限CEO尹伯昊说道,透明度是自组织内驱的助燃剂;只有将信息共享出来,才能让全体成员参与决策,成为产品和价值的主导者。

此外,异步协作最怕信息偏差和空耗等待。中国区成员对需求存有疑问,如何同美国小伙伴及时沟通,并有效避免理解误差?

LigaAI的【智能助理+飞书集成】提供了良好的解决方法。在故事详情评论区,成员可以以文字、图片、视频等形式,展开对话与讨论;

新的评论和留言会自动触达远程伙伴,确保信息的时效性;通过与飞书集成,成员无需登入系统,也能在第一时间收到相关通知并及时处理反馈,避免空耗和等待;

同时,在日常讨论中产生的新工作还可以【一键转代办】,快速同步、沉淀到LigaAI中,大小需求不错漏,真正实现「让事找人」。

小微团队克服研发协作和异步协同的难题,就要做到事无巨细、巨细无遗。

03 大胆探索极速陡转也能得心应手

创业团队在探索PMF过程中,总会遇上业务方向调整的情况。一旦发现产品定位不符合市场预期,或者挖掘到潜力巨大的市场新机遇,推翻产品、快速重置也在所难免。

借助灵活易用的LigaAI【路线图】,业务探索期的猴子无限仅用半个月,便完成了一次超高效的定位转型和产品重塑。

LigaAI提供【项目 – 史诗(需求集)- 故事】三级需求管理模型。猴子无限先为新的业务方向创建了新的项目,再将其中各个功能模块拆分为若干个史诗,再进一步细化拆解为模块下具体的用户故事;

随后,结合优先级、工作量等综合标准,将故事依次排入相应的开发迭代,形成迭代计划和项目里程碑。这样,整个产品重塑计划就可以一目了然。

进一步在LigaAI【工作列表】中,可以随时创建新项目、补充需求细节、丰富故事层级、明晰拆分任务,生成详尽版甘特图。由虚到实,逐步完成产品规划重置。

业务探索期,依靠LigaAI提供的可视化定位转型体验,猴子无限大大提高了业务确定性。

半个月上线一个产品,光有清晰的路线图还不够。被问到团队的「效率之魂」时,猴子无限特意强调了「小粒度需求管理」的重要性。

根据MVP原则,将史诗需求纵向切分成一个个可独立交付价值的小粒度需求,也是猴子无限在LigaAI公众号」了解到的敏捷方法。

将需求颗粒度拆小,每日会议便可以直接从看板和项目甘特图中,明确当前的项目进度和风险,及时调整后续安排,更灵活地应对内外部的变化。

让团队每日进度具象化,LigaAI能呈现给创业团队,最大的确定性。

04 饱和度管理高效迭代的续命术

小微团队,成员少而精。时间紧、任务重,更要格外重视「饱和度管理」

在高速成长期,任何超负荷运转或者闲置空转浪费,都会成为阻碍团队前进的效能瓶颈。尤其对分布式团队而言,想要齐头并进,就必须让所有人都处在健康的可持续研发状态当中。

那么,高度远程的猴子无限,如何了解大家的工作负载和工作状态,并提供恰当的帮助呢?

得益于小粒度的需求管理方法,伙伴们的每日工作量会被合理量化和预估。透过LigaAI【团队】视图,便能清楚了解每位成员的工作容量和当前/预估工作量。

结合可视化面板和智能预警,持续跟踪大家的工作负载情况;必要时候,介入合理的资源统筹和再分配,调整研发工作和计划,为团队注入源源不断的高效驱动力。

05 智能预警全速前进的护身符

小而美的研发团队想要稳固又高速地成长,既要埋头拉车,也要抬头看路。体验沉浸式高效研发的过程中,要时刻关注团队关键指标,及时洞察潜在风险,提前化解危机。

在LigaAI【仪表盘】中,猴子无限使用自定义的可视化组件,将重要指标一一呈现。数据实时更新,效率与成果一览无余;更有AI机器人提供智能风险预警,实时监控,异常数据无所遁形。

  • 燃起图实时展示研发成果,监控项目迭代效率,在增量与变量之间洞察进度风险;
  • 累积流图可以跟踪和预测项目的完成情况,识别不同开发阶段的异常风险;
  • 工作量对比图用于跟进组织成员的开发状态,有效维护可持续研发团队。

洞悉数据指标中的潜在风险,LigaAI为猴子无限的高速研发保驾护航。

# Liga总结

在充满不确定性的环境之中,迭代增速和业务增长就是最大的确定性。

LigaAI通过高度灵活的协作工具,为猴子无限等小微创业团队带来信息透明度、变化适应力和抗风险能力的提升,锻造高效、敏捷的驱动核心。


>> LigaAI 往期精彩阅读 <<

如何基于GitHub Pages+Hexo,搭建个人博客?

多测试环境的动态伸缩实践

被忽悠入坑后,我如何让产品「起死回生」?

研发效能应该如何管理与度量?

了解更多敏捷开发、项目管理、行业动态等消息,关注我们的团队博客或点击 LigaAI-智能研发协作平台,在线申请体验我们的产品。

]]>
Liga译文 | 被忽悠入坑后,我如何让产品「起死回生」? https://ligai.cn/blog/alige/1114.html Fri, 11 Nov 2022 03:49:10 +0000 https://ligai.cn/blog/?p=1114 阅读更多]]> 原文作者 | David Theil

文章来源 | Medium

我将在这篇文章与诸位分享,如何扭转产品管理过程,并获得真正的成功。而你只需花上 15 分钟,便能获得一名产品负责人用三年血泪史总结出的产品管理经验。

PART 1:夸夸其谈的创始人

刚加入这家初创公司担任产品负责人时,我非常激动和兴奋。在我入职的第一天,其中一位创始人向我介绍了公司愿景和产品,还点出了竞争对手的种种弱点,全程口若悬河,十分精彩。

但几天后,我发现所有声称的「成绩」都只停留在宣传层面,产品本身的进度并不明朗。震惊之余,我也意识到,想让产品获得真正的成功绝非易事。

PART 2:自欺欺人的谎言

入职几周后我再次意识到,原来我们一直处在幻想之中:我们日复一日地向彼此传递同样的谎言,并且毫不质疑这些信息是否真实、是否来自真实出现的事实。

我们经常说的谎言是这样的:

  • 这个功能对用户非常重要。
  • 我们需要完成此功能,然后用户才会喜欢我们的产品。
  • 这是我们产品的主要用户。
  • 用户需要这个和那个。

我们总是告诉对方这些「事实」并将它们奉为真理,但实际上,我们并不知道「用户是谁」以及「他们想要什么」。

PART 3:产品负责人的唯一职责

目前来看,在我之前没有人在做产品管理,也没有真正的产品负责人——虽然确实有人担任了该职位,工作内容也都被不同的成员分担。

但是,没有人提出「我们该如何构建正确的产品?」这个问题,而产品负责人的唯一职责就是要构建正确的产品。

PART 4:好的产品负责人总是「一无所知」

一个好的产品负责人总是持空杯心态,且自觉「一无所知」。要知道我们的所有主张和决定都只是假设,而我们要做的就是将其证伪或证实

Dan Olsen 的精益产品管理金字塔

我们的所有工作都以假设为基础。当一个假设在上述金字塔中的位置越低,想要纠正不正确假设引起的错误就越困难。

因此,为了确保我们在构建正确的产品,我们必须通过测试来确认或否定这些假设。

PART 5:倒金字塔型假设试验

几周后,我成功说服团队理解并接受:我们对用户和真正的需求一无所知,我们的产品是基于假设构建的,而我们必须通过试验来验证这些假设。

产品的假设试验最好按照精益产品管理金字塔的相反顺序来完成。

受《价值主张设计》启发而改编的测试漏斗

我们首先验证了当前的目标用户群体是否是正确的目标群体

其次,验证我们是否用计划的和已交付的功能解决了正确的用户需求

最后,我们测试了客户的支付意愿

Alexander Osterwalder 等人的测试漏斗更侧重精益产品金字塔的市场方面。为进一步测试价值性、功能集和用户体验,我们必须扩展测试漏斗。

PART 6:提出假设,制定测试验证方案

使用测试卡片制定和记录假设,并定义验证方案,对我们的帮助很大。测试卡片为我们创造了一个定义假设确认状态的待办列表;在这些测试验证的过程中,我们也得到了大量的学习成果。

价值主张设计》启发而改编的测试卡片

PART 7:了解真正的问题空间,而非解决方案

进行假设验证时,产品负责人应该向用户询问有关需求和问题空间(Problem Space),而不是解决方案(Solution Space)。

福特汽车公司的创始人亨利·福特曾说:「如果我问人们,他们想要什么?他们会说更快的马匹」。

因此,永远不要向用户询问可能的解决方案,而应该尝试了解他们的问题,并为此找到更好的解决方案——这是我们学到的重要教训。

谈到用户需求,就少不了要聊卡诺模型。卡诺模型将用户需求分为三类:必备型需求即基本需求-Basic Features、期望型需求-Performance Features 和兴奋型需求-Excitement Features。

卡诺模型 – Kano Model

试验假设时,我们必须瞄准一个目标,而这一目标应该在产品战略中加以定义,使我们有别于竞争对手。然而,通常情况下最大的竞争对手不是公司,而是满足用户不同需求的替代方案。

开始试验大量测试卡片前,我们先为产品定义了一个明确的定位策略。受到 Dan Olsen 的启发,我们设计了一张用于定位策略的表格。

如果是亨利·福特,那他的表格应该长这样:

PART 8:产品负责人应如何进行假设试验?

01 评估优先级和工作量

不同假设的试验难易程度不同,所需要付出的努力也不一样。因此,第一步就是要确定试验任务的优先级次序,并评估试验工作量,这非常重要

02 对试验类别进行区分

除了区分定性试验定量试验,我们还会区分市场验证产品验证

改编自测试类别,灵感来自《精益产品手册》

我们尝试了不同类型的用户测试,最开始做的大多数都是定量的。通过采访和观察部分用户,我们了解了很多关于用户、客户以及问题和需求的信息。

在那之后,用户测试的重点更多地转移到了价值主张和实际解决方案上,包括用户体验等等。

03 产品策略细化到目标

确认了真正的目标用户群体,确定我们确实在解决正确的用户问题后,我们便进一步细化产品策略。这也是从问题空间切换到解决方案的时间。

我们先制定了一个发布计划(Release Plan),详细地说明了我们希望在第一个 MVP 中包含的内容。而先前得到的关于用户需求和实际问题的信息,则很好地帮助我们创建了一个优先级最高的用户目标列表。

有了这些用户目标,就可以通过用户故事地图(User Story Map)将用户旅程从头到尾地进行可视化和建模。

04 用户故事地图搭建解决方案

在用户故事地图中,你可以为特定用户的主要目标旅程进行建模。也就是说,用户故事地图是针对特定用户群体量身定制的。

一个用户故事地图的主要目标就可以被分解为多个连续的子目标;

每个子目标又可以被划分为若干个用户完成子目标所必须进行的活动流程

每项活动又能创建一个或多个详细的活动卡片

举个例子,「购买某种产品」的用户故事地图应该这样呈现:

  • 白色卡片 | 主要目标/用户故事地图主题:购买产品
  • 蓝色便笺 | 子目标:查找产品、比较产品、将产品添加到购物车等
  • 绿色便笺 | 查找产品的活动流程:浏览类别、搜索产品等
  • 黄色便笺 | 具体的详细活动:导航到商店、选择类别、搜索产品关键字、滚动浏览产品搜索结果等;也可以是活动的替代任务、例外或详细信息。

不难发现,通过逐步拆解和细分,用户故事地图进一步将问题空间(蓝色和绿色便笺)与解决方案(黄色便笺所示的史诗和功能集)联系起来。

05 发布规划和 MVP

通过对产品发布进行分割和切片,用户故事地图还能定义每个发布版本应该为目标用户提供哪些价值主张。这进一步加强了我们的产品战略。

对用户需求进行切片时,正确地切分价值主张非常重要。Dan Olsen 创建了一个很好的切割 MVP 的可视化图像:像左图般纵向地切割你的 MVP,而不是右图的横向切片。

MVP 必须具有真实产品的所有功能,具备功能性、可靠性、可用性、直观性和令人愉悦等特点。

06 用情感曲线测试 MVP/发布

要验证 MVP/版本是否具有良好的用户体验,我们可以将情感曲线图和用户故事地图结合起来。

让一些用户来尝试你的解决方案,先观察他们在使用某个功能时的整体感受

体验结束后,采访他们并询问对过程中某个特定步骤的想法

然后,通过对试验结果取平均值,可以很清晰地了解产品的可改进空间;

也可以通过比较情感曲线图,比较不同版本的产品,并找出改进的地方。

# SUMMARY:总结一下

不要相信自己的谎言。做一个真正的产品负责人,问问自己「我们该如何构建正确的产品」

不要相信任何人,用试验和数据说话。首先要验证产品是否在正确的市场中、谁是最大的竞争对手,以及用户的真正需求和问题是什么。

对提出的假设进行优先级排序,并选择适当的测试来验证它们。

只有去除最大的不确定性,确认假设后,才能借助用户故事地图和情感曲线,对产品策略进行细化和可视化


>> LigaAI 往期精彩阅读 <<

研发效能应该如何管理与度量?

如何让敏捷小组在迭代回顾会上「知无不言」?

如何搭建可伸缩的研发流程管理方案?

影响研发效能的7个常见场景解读

了解更多敏捷开发、项目管理、行业动态等消息,关注我们的团队博客或点击 LigaAI-智能研发协作平台,在线申请体验我们的产品。

]]>
敏捷实践 | 如何让敏捷小组在迭代回顾会上「知无不言」? https://ligai.cn/blog/alige/1105.html Fri, 21 Oct 2022 06:21:51 +0000 https://ligai.cn/blog/?p=1105 阅读更多]]> 观点共创 | 潮海项目教练

撰文编辑 | LigaAI

敏捷团队在迭代评审会(Sprint Review)中展示和评估产品增量,并调整待办列表;随后,在迭代回顾会(Sprint Retrospective)上,聚焦开发与发布全过程,讨论有关工作和协作的优化方案,以改善开发过程、提高开发质量

释放有效沟通,鼓励畅所欲言。迭代回顾会通过分析流程和协作中存在或潜在的缺陷,帮助团队识别和解决冲突,是实现迭代提升和增补动力的最佳实践,也是不可或缺的重要敏捷环节。

但在真正的落地与实践中,许多团队总会因为成员参与度低、内容空泛不聚焦、气氛压抑流于形式等挑战,最终放弃会议。如何让含蓄内敛的成员发声,言之有物地参与到迭代优化的建设中,困扰着每个回顾会失意的敏捷团队。

其中,构建安全场域,赋予全员安全感是激发自主表达的第一步,也是最容易被忽视的关键技巧。

一、什么是安全场域?

场域(Field)一词起源于物理学,后成为社会学的重要概念之一。布迪厄将场域定义为「位置间客观关系的一个网络或一个形构」。 它不是由一定边界物包围的领地,也不等同于一般领域,而是有内含力量的、有生气的、有潜力的、相对独立的社会空间,通常分为物理场域和心理场域两种。

场域理论指出,人的每一个行动均会被行动所发生的地理和行为环境所影响,而勒温的场动力理论则说明,一个人的行为与个人主体、内在动力、空间环境及氛围等多重因素有关。

基于场动力理论,打造迭代回顾会的安全场域可以从三个维度解释:

  • 建立与会者间的相互连结,给彼此安全感;
  • 创造与会者和空间环境的连结与舒适感;
  • 打造与会者与会议带领者的连结和信任感。

关注迭代回顾会的场域价值,为敏捷团队创造和维护一个高效的、有安全感的空间环境和氛围,能促成与会者间的良性互动,更聚焦地达成会议目标;

同时,安全场域所激励的情绪或能量上升,还能让会议流动起来,在保证会议目标顺利完成,提供迭代优化价值等方面都有重要意义。

二、如何打造回顾会的安全场域?

引导和打造场域应关注五大要素,他们既是场域的组成部分,也是场域的显化。

  • 信息:大量的事实、想法、意见、观点、决策、行动方案等信息会在会议过程中浮现出来并不断变化,相互作用,彼此影响。
  • 能量/情绪:与会人的能量状况包括活力、情绪、精神状态等等,会直接影响场域,并最终作用于目标和结果的产出。
  • 空间:在心理场和物理场两个层面,为敏捷团队营造一个开放、安全的空间,是会议有效产出的必要前提。
  • 关注/关系:包括会议引导者对与会者个人和会议产出的关注,以及与会者对议题、流程和结果的关注,通常是动态变化的。
  • 流程/时间:流程的变化会直接带动场域的变化,并影响到其他四个要素。

引导场域的五大要素相互关联与作用,最终影响整个场域和会议结果。更具体地,创造和维护迭代回顾会的安全场域,可以从以下四个方面着手。

01 培养场域意识

心理场域层面,会议组织者/主持人在会议过程中应结合上述五大要素,关注场域的动态变化,观察成员的状态和心理活动,并在必要时候提供支援,引导会议向和谐、轻松、开放的方向进行。

在物理场域角度,敏捷团队应重视会议环境、流程以及外在行为线索环境的搭建,尽可能帮助与会者投入到迭代回顾的主题当中,获得沉浸式体验。

  • 会议室的物理环境和设置
  • 视觉海报冲击,如主题欢迎海报、总结海报等
  • 日程安排的衔接
  • 入场调查的细节
  • 参与原则的说明
  • 问题停车场的安排
  • 流程中的工具规则
  • 与会人的着装和动作

02 关注所有人

经验表明,人们更倾向于在熟悉的环境和团体中坦诚交流。也因此,营造安全场域的第一步就是「消除」陌生人。

花点时间介绍在场的所有人,让与会者彼此熟悉,包括那些在线上或角落里旁听,不参与讨论的伙伴。别让会议空间里存在陌生人,是维护安全场域中不可忽视的技巧。

03 尊重不同声音

有效的迭代回顾会欢迎多元的建议和意见;有安全感的场域也意味着所有成员可以自由表达,且所有声音都会被尊重和认真聆听,而不是被无理由驳回、批评或攻击。

有领导者在场的迭代回顾会议,或许需要一个能够鼓励成员勇敢发言的引导者,或者也可以考虑采用更能激发群体表达的创意会议形式。

场域的安全感来自于团队内自由的表达:允许并尊重各种声音,安全感就会升起;而当会议充斥着热烈的表达,场域安全感又会被进一步增强,进而形成良性的动态激励。

04 共创参与规则

参与原则是指为了实现既定目的和产出,由团队成员共同承诺并愿意遵守的行为准则。同DoD与DoR类似,迭代回顾会的参与原则规范了会议开展和进行的标准,通过建立统一的共识和规则,确保会议可以融洽地、相互支持地完成。

敏捷团队可以邀请所有成员一起制定规则,让每个人在参与讨论时获得安全感;在定期的会议经验加持下,持续优化和补充规则集,避免参与原则成为形式主义产物。

而在共创参与原则时,聚焦成员互动和具体行为,提出针对性的、非抽象的说明,更能发挥参与原则的最大价值。

当回顾会上出现震荡/冲突,或团队触及敏感话题时,主持人做出积极引导,让讨论回归到参与原则上,也能为安全场域的维护做出贡献。

# Liga 总结

一个好的场域能够激发参与者的表达欲,建立成员间的和谐关系。

而基于安全场域开展的迭代回顾会可以赋予所有成员坦诚表达的勇气,成就更紧密、更顺畅的协作,并成为敏捷团队前进的核心动力。


>> LigaAI 往期精彩阅读 <<

如何搭建可伸缩的研发流程管理方案?

影响研发效能的7个常见场景解读

分享一个优先级系数计算公式

迭代评审会的七宗罪,你知道吗?

了解更多敏捷开发、项目管理、行业动态等消息,关注我们的团队博客或点击 LigaAI-智能研发协作平台,在线申请体验我们的产品。

]]>
技术分享 | 影响研发效能的7个常见场景解读 https://ligai.cn/blog/team/1079.html Thu, 29 Sep 2022 07:40:18 +0000 https://ligai.cn/blog/?p=1079 阅读更多]]> 伴随着数字化与信息化的发展,研发效能和降本增效日渐成为企业管理焦点。尤其对于研发型团队而言,快速地、保质保量地交付价值是优先级最高的任务,但在实际的开发过程中,我们总会遇到技术债务、并行冲突等影响研发效能的情况。

在告别野蛮生长,主张精耕细作的今天,企业/组织应该如何解读种种效能障碍,制定可复制的解决方案?本篇文章将从7 个常见的研发场景出发,分享有关研发效能提升的心得与经验。

场景一:并行开发导致代码冲突

组内/组间并行,或由代码回退/合并等造成的各种并行开发导致代码冲突是常见的效能问题之一。并行化的分支管理和版本管理是比较重要的议题,而合并策略、Feature分支管理、变更管理都可能影响研发效能。

解决这个问题,可以考虑以下三种优化方式:

1. 时序串行管理

以时间为轴,串起整个版本主线,代码对版本负责,版本对功能负责。

对同一系统而言,代码是并行开发的,但最终的交付物/发布物是顺序发布的;对不同系统而言,主要考虑相互间的依赖关系,影响面以及发布顺序。

2. 功能化整为零

按照敏捷迭代方式将大功能化整为零,更好地应对变化。如遇到迭代周期内需求必须变更的情况,需要确定好变更的影响范围和需求优先级。

3. 需求分而治之

技术/优化需求和跟版迭代需求可能需要采用不同的发布策略和分支管理。这样既可以保证业务目标按期、有效地达成,还能保障各种优化和支撑工作灵活地进行和并行。

场景二:技术债与架构腐化

技术债是一个老生常谈的话题。企业在平常的研发管理中,应重视「好习惯」的培养,若等到技术债堆积成山,系统病入膏肓才着手解决,恐怕就为时已晚了。

建议在日常的研发管理中,加强代码审核机制,实行代码的P3C规范化检查;前期对业务的技术方案也应作出合理取舍。

另外,架构设计应结合实际业务和资源进行充分考虑,谨防过度设计。好的架构是演化而来的,没有一劳永逸的完美架构。

场景三:频繁的故障排除任务

并行协同时,配置和资源文件的不同步也是造成冲突和问题的重要因素。为避免额外的排除工作影响研发效能,企业可以考虑提升配置和资源的独立性以及简化性

第一,尽量按时间顺序管理需求配置的唯一值;如果不能保证唯一配置,则推荐按分组逻辑管理各组的修改值(不冗余其他组的原有配置)。

比如,按时间序列管理或分组并列管理,待确定提测节点后再进行合并。这样可以较清晰地发现冲突项,防止互相覆盖。

此外,除公共配置外,考虑按功能进行分组配置,不要将全部内容写在一个配置文件里。

第二,配置合并时,签入签出流程要尽可能短。配置的合并过程需要审核,但配置调整的流程时间窗口不易过长,以免造成额外的等待成本,诱发潜在的冲突。

场景四:生产问题排查与数据安全性

许多时候,生产环境的数据必须脱敏,但同时,研发团队又需要验证生产问题或缩小问题的影响面。这种情况应该如何解读和解决?

1. 用脱敏后的非敏感数据完成验证

生产环境的客户数据脱敏后,记录部分非敏感的ID参数、异常等日志仍可以作为有效数据,完成特定场景下的分析诉求。

2. 在Pre准生产环境同步非客户数据准备一个与生产环境相对一致的「克隆体」——Pre准生产环境,同步并通过非客户数据完成生产环境的验证。

非客户数据包括部分生产测试的数据、经客户允许的可搜集的部分数据,以及经过合规完全脱敏后的数据等等。

3. 采用A/B测试,先行渗透运行

通过少量客户渗透,或对部分特定租户进行生产环境的短时渗透运行,稳定后再投入大规模部署。

场景五:环境复杂度

研发过程中,开发环境和部署环境的复杂度也会影响研发效能。因此,建议尽可能地降低自测、联调、环境部署的复杂度,以及同一个服务的代码量和复杂度。

举个例子,有些系统仅是启动就要耗时 30 分钟,那么每位开发者每天花在应对环境、应对启动的时间成本也显著增加了。

场景六:生产问题和潜在问题

不可否认地,没有一款产品、一项服务能永远不出问题。因此,搭建有效、可快速反应的业务监控和运维监控体系非常重要。

不管选用哪种监控平台系统,核心目的都是监控核心目标,并实现关键指标的及时预警和通知。有效、直接、快速地反应和处理发现的问题,比丰富的监控方案更为重要。

其次,重视测试环节。考虑补充多种测试手段,尽可能地发现问题,比如针对接口的自动化测试、针对场景的集成测试、对大型系统的压测环境等等。

场景七:非技术影响因素

在研发流程管理过程中,非技术因素也会对研发效能产生重要的影响。

  • 研发流程的简洁性与合理性
  • 产品持续输出与合理的需求粒度
  • 会议效率和沟通协调的成本及损耗
  • 性格不同导致的有效沟通方式的差异
  • 长期的紧绷状态

# Liga总结

研发流程管理是研发效能提升领域中重要的议题。管理者可以以鸟瞰视图,分析和判断研发全生命周期的运转情况,并借助智能化的监控和预警工具,发现问题、解决问题、避免问题,做出更可靠的管理干预和引导。


>> LigaAI 往期精彩阅读 <<

分享一个优先级系数计算公式

迭代评审会的七宗罪,你知道吗?

优秀的程序员如何提升个人能力?

每日站会能不能取消?

了解更多敏捷开发、项目管理、行业动态等消息,关注我们的团队博客或点击 LigaAI-智能研发协作平台,在线申请体验我们的产品。

]]>
敏捷实践 | 迭代评审会的七宗罪,你知道吗? https://ligai.cn/blog/pmo/1063.html Fri, 16 Sep 2022 01:27:03 +0000 https://ligai.cn/blog/?p=1063 阅读更多]]> 迭代评审会(Sprint Review)基于真实的用户使用场景,展示当前整个产品增量,通过获取贴合用户使用习惯的反馈和建议,最终输出产品待办列表的优化调整。迭代评审会应重点关注用户需求的解决情况,而不是查找缺陷(当然,影响核心流程的重大缺陷一定要记录在册)。

  • 展示结果:开发团队检视当前迭代的最初计划,展示每个需求/故事的工作结果及变化;
  • 评审反馈:产品负责人进行确认,收集反馈,并记录进一步的改进设想为新需求/故事;
  • 调整方案:产品负责人及关键干系人介绍有关后续迭代的新信息/设想,为接下来的迭代计划会议提供有价值的输入信息。

基于清晰的会议目的和讨论重点,如何更好地完成迭代评审会?高效的迭代评审会又有哪些不为人知的事半功倍小技巧?

本期敏捷实践,LigaAI联合潮海项目教练团队的资深敏捷教练,一一为你解答。

一、 每个迭代结束都要进行评审吗?

作为Scrum五大事件之一,迭代评审会通常在产品增量完成后、正式发布前举行,但在实践经验中,并非每个迭代完成都必须要进行评审。依据时间频率的不同,迭代评审会分为高频评审和低频评审。

  • 高频评审适用于产品增量复杂的情况。通过将产品增量拆分成若干个关键需求,并在每个关键需求完成后,邀请重要干系人参与检验,以分散一次性评审的会议压力。
  • 低频评审适合产品增量对干系人的感知价值不大(比如长流程的端到端需求)或干系人时间紧张等情况。通过多个增量的合并评审,减少频繁会议对工作的占用和干扰。

此外,以迭代周期/固定周期作为举行迭代评审会的标志并非最优解。迭代周期是团队内部的开发管理单位,在可感知价值增量层面或有不稳定性。

更好的做法是基于里程碑/史诗的完成情况,以增量结果为指标,按需开展迭代评审会。研发里程碑一般分为「内部里程碑」和「外部里程碑」两种。

  • 内部里程碑常指项目关键决策点,如需求阶段完成、设计阶段完成,主要用于评估项目内部风险,一般不涉及用户反馈,无需迭代评审;
  • 外部里程碑是涉及对外发布的重大功能/模块的完成,需要在产品发布前邀请关键干系人参加迭代评审会,并贡献真实的使用反馈。

二、 干系人一定要参与会议吗?

在《每日站会能不能取消?》一文中,我们曾提到「非必要情况下,Scrum Master可以不参加每日站会」。那么,迭代评审会是否也可以不需要关键干系人的参与呢?

答案是——不可以。

迭代评审会的核心目标是收集用户反馈,所以关键干系人的参与极其重要。但在实际中,不可避免地,干系人可能因各种原因无法参加会议,可以采用以下几种方式解决:

第一,由干系人授权的决策代表代替干系人参加会议。决策代表需要具备对产品增量贡献反馈的能力,并且要能够提出「通过与否」的关键性意见。

第二,如果干系人无法分出大块时间参与评审,那Scrum团队可以采用高频小模块的方式完成反馈,遵循「价值原则」——先展示最急需反馈的功能。

第三,将干系人的会议预约列入待办。避免临期预约的冲突和缺席隐患,产品负责人可以在迭代规划期,就提前锁定关键干系人的会议时间,或将迭代评审会以定期形式常态化(比如安排在每双周的固定时段),确保干系人可成功出席。

贡献反馈的干系人必须参加,而执行反馈的技术人员也同样不能缺席。开发团队应在现场直面用户,聆听最真实的用户声音,才能更好地理解业务、理解需求。

理想情况下,开发团队应全员参与迭代评审会,同干系人一起协作讨论,在认同中建立工作价值感;

如果团队规模较大,也可以轮流派代表或者安排技术骨干参与会议,负责本次产品增量的开发人员必须出席会议,以减少一手反馈的传递偏差。

三、迭代评审会的多种打开方式

01 合而为一

践行Scrum的过程中,敏捷四会(即计划会、每日站会、评审会和回顾会)不必是完全独立的。

例如,对于每周一迭代的短周期敏捷团队,可以考虑将评审会和计划会合为一次会议,以减轻会议负担。会议中,应先评审、后计划:关键干系人在评审结束后先行离场,Scrum团队继续进行新一期迭代的计划会议。

迭代回顾会则不建议与其它会议合并,因为这是敏捷团队复盘和经验总结的宝贵机会;但可以考虑将多个迭代的回顾会合并一次举行。

02 一分为二

对于产品增量价值低、客户感知影响不大、或有高质量要求,需先进行内部评审的迭代而言,可以采用「一分为二」的低频评审形式,将迭代评审会分为「Scrum团队的内部会议」和「与干系人协作的外部会议」两次进行。

  • Scrum团队的内部评审侧重于完整的功能展示,重点审查关键功能能否跑通、是否存有Bug尚未修复/发现等;
  • 有关键干系人参与的外部评审以收集用户真实反馈为主,重点验证产品增量是否满足客户需求、是否达成真正的产品价值。

03 直面客户是最好的形式

远程办公和异地协作盛行的今天,共享式的文档/表格/问卷协作免去许多会议,但是迭代评审会以获取用户真实反馈为核心,面对面沟通却是不可省去的重要仪式——直面客户、观察用户的使用和体验是获取有效反馈的主要途径。

对于重要且急需用户反馈的功能,Scrum团队应主动安排产品负责人和功能相关的技术骨干前往干系人公司,进行一对一展示和沟通

面对无法克服的时差或时空问题,也可以采用线上视频会议的形式,但是建议要开启摄像头和麦克风,并使用共享屏幕,让Scrum团队「看到」干系人的使用反馈。

四、迭代评审会的四个黄金搭档

高效的迭代评审会一般包含功能演示、体验试用、讨论反馈和待办优化四个环节。

01 功能展示

述清会议内容与目的后,开发成员结合迭代计划与产品增量,向产品负责人和关键干系人展示已经完成的产品价值。

看似简单的功能展示,也存在一些共性的改进空间——《深入核心的敏捷开发》提出「展示会七宗罪」以描述功能展示过程中的七大问题,并针对性地提出了高效展示会的七个技巧。

七宗罪之一:准备工作没做好,空耗会议时间

正确做法:主讲人在正式展示前,应该做好充分的准备工作——预演演示步骤,准备测试数据,提前部署演示环境等等,避免手忙脚乱和空转浪费,提高会议效率。
七宗罪之二:没有说明铺垫,云里雾里不知所以

正确做法:在开始演示之前,主讲人应当简要地介绍会议目标和所要展示的功能,并说明功能给用户带来的价值。清晰的上下文能让产品负责人和干系人更快地进入状态。
七宗罪之三:逐条过验收标准,缺失业务完整性

正确做法:不要逐个演示用户故事/验收标准。主讲人应以功能为单位,将完整的产品/功能/模块串起来展示;最好定义出单独的业务场景,使用业务语言让业务成员更有代入感。
七宗罪之四:相同/类似的功能,演示所有路径

正确做法:只演示最关键的路径。遇到多个路径实现相同或相似功能时,选择其中一条最复杂/重要的路径详细演示,其他路径指出不同的地方,点到为止,无需覆盖全部路径。
七宗罪之五:过多提及跟演示功能无关的内容

正确做法:专注于最有价值/重要的功能演示,不要让小反馈/未完成的模块耽误会议时间;尽量不提及技术难题或技术方案等业务人员不感兴趣的内容。
七宗罪之六:认为展示仅仅是BA或QA的事情

正确做法:不让某个角色独占功能展示,人人都应该参与进来;可以采用人员轮换的方式进行展示,这样可以提升成员的业务意识,更熟悉整个系统功能。
七宗罪之末:不熟悉的新人负责展示,重点模糊

正确做法:新人展示前应充分了解业务和系统,确保能够应对和解答业务的挑战和疑问;也可以让新人在结对编程、Story Kickoff等多做主导,具备一定的系统和业务意识后再向干系人展示。

02 体验试用

开发成员完成功能演示后,更重要的是要让关键干系人亲自体验和试用系统功能/Demo。用户亲自下场试用和检验,是获取反馈中必不可少的一环。

产品负责人也可以邀请真实的用户参加评审会,让其在开发的指导下体验演示的功能和系统。需要注意,此时开发团队应该为干系人和用户留出足够的自主探索空间,引导他们重现最真实的使用习惯和场景,避免成为「产品推销员」。

03 讨论反馈

产品负责人要收集关键干系人(和用户)的反馈,及时记录最新的改进与设想为新的需求。在体验试用环节,Scrum团队可以通过以下几种方式,观察用户体验,洞悉真实反馈。

  • 注重整体的展示,避免成为验收测试

让干系人和用户体验功能并非授权全自主探索——开发成员需要提供必要的场景描述,还原功能使用场景,让用户更有代入感;但,切忌进行步骤指导,过多的干预反而不利于观察真实反馈。

另外,不要逐一演示用户故事。开发成员应尽可能引导用户对整个产品/功能模块进行试用,并关注基于产品整体的功能完整性和衔接流畅性,观察和分析可能存在的优化空间。

  • 细心观察用户表现,敏感捕捉使用障碍

在用户体验功能时,Scrum团队应该留心观察他们自然表达和流露的语言、表情、操作和使用习惯等,要具备捕捉异常的敏感度

比如用户是否在使用期间出现困惑表情、在无反应的地方反复点击等等。通过深入挖掘用户行为背后的原因,了解最真实的行为反馈。

  • 使用开放性问题,引导用户自主表达

与干系人和用户协作、沟通和讨论时,避免使用「是不是」、「有没有」等封闭性问题;多用开放性问题,一步步引导用户表达出最真实的想法

发现用户频繁点击某个按钮,或浏览选项的时间过长时,可以通过抛出「你希望这个按钮提供什么功能/选项(但没有满足)吗?」等问题,鼓励用户主动说出对系统功能的期待,为后续的迭代贡献有价值的意见和改进空间。

04 待办优化

在评审会的最后,产品负责人及关键干系人介绍有关后续迭代的新信息或新设想,结合当前产品或市场环境的变化,讨论即将发布的版本的产品功能,为接下来的迭代计划会议提供有价值的输入信息。

通常在会后,产品负责人会结合迭代评审会的结果,对产品待办列表和需求优先级进行重新评审和整理——这也是衡量会议成功的关键。

为了更准确地完成待办列表调整,开发团队应对未完成的工作保持开放和透明,以便产品负责人能制定更全面的决策;

产品负责人可以结合不同决策依据,应用优先级排序的三个模型和四张画布,更高效地完成调整。

更多阅读请点击:

不同决策应参考什么指标进行优先级优化?

做优先级排序时使用最多的三个模型

译文 | 四张画布教你判断「产品开发优先级」

# Liga总结

迭代评审会的核心目标是为了获取真实的用户反馈,为后续的迭代和研发工作贡献有价值的意见。

高效的迭代评审会要抓住功能展示、体验试用、讨论反馈和待办优化四个关键环节。Scrum团队以专业、高效的姿态,向干系人和用户呈现研发成果;通过引导和观察,洞悉真实场景下的使用反馈,更好地服务后续迭代。


>> LigaAI 往期精彩阅读 <<

优秀的程序员如何提升个人能力?

每日站会能不能取消?

如何消减协同合作中的认知偏差?

敏捷实践 | 《需求拆分流程图》详解

垂直切片是敏捷需求拆分的最佳实践 | 敏捷实践

了解更多敏捷开发、项目管理、行业动态等消息,关注我们的团队博客或点击 LigaAI-智能研发协作平台,在线申请体验我们的产品。

]]>