上一讲主要介绍了看板方法以及如何使用看板方法来解决软件研发过程中出现的团队过载、工作不均、任务延期等问题。通过学习前面几个课时介绍的知识,你的团队开始源源不断地交付用户价值。用户对交付的功能非常满意,但等到系统上线后经常出现服务不可用的情况,比如:服务出现单点故障,这个时候运维团队就要处理这些焦头烂额问题了。这是因为在软件开发过程中,团队关注的一直都是用户需要的功能,忽略了那些能够让产品维持工作的非功能需求。今天介绍的内容——有效关注非功能需求,就是教我们要在关注功能需求的同时,也要关注非功能需求。帮你彻底改善软件系统的可用性和性能。
什么是非功能需求
当谈起需求,很多人都认为是“用户需求”,需求也分为不同的种类。需求的分类可以参考由惠普的 Robert Grady 设计的 FURPS+ 需求分类模型:
& 功能性(Functionality);
& 易用性(Usability);
& 可靠性(Reliability);
& 性能(Performance);
& 可支持性(Supportabilit)。
FURPS+中的“+”号指的是其他需要关注的方面,比如:
& 设计需求(Design Requirements);
& 实现需求(Implementation Requirements);
& 接口需求(Interface Requirements);
& 物理需求(Physical Requirements)。
该需求分类模型包含了功能性需求和非功能性需求。功能性需求指的是产品功能,比如:聊天功能,支付功能等。非功能性需求是整个系统交付的标准,比如系统可靠,性能高等。
功能需求通常很容易定义、度量和测试,一般可以描述为特定系统执行特定功能的需求。就像前面课时中提到的用户故事,作为用户,当我在地址栏里输入内容时,显示所有可用的地址列表。因此,功能需求通常被视为系统“应该做什么(What)”。
非功能需求则较难定义、度量、测试和跟踪,这也是经常被忽略的原因。一般可以描述为系统如何提供功能。比如,当用户在地址栏里输入内容时,要快速的显示所有的地址列表(性能)。因此,非功能需求通常被视为系统“应该如何做(How)”。
为什么要关注非功能需求?
在《敏捷宣言》里讲到,“可工作的软件,胜过面面俱到的文档”。这里的“可工作”是对交付到用户手中的软件最重要的度量标准,软件不仅要具备用户需要的功能,还要能够持续不断的使用该功能。但实际情况却不尽如人意,这是因为我们在开发软件的时候,总是关注功能性的需求,而忽略非功能性的需求。我举个例子,下面这张图是一座冰山,当一个管理家庭地址的需求被提出后,用户需要的功能是新增、编辑、删除和查询家庭地址,但在这几个功能需求的背后却是表单重复提交验证、浏览器兼容、性能、用户身份认证等隐藏需求。没有这些隐藏需求,一般情况下用户也能使用该功能,但在使用的过程中,就会出现各种问题,令用户体验非常不好。
随着敏捷开发方法的采用,这一问题已经有所转变。比如,将非功能需求和功能需求一起纳入流程管控等措施。在 DevOps 实践中,这个转变更进了一步,产品负责人要关注整个产品需求,包括功能需求和非功能需求。只有从源头上重视非功能需求的开发,团队才能真正交付 “可工作” 的软件。下面介绍一下在软件开发流程中,如何关注并落实非功能需求。
如何关注非功能需求?
在软件开发过程中,明确定义的非功能需求的实现是用于评估整个软件成功的关键。功能需求的实现表示用户可以使用该软件提供的功能,非功能需求的实现表示用户可以一直使用该功能。定义明确的非功能需求,不仅容易度量和测试,而且团队也能更好地了解这个需求离实现还有多远。
非功能需求的管理
既然非功能需求对于系统交付非常重要,那么在系统一开始团队中的每个人都要考虑非功能需求,以及它们对系统架构,系统交付时间,测试策略和总成本的影响。
需求管理
非功能需求和功能需求一样,它们也有实际的业务价值,在需求管理阶段就要将其纳入统一的流程管理。比如“管理家庭地址”的需求,在进行“新增地址”功能需求排期的同时,也要考虑“表单验证”“表单重复提交验证”“身份认证和授权”“浏览器兼容性”等非功能需求。因此,需要创建一些具体的任务来管理这些非功能需求,或者在其他功能需求中加入非功能需求的验收条件。
跨团队协作
非功能需求又被称为“跨功能需求”,因此在非功能需求的讨论、方案的制定,验证条件等方面,都需要其他团队一起来完成。安全性相关的需要安全团队,性能、可扩展性相关的需要架构团队,验收方面需要测试团队,监控告警方面需要运维团队。跨团队协作不容易,我的建议是将这件事情写到每个团队的 OKR 里,这样大家的目标就一致了,实施起来会更加容易。
权衡利弊
非功能需求的实现是有成本的,而且它们之间可能彼此排斥。比如,对安全性要求高的系统往往易用性就不会太好。在理想情况下,每个人都想开发出具有高安全性、高性能、高扩展性、高易用性的系统。但是,在实际开发中,考虑到系统的交付时间,系统的总成本,往往会根据非功能需求的实现难度,做出权衡。
非功能需求的分析
非功能需求的分析也是非常重要的一部分。在进行需求分析的时候,不仅要分析出系统所提供的功能需求,同时也要知道这些功能存在的限制条件。
这里介绍一个四层非功能需求分析法。该方法包含一些规则和非功能需求分析的过程。通过这种方法,我们可以确定系统提供的功能,子功能和每个子功能需要具备的非功能需求。有利于从不同的视图识别所有的非功能需求。这种方法的目标是找出任何系统中都非常重要的非功能需求。如下图所示。
任何一个系统的需求都可以总结为下面这句话:每个<系统>提供<功能>给<关系人>,每个<功能>必须满足<条件>才能满足<关系人>的需求。
正如前面提到,功能需求表示系统应该做什么(What),非功能需求表示系统应该如何做(How)。 上图利用分层的方法非常清晰地将系统具备的功能需求和非功能需求表示出来。前面三层是功能需求的拆解,第四层是每个子功能具备的条件,即非功能需求。
整个非功能需求的分析过程可以分为下面四个步骤:
step1:识别系统中的关系人;
step2:基于开发人员的知识和经验,从关系人角度制定功能目标;
step3:拆解功能为子功能;
step4:识别每个子功能中的非功能需求。
下面利用四层非功能需求分析法分析开头的 “管理家庭地址” 的需求,结果如下图所示:
经过这个分析过程,就非常清楚地知道哪些功能需求需要具备哪些非功能需求,可以基于分析结果编写用户故事以及验证标准。
非功能需求的度量
为了使系统能够达到验收的标准,在定义非功能性需求的时候就需要是可度量的,这样团队才能知道非功能性需求当前值是多少,离满足要求还差多少。上面提到非功能需求是指系统应该如何做,即对系统属性和质量的描述,比如:可扩展性、可靠性、高性能等,这些都属于定性的描述。定性的描述缺少量化的指标,是不可度量的,这一点团队在定义非功能性需求时应该注意。
还是以上面的例子为例:
“快速地显示所有的地址列表”,这就是一个定性的描述,它是不容易度量的,也是不容易测试的,不知道要达到多快才能满足要求。
修改后:
“0.1s显示所有的地址列表”,这就是一个定量的描述,提出了对该非功能需求的度量、测试和验收标准。只要能在 0.1s 内显示所有的地址列表,就能满足要求。假如在一次测试时得到的结果是 0.2s,团队也很清楚地知道还需要再提高 0.1s 才能满足要求。
常见非功能需求的度量指标有:
& 吞吐量: TPS(Transactions Per Second)每秒处理事务数,QPS(Queries Per Second)每秒查询数,RT(Response Time)响应时间。
& 可靠性: MTTF(Mean Time to Failure)平均无故障时间,MTTR(Mean Time to Repair)平均故障恢复时间,MTBF(Mean Time Between Failure)平均故障间隔时间等。
& 扩展性: 随着服务器数、服务数或线程数的增加,单个请求的响应时间和并发用户数。
目前在监控或统计系统中都会包含上面的度量指标,如下图是请求 QPS 变化趋势图:
非功能需求的设计
非功能需求在设计时可以遵循下面两个原则。
1. 面向对象设计的 SOLID 原则。
SOLID 原则是由 Robert C. Martin 于 2000 年编写的面向对象设计的五个原则。后来,人们将这五个原则的首字母组成缩略词,也就是SOLID,并被业界广为流传。这五个原则是:
1.单一责任原则(Single responsibility principle),指一个类有且只有一个职责。这个原则增强了类的灵活性和可维护性;
2.开闭原则(Open/closed principle),指一个类对外扩展开放,对修改关闭。这个原则增强了类的可扩展性和可维护性;
3.**里氏代换原则(Liskov substitution principle),**指程序中使用基类进行定义,在运行时再确定其子类类型,用子类对象替换父类对象,这个原则增强了类的灵活性和可扩展性;
4.接口分离原则(Interface segregation principle),指类的依赖关系应该建立在最小接口上,即建立单一接口,而不是臃肿庞大的接口,这个原则增强了类的可维护性和可扩展性;
5.依赖倒置原则(Dependency inversion principle),指高层模块依赖抽象类或接口,而不是具体的类,这个原则增强了类的可扩展性。
SOLID 原则处理的是面向对象中的类、接口、继承等元素,能够更好地满足面向对象设计中的非功能性需求。
2. 微服务设计的 IDEALS 原则
如今,随着微服务架构的广泛应用,SOLID 原则并不能完全适用于分布式系统中的**“微服务”**这一元素。Paulo Merson 提出了一套微服务设计的核心原则,即 IDEALS 原则。IDEALS 是这几个原则首字母组成的缩略词。原则如下:
接口分离(Interface segregation) 指的是不同类型的客户端,如,移动应用程序、Web应用程序、CLI程序,能够通过适合其要求的协议与服务端交互;
可部署性(Deployability (is on you)) 指的是在微服务时代,也就是 DevOps 时代,开发人员需要在打包、部署和运行微服务方面做出关键的设计决策和技术选择;
事件驱动(Event-driven) 指的是在任何时候,都应该通过异步消息或事件进行调用,而不是同步调用;
可用性胜于一致性(Availability over consistency) 指的是最终用户更看重系统的可用性而不是强一致性,他们对最终一致性也很满意;
松耦合(Loose coupling) 指的是服务之间相互依赖的程度,对于基于服务的系统来说,传入耦合与传出耦合的设计是一个重要问题;
单一责任(Single responsibility) 指的是服务内聚性,一个服务只包含单一职责,功能庞大服务在维护和部署方面都会变得繁重。
SOLID 原则和 IDEALS 原则是在进行系统架构设计时需要遵循的核心原则,可以有效地提高系统在非功能性方面的质量要求。
非功能需求的测试
前面也提到,非功能需求是验证系统是否满足要求的衡量标准。这里以**“响应时间”**为例,简单介绍一下非功能需求的测试方法。
定义基准
目标明确的基准式测试有利于判断局部代码优化为系统带来的具体影响,例如,在添加数据缓存后,与基准数据比较,响应时间到底提升了多少。
定义场景
把系统的一个具体使用场景作为一个测试。不同使用场景对测试的要求和验收标准不一样,同样是响应时间这个指标,当数据量在 1000 和 1000 万时的标准是不一样的,当并发用户数在 1000 和 1000 万时的标准也是不一样的。
定义成功与失败
测试的成功与失败与场景也是强相关的,成功与失败不过是当前测试值与目标基准值的一个比较。因此,成功与失败不是关键,基于场景的基准值的设定是关键。该值的设定可以通过“预估+核验”的方式,在开始时先预估一个初始值作为最低验收标准,当测试通过了最低验收标准,就把验收标准提高一点,调整该测试的成功门槛。如果测试失败,如果最低验收标准高于需求中定义的门槛,那么只要降低最低验收标准是被接受的,直接降低就可以了,否则就要优化代码直到满足最低验收标准。
非功能需求的监控
当系统在生产环境运行时,对系统的监控是也是很重要的一部分。有一些开源APM(Application Performance Management,应用性能管理)工具能够追踪每个请求的完整调用链路,收集调用链路上的每个服务的性能数据,并通过页面进行展示。常用的有Skywalking、Zipkin、Pinpoint 等。如下图是 Pinpoint 的监控界面。
图中右侧显示的是 ApiGateway 这个服务的响应时间汇总信息,响应时间在 1s 内的有 600 个,大于 5s 的有 41 个,还可以针对每个请求查看完整的调用链,对于排查慢请求的问题非常有帮助。
总结
本课时从非功能需求会导致哪些问题入手,描述了目前软件开发中过于关注功能需求,而忽略非功能需求的现象,从而提议软件开发人员在进行软件开发时要关注非功能需求。并在随后的内容里,从非功能需求的管理、分析、度量、设计、测试和监控等方面,简单而全面地描述了如何关注非功能需求。由于篇幅有限,并不能面面俱到,以非功能需求作为一个课题来讲,也是希望软件从业人员能够对非功能需求引起重视,开发出满足用户需要的、“可工作” 的软件。