如何在咨询项目开展Inception

本文通过我在咨询工作中的真实案例讲解了如何将敏捷开发的Inception与可视化咨询手段结合。2014初,我作为咨询师为客户提供咨询服务,负责一个新项目的敏捷咨询、架构以及开发编码工作,距今已有四年时间,文中内容已经不再敏感。个人认为,这次咨询的一些方法与经验有值得借鉴之处,因而分享给大家,以资参考。

破冰之旅

我们要开始的项目是重写原有的一个版本管理系统,将其一部分内容从主控板中剥离出来,并将原有的C实现改为Java实现。项目的目标和范围相对确定,但架构方案还有待进一步确认。此外,团队成员只有C语言背景,没有任何Java语言的背景知识,也不了解面向对象编程。针对此情况,我决定对项目做一个Inception。下图是我确定的Inception计划:

我们召集相关干系人(包括支持软件产品部部长,部门的大项目经理,版本管理人员,项目组所有成员)参与了Inception的Kick Off会议。会议中,我们一起梳理了项目的目标与范围。整个咨询项目的目标为:

  • 高质量的交付NVUM(即NodeB Version Upgrade Manager)项目;
  • 设计优良的架构,保证架构的可扩展性,以支持未来需求的变化;
  • 培养团队成员的面向对象设计能力;
  • 打造敏捷团队;
  • 提炼项目最佳实践。

在确定项目的目标与范围后,我开展了破冰之旅,以热气球的形式展现团队的动力与阻力:

部门领导对这个项目非常重视,希望树立一个推行敏捷的模型,获得落地经验,并将这些经验分享和推行到整个部门。因此,部门领导给与了很大的支持。不过,挑战也非常大。

首先,团队成员完全不了解Java,也没有任何OO知识。其次,团队成员也不了解敏捷。同时,还需要承担相对紧张的交付压力。项目的特殊性决定了部门领导对项目高质量的重视。整个项目的主要功能点并不多,但涉及到的场景非常多,情形也比较复杂。同时,设计的系统必须具备良好的可扩展性和低缺陷率。在交付项目的同时,咨询师还需要培养团队成员,带领团队成员在技术和敏捷实践上获得提高。

需求的梳理

针对需求,在Inception阶段,我着重进行对Master Story的识别与拆分。由于该系统的主要工作是对原有系统进行重写,除了极个别的新需求外,需求功能与范围都是明确无误的。但考虑到我们需要在Inception阶段确定MVP(最小可用产品),从而获得发布计划与迭代计划,重新梳理需求是有必要的。而且,这个梳理过程要求整个团队的成员都参与,这也是一个极佳的通过协作明确需求的机会。

我在梳理需求时,首先还是从调用者(用户角色)的角度出发。这相当于Use Case中的Actor。从这个视角去分析需求功能,可以更容易帮助我们去识别需求的价值,并找到系统的边界。该项目的情况比较特殊,从使用者角度看,仅有外场的用服作为参与者;但调用或触发功能的角色却不仅限于用服,一些参与的系统也可以视为Actor,这其中包括:SCS、DBS和VMP Timer。

项目主要的功能就是对版本规格包的管理(包括升级、回退、删除等),因而有必要识别规格包的类型。可以分为:BBU、RRU和固件,而且BBU和RRU还包含了补丁规格包。补丁规格包又分为冷补丁和热补丁。这些规格包可能组合。同时,不同的产品制式包含的规格包也有所不同。

站在Actor的角度,我们梳理出了如下图所示的Master Story列表,并以用例图展示:

我们可以将这些用例视为系统的一级功能或者Feature,它有助于我们建立对整个系统的全局感官。但这样的分解太粗犷,因此还需要继续细化。我使用了卡片来帮助我们表现功能之间的层次关系。同时,根据规格包的类型,又以表格的方式展现各种规格包之间的异同。例如下图就是关于版本升级功能的需求分解:

通过这样的需求分析活动,团队成员基本上就系统的功能达成了一致意见。现在,我们就可以把最高层次的Feature卡排列在白板上,根据大家对系统的认识来确定优先级,并基于功能完备性选择可以放在同一个MVP的Feature卡:

为了避免需求的缺失,并确定这些功能与网管系统能够对应,我们还做了Requirement Map,如下图所示:

我们将整个项目周期划分为四个MVP。我们一致认为版本的升级是整个系统最重要也是最主要的功能,它的价值最高,可能存在的风险也最高,完全有理由放在迭代的最前面。最初,我们并没有考虑将“查询”功能放在最高优先级,相较于“升级”和“上电”,甚至于“回退”,它的优先级都有所不如。但当我提出如何在每个发布阶段进行演示时,大家才认识到如果不尽快实现查询功能,可能会使得我们发布的最小版本并不可用。我们当然也可以考虑命令式脚本进行查询,但这样的开发成本相较于开发UI的查询功能而言,并没有太大的优势。

虽然所有规格包的升级功能在重要性方面都要高于“回退”以及“全同步”。但由于升级功能的工作量最大,所有规格包的升级功能开发完毕大约会占用超过1/2的时间。从最小可用的角度来看,我们选择了整体功能的完备性。因此将固件与补丁包的升级放到了更其次的MVP 3。而且我们认识到,一旦开发完BBU和RRU规格包的升级,对于固件与补丁包而言,实现就变得简单了。

我们之所以考虑将“全同步”与“预上电”功能放到了MVP 2,与它们的重要程度(从重要程度讲,它们要低于升级与回退)无关,而在于对它们的开发需要与其他团队协作。若能将这部分功能的迭代周期提前,可以便于我们更好地与其他团队进行协作,同时,帮助我们提前发现风险。

在分析需求时,团队还统一了主要的领域术语,并确定了各个领域对象之间的关系。我用各种颜色的即时贴来展现(并非采用四色建模),这其中,绿色即时贴代表受控板,白色即时贴为主控板,即CC。NodeB为基站,有时候也称为网元,为保持一致,我们决定统一为NodeB。我们还识别出Small NodeB的概念。对于Small NodeB而言,BBU和RRU是在一块单板上的:

RAID系统分析

在梳理了VPM的需求后,我在Inception阶段开展了对系统架构的分析。由于这个系统自身并不复杂,但牵涉到太多的系统,且系统与系统之间的通信采用了各种方式,且系统的稳定性和性能的要求都较高,因而需要对整个系统的风险进行充分地识别。为此,我引入了RAID(Risk, Assumption, Issue, Dependency)分析,如下图所示:

进行RAID分析要比单纯的问题分析更为收敛,因为它明确了头脑风暴的范围及其类别。

识别风险

通常而言,对风险的识别可以引导我们对系统质量属性的思考,利益相关者可以充分表达对这些属性的担心,从而驱动我们去寻找解决方案。

稳定性

在这次RAID分析中,网管系统的负责人明确提出了对稳定性的担忧。由于之前的版本管理系统驻留在主控板CC中,网管系统(Java平台)与主控板以及受控板之间的通信较少。在将版本管理系统的部分功能转移到Java平台之后,形成NVUM专有模块并由网管系统发起调用。这就导致网管与主控板之间的通信可能会增多。从过往的系统看,这种通信的稳定性欠佳。基于这一问题,我们在后续的架构设计中对此进行了深入分析,决定在主控板一端设计粗粒度的接口,一次性地传递升级需要的信息,并以写文件的方式,将返回的数据传递到NVUM。

可扩展性

风险对扩展性的识别,帮助我们确立了一个架构原则,就是版本规格包的结构不应该影响到主控板的系统。这是因为主控板系统的升级受到的制约最多,我们不希望当产品发生变化时,影响整个版本管理系统。

性能

当选择的基站数量较多时,系统的版本升级过程会变得缓慢。而版本升级必须要求无线基站不能处于shutdown状态,否则会影响业务。因此,升级过程通常会选在凌晨,并且必须在较短时间内完成整个升级工作,故而性能可谓重中之重。目前网管采用并发方式为每个基站分配一个线程进行升级。由于本次系统的设计采用了配置文件的形式,网管担心在启动多个并发线程时同时加载多个配置文件,可能会导致OutOfMemory异常。这个风险的识别及时地为我们敲响了警钟。我们为此安排了技术Spike,并确定确实存在这方面的问题,目前还在解决之中。此外,为了提升性能,主控板与受控板之间的版本下载也需要并发进行,即当一个版本文件下载到主控板时,不必等到所有文件下载完毕,就可以并行地传递给受控板。就这个风险,我们讨论的结果是在新版本中可以支持这一功能,但在旧版本升级到新版本场景下,无法支持。

给出假设

Assumption可以是关键的架构约束,也可以是系统功能性的约定。架构约束既可能是设计的阻力,也可以成为动力。经过讨论,我们基本上确定了两条最为重要的架构约束,包括:

  • 系统必须支持双向兼容,即网管一侧的NVUM(Java以及Scala)与主控板一侧的VMP Manager(C语言)互为兼容。例如,倘若NVUM升级为新版本,同样可以管理旧版本的主控板系统;反之亦然。这个约束的提出,要求我们在开发过程中,只要我们的接口已经发布,则不能再修改接口。除修复缺陷外,我们不能删除旧有功能,只能增加新功能。若旧有功能不合适,我们也不能删除,但可以将其置为@deprecated标注。
  • 版本升级过程中,若前后操作具有依赖关系,则必须保证事务的一致性,要么全部成功,要么全部失败。事实上,这一条也是对质量属性“可靠性”的一个回应。

现有问题

整个RAID的识别都针对技术层面,而非管理层面。因此我们识别的问题也限制在技术范围。

在我们识别出来的问题中,最致命的一个问题是关于NVUM模块的加载。NVUM是一个Jar包(即本项目最主要实现的内容)。在我们现在的设计中,希望的部署方式是在网管系统中动态加载这个Jar包。之所以选择动态加载,而非静态依赖,原因有二:

  • NVUM由我们项目组维护,网管系统则属于另外一个项目,两边的版本计划完全不一致。网管系统为CS系统,被独立地部署到全球多个外场。若采用静态依赖,需要我们将其纳入到网管系统中,但NVUM的版本更新会更频繁,外场不可能因为NVUM一个模块的调整,而付出频繁更新网管系统的代价。
  • 网管系统负责监控外场各个基站的设备运转状况。虽然网管系统的重启(耗时数十分钟)并不会影响设备的功能,但却可能在重启过程中,因为未能及时掌控设备状态,而导致无法及时发现问题去解决。这样的事故是必须避免的。换言之,网管系统的重启代价太高,不能经常重启。

目前的设计是由技术部制作规格包,规格包会包含我们的NVUM Jar包。外场人员得到规格包后,导入规格包,此时,网管系统会动态加载NVUM。具体流程如下:

这就需要解决动态加载的问题。网管系统的开发人员认为无法完成Jar包的动态加载。但就这一点,我已经给出明确答复,从技术上是可行的,并且做了一个Demo来演示,方法是通过URLClassLoader实现加载。当然,我们也可以选择OSGI,但我认为这个方案过于重型,成本太高。

但是,动态加载也存在一个问题,即我们需要将NVUM分为interface和impl两个模块,并保证interface的稳定性。规格包中应该只包括impl模块。

另一个方案是采用脚本。我比较倾向于选用Groovy,因为它能很好地与Java集成。我们只需要在Java中调用Groovy提供的GroovyShell,就能直接读取groovy脚本文件;然后调用run()方法即可执行脚本。

识别依赖

除了NVUM与网管系统(OMMB、LMT),NVUM与主控板,主控板与受控板之间的依赖外,牵涉到依赖的还有很多。有的属于输入依赖,例如ACS、SCS;有的则属于输出依赖,例如OSS、BSP、TCM、SCS、DBS、COM、BRS等。此外,还有版本制作工具等系统也会受到NVUM的影响。同时,NVUM还需要访问OMMB文件系统,FTP,读取诸多外部文件。通信则可能采用Telnet、SNMP、SSH等多种协议。

这些依赖的识别便于确定本系统对其他系统可能造成的影响,事先识别有利于我们及时做好沟通,同时还需要就一些架构约定以及接口定义达成一致意见。依赖的识别也有利于我们设计系统的物理架构,并考虑系统的部署方式。

架构设计

在经过项目的需求分析与风险问题的识别后,就进入系统的架构设计阶段。我在Inception计划中安排了将近两天的时间,用以开展“设计工作坊”活动,通过引入可视化的架构设计手段带领整个团队一起开展架构设计。团队参与的形式有助于促进交流与沟通,形成架构知识的共享。因为只有团队共享了架构知识,并能就架构的思想和原则达成一致,架构才能在软件开发过程中发挥作用。同时,这种活动也是培养团队成员架构设计能力的一种手段。

由于在咨询开始之前,该项目已经开展了足够深入与详细的架构分析与设计,因而我的工作主要是在现有方案基础上,引入一些架构设计手段,使得整个系统架构更加清晰直观,并从面向对象设计的角度,对设计进行调整和优化。其中,主要是建立系统的物理架构与逻辑架构。

物理架构与系统(模块)之间的通信、部署有关。我们运用了Cockburn提出的六边形(Hexagon)架构模式来展现。Hexagon架构并不深入关注内部边界中的领域逻辑,仅仅将领域逻辑简单地划分为Application与Domain两层。但它有助于我们获得基础设施层以及相关集成点的包结构。在RAID分析中识别出项目依赖的前提下,运用六边形架构就显得水到渠成。它更贴近应用逻辑架构(相对业务逻辑架构而言),也可以作为系统的物理架构(例如部署视图),并可以驱动我们去发现诸多集成点,寻找集成模式。内外边界的分离也有助于我们将业务逻辑与应用逻辑分离开。这实际上符合“关注点分离”的架构原则。

对于我们的系统,一方面需要与前台的主控板(CC)和受控板通信,通信的协议包括FTP、SFTP、HTTP、Telnet、SNMP等多种协议;另一方面在后台又要与网管系统(OMMB、LMT)通信。除此之外,还需要与诸如OSS、BSP、TCM等多个“第三方”系统通信。如果不通过划分边界将这种通信方式直观地展现出来,将不利于我们系统的架构设计,而且可能会导致在设计过程中出现疏漏。六边形架构通过内外边界很好地体现了这种进程内与进程外的通信方式。如下图所示:

图中的三个六边形分别表示三个独立进程的系统(模块),分别为驻留在JVM中被网管系统动态加载的NVUM、主控板(VMP Manager)以及受控板(VMP Agent)。六边形边界的贴纸代表端口与适配器,并与六边形外的系统或外部资源通信。注意,在六边形内,还绘制有一个小六边形,它代表的是当前系统(模块)的内部边界,我们采用领域驱动设计的应用层服务来公开服务接口。事实上,在这个内部六边形中,将通过逻辑架构的形式来体现。我们运用了领域驱动设计的分层架构模式,获得NVUM的逻辑架构图:

由于我们的NVUM模块会被网管系统动态加载,因而它自身并没有UI展现层,因此被分为Application Layer(应用层)、Domain Layer(领域层)与Infrastructure Layer(基础设施层)三层。

我们的系统不需要访问数据库,因此基础设施层主要针对文件和网络进行处理。由于系统的逻辑功能模块并不多,在对领域层进行模块设计时,仅仅是通过领域驱动设计中给出的标准模块结构,按照对象的类型及其承担的职责分为Repository、Service、Entity以及Value Object。至于应用层,因为它是整个系统的发布接口(Published Interface),我们需要站在接口的消费者(即网管系统)的角度思考,因而,该层按照业务逻辑进行了划分。应用层应该是一个非常薄的层,其中的应用服务对象(注意它与领域层的服务对象是两个完全不同的概念)不应该包含业务逻辑,而仅仅是对领域对象的组合与调用,同时可能包含事务、安全等方面的处理。由于我们在系统中引入了验收测试(基于ScalaTest框架),针对验收测试而言,它应该只关心应用服务接口。考虑部署问题,这些接口会被抽象出来,形成一个单独的Jar包,并在发布后,保持接口的稳定性以及向后兼容性。

为了让整个团队分享与理解整个系统的物理架构与逻辑架构,我们通过这种可视化的手法绘制在白板上,使其更加直观。一旦架构发生变化,我们也可以及时进行更新。

在对整个系统进行了高屋建瓴似的架构概览之后,我们又针对一些核心模块进行了更深入,更牵涉细节的设计。主要的手法是以用例(Use Case)为入口点,站在Actor的角度,通过绘制时序图来推导参与该用例的对象、扮演的角色、承担的职责以及它们之间的协作方式,如下图所示:

我们还引入了领域驱动设计的Bounded Context(限界上下文)与Context Map(上下文映射)来帮助我们识别系统的模块,以及模块之间的关系。例如下图是针对升级功能绘制的上下文映射:

整体而言,在整个“设计工作坊”活动中,我们通过需求分析以及RAID分析建立的基础,围绕着领域驱动设计的主要思想与原则,通过引入一些可视化手段与架构模式,对整个系统的架构进行了宏观的梳理,并针对部分核心内容进行了深入分析与设计。然而,这样建立的架构并非在将来是一层不变的,而是会随着项目迭代式的增量开发,通过引入ATDD、TDD与重构,并在持续集成环境的保障下,演进式地对整个系统架构进行改进、完善乃至于重构。