写出高质量软件:服务端代码质量概要
质优制胜。
软件品质概述
提供同样功能、产品和服务的服务者中, 竞争力来自功能的多样化和服务品质的差异化, 无论是个体、企业还是国家。
-
这里的服务指功能、产品的实现程度和处理能力,以及研发/客服提供的技术支持程度(7*24, 随时响应, 沟通便捷,快速解决,温馨提示,有效指南等)。
-
从某种意义来说, 一切皆服务。 功能和产品只是形式, 服务才是本质。服务响应某种需求从而具备存在价值。个体、企业为社会提供某种类型、某种程度的服务,并获得相应回报。
程序员提供的服务是,在特定的工作环境和企业文化中,运用可用的资源以及自己的知识、技能、经验、时间、精力、资源,在适当可接受的开发成本下,通过将现实问题构造和组织成正确的逻辑流而获得问题的解决并持续维护的能力程度。
- 这种能力程度既包括服务能力本身,也包括通过这种能力所创造的服务的能力。 服务能力本身主要包括: 精通特定语言、技术、平台、架构等的技术深度; 组合技术、产品、运营、服务、商业的综合认知广度;具备多样化技能(开发、写作、外语、沟通、组织、谈判等);做事的能力素养,耐心、专注、缜密、魄力、幽默等; 具备非技术方面的特长,比如体育、音乐、文学、艺术、设计等。 通过这种能力所创造的服务, 通常是指软件、产品或服务。
每次解决用户最关心的前三个问题, 不为无关痛痒的事情浪费时间、精力和资源。
- 首先是功能与服务,功能与服务是核心,是存在的价值;
- 其次是设计,设计使功能与服务更加突出具有吸引力,更好的使用品质;
- 接着是成本和传播。保证优秀功能和设计的基础上降低资源时间和人力成本,同时良好的传播和使用让边际成本更低。
如何保证服务端开发的功能和服务达到应有的品质呢?
- 明确需要达成的品质; 知道做什么以及如何做;
- 将服务品质尽可能量化并进行测量,做到服务品质透明化;
- 更倾向于情感因素的服务品质,采用情感化方法处理。不在本文讨论范围内。
要写出高质量软件,首先需要对软件质量属性有一个综观概览。
基本质量属性
正确性
软件是否正确实现了指定的功能, 满足客户的多样化需要。
- 在外部展现上,符合用户的心智和操作习惯。
- 在内部实现上,是数据记录是否正确完整地构造、存取或销毁,且没有产生脏数据和无效数据。
- 需要建立功能实现的整个流程环路,对其中的所有细节知根究底,对交互的外部系统提供的相关服务也了如指掌。
- 量化: 业务数据是否合理的存取;若涉及多个子系统,则需要保证数据的一致性。
相关建议
- 遵循 KISS 原则,在满足需求的前提下,尽量保持简单性,不引入不必要的复杂性。
- 沟通, 准确理解需求、场景及业务流;
- 绘制业务流程图、子系统交互图,确定数据表设计,遵循意图导航编程方法,通过分解、实现和连接若干紧密联系的逻辑单元而实现。
- 重视并仔细做好存储设计,遵循数据库范式,少量冗余字段。
- 仔细挑选开发使用的工具箱、库与框架。
- 常用子任务使用主流开发库并仔细测试。
- 完整理解API, 包括其功能、原理、适用场合与局限性。
- 编写类/方法/函数时注明使用契约及特殊处理。
- 每个类/方法/函数各司其职,相互协作。
性能成本
软件在指定时间和可用资源的前提下实现功能和服务的响应能力和请求处理能力。 需要有误差范围的测量或估算。
- 构造大量串行请求测量平均响应时间;构造大量并发请求测量吞吐量。
- 可参考 SLA (服务等级协议)。 程序的执行效率应当在可接受的开发成本下达到最佳。
- 这就是说,在 "开发功能所需要的时间和资源成本" 与 "最终实现的服务能够提供的品质" 存在一个权衡。
- 一般来说,品质越高的服务所需要的时间和资源成本越高;无止境追求更高质量和效率而不计成本和服务目标是不可取的。
- 比较理想的情况是: 通过适量改进、技术革新、管理改善等,可以使得花费较少的时间和资源成本能够获得更高的服务品质。
- 量化: 运行时间、 所需的存储资源(内存、数据库记录数、KV 数、缓存、文件)、 响应时间、吞吐量/并发能力(QPS, TPS, UPS)。
相关建议
- 选择合适的数据结构和算法。
- 建立合适的索引(最左匹配准则)。
- 耗时长/频繁读操作/重复计算适当利用缓存。
- 大数据量结果集使用多线程技术获取子集并汇总。
- 多IO任务使用异步方式来处理。
- 耗时长的任务进行分解/并发/异步处理。
- 避免不必要的重复创建/计算开销。
- 耗内存的大对象的延迟创建与复用。
- 测量时间,对热点区域进行集中优化。
- 学会使用性能分析工具,比如arthas, Jprofiler,MAT等。
- 使用更好的硬件(更多的核、更大的内存)提升性能。
- 减少启动加载组件,按需加载和缓存。
- 使用异步模式逐步加载与展示。
- 精简流程。
- 调整参数。
健壮性
软件应对错误的能力。能够在非预期环境降级运行,在预期环境能够捕获所有的错误条件(前提不符合、非预期输入、脏数据、恶意数据等)和异常情况并给出合适的返回信息。
- 对于前端交互而言,需要友好容易理解的提示信息; 对于后台服务而言, 需要规范的错误码和错误消息。
- 软件健壮性的提高需要大量的错误检测,容易导致维护性降低。最好将错误检测从主业务流程中抽离出来,建立可复用的检测库函数。
- 建议先建立正确完整的业务主流程,然后对于主流程的每个环节进行推敲,找出各种错误场景并进行处理。
- 量化: 错误场景测试用例数;覆盖场景数。
相关建议
- NPE、数据越界、JSON解析出错是常见的健壮性问题。避免这三类问题,就能做较好的健壮性。
- 防御式编程。操作之前,先看有木有,是否成功。
- 对于各个模块,制定不同的错误码和错误消息;
- 错误码和错误消息使用配置文件或枚举,与代码分离。
- 不同类型的参数检测使用不同的参数验证器(通常是注解、正则表达式)。
- 捕获所有异常,避免“吞吃”异常(捕获异常后什么都不做)。
- 使用全局统一的错误检测函数尽量在一个地方集中检测错误并返回错误信息给前端。
- 细分应用中出现的各种异常并进行不同的处理。
- 业务异常自行处理, 底层通用异常转译给业务层具体分析具体解决。 `
易追踪性
程序运行过程中输出适量的日志信息,便于追踪程序执行状态,快速排查错误【内部品质】。
- 识别业务流程的关键路径和关键状态,记录其运行时状态便于分析。
- 需要格式良好规范的适量且关键的 INFO 日志和 Error 日志。
- 量化: 排查、定位每个问题的所需时长。
相关建议
- 生产环境的日志尽量少而精。因日志打印过多导致线上出故障的例子还是有的。
- 开发环境的日志尽量详细,减少联调时间。打一行日志很廉价,但你的时间价值千金。
易测试性
主要是单元测试、自动化功能测试和性能测试。 单元测试和功能测试保证正确性,性能测试保证效率【内部品质】。
- 单元测试要求在开发过程中尽量编写具有单一职责的短小方法、类;代码对象越小,单元测试所发挥的作用越大。核心方法逻辑一定要有单测。
- 功能测试要求针对具体使用场景设计有效的测试用例、测试计划和测试执行。
- 性能测试要求对业务处理所需的时间和资源进行测量,给出最佳值、最差值和平均值。至少要对运行时间进行测量。
- 量化: 业务方法的测试覆盖度、代码行、分支覆盖程度、测试用例集合、性能测试报告。
安全性
避免泄漏重要、敏感、隐私信息; 保护业务和数据不受非法访问、非授权访问的破坏; 追踪系统访问日志。
- 常见的方法有敏感信息识别和屏蔽、访问授权机制、 访问日志审计、代码漏洞防备。
相关建议
- 用户角色与权限分级。
- 只读与操作权限分离。
- 机密数据加密保护/权限控制/操作限制/身份认证。
- 权限申请加入审批流程制度。
- 记录请求日志与用户操作日志。
- 请求连接进行加密。
- 避免显示请求 URL。
- 防止注入攻击,识别非法请求模式。
- 敏感信息脱敏处理。
可维护性品质属性
可复用性
要求软件中的函数、方法、类、库、模块、组件、框架等能够在同一工程或不同项目或不同产品中多次使用,便于替换更新【内部品质】。
- 最起码要求是在同一工程中能够尽可能复用, 避免重复代码段。
- 量化: 重复代码段数量; 函数、方法、类、库、模块、组件、框架的复用次数。
相关建议
- 遵循单一职责原则(SRP)。
- 每个方法或函数最好不超过 60 行。
- 设计与接口定义具备正交性。
- 每个事实(知识点和业务点)都有合理的抽象。
- 从业务逻辑抽离出通用技术逻辑,做成工具类并仔细测试其可靠性。
- 使用函数式和泛型编程写出具备通用功能的工具类。
- 抽离共性,编写简易微框架处理同一类需求。
- 良好的代码和设计抽象,建立通用机制来解决一类问题。
可扩展性
以较小的成本来满足多变需求的能力。
相关建议
- 遵循面向对象五原则(SRP, OCP, DIP, LSP, ISP),基于接口和语义编程。
- 使用插件架构,提供扩展点。
- 模块化、组件化,最小化公共服务接口。
- 举一反三,从相似需求、过往需求、关联需求中推断潜在的变化。
- 使用配置参数, 避免硬编码。
- 编写自包含、自封装、不修改外部状态的代码,最小化依赖。
- 使用设计模式优化代码表达结构。
- 文档化依赖关系, 定期整理、更新依赖关系。
- 表达与执行分离, 使用领域语言/规则表达逻辑, 预编译规则, 规则的动态生成、配置和加载。
可维护性
要求软件容易理解、容易做出修改以满足新的需求,通常系统应具备模块化、高内聚、松耦合的特性【内部品质】。
容易理解
- 需要可复用性的支持,同时要求代码尽可能具有语义与细节分离、自解释能力、适量注释、使用习惯用法。
- 语义与细节分离"指高层代码表达意图,低层代码呈现细节;"自解释能力"指代码通过合适的变量和函数命名能够望文生义;
- "适当的注释"需要注明代码意图及设计思路, 作为理解代码的有效指南;"习惯用法"是代码的常用模式,便于快速浏览和理解。
容易修改
需要快速定位一个功能服务所应对的代码集合,要求代码具有良好的组织和实现结构,避免对多个地方做出相同或类似的修改; 避免对原有整体设计做出大幅度修改。
- 请参考《敏捷软件开发:原则、模式和实践》一书,尽可能满足五大设计原则(SRP, OCP, LSP, DIP, ISP),实现“对修改关闭,对扩展开放”的境界。即:对于新需求的开发,尽可能仅仅是增加代码,而不需要修改原有代码。
量化: 理解一个功能需要的时长; 修改或扩展一个功能需要修改的代码行。
全局性品质属性
稳定性
程序运行中能够始终如一、持续长时间地提供正确、一致性的服务。
- 要求程序能够合理地使用和释放资源,监控程序的运行状态并及时发现异常情况解决。
- 对于高并发场景, 需要采用合适的技术进行分流,及时处理或拒绝请求,避免因为资源消耗过快过大而导致服务崩溃。
- 量化: 程序的持续可用性时长; 高并发能力。
相关建议
- 确保软件的运行依赖环境正常稳定,包括硬件、操作系统、依赖系统。
- 软件能够有效应对环境波动(比如网络中断)而不至于终止。
- 外部系统服务调用的影响局部化(熔断降级), 不影响整体运行。
- 系统组件服务模块的影响局部化(熔断降级), 不影响整体运行。
- 避免内存泄露,死循环,死锁等会导致程序崩溃的问题。
- 批量资源释放问题要仔细检查,重点关注是否有内存泄露。
- 高并发大流量下具有限流保护机制。
- 具备超时重试机制。
- 避免异常、边界、极限情形下的全表查询或全表更新。
- 使用线程池,切忌在(频繁调用的)方法动态创建单个线程去执行任务。
可用性
服务在严重错误情况下的可恢复能力和可替换能力,避免单点故障,保证可用性,保证用户数据的正确性和完整性。
- 主要包括服务的冗余、容错、备份、监控、自检测和恢复机制。
- 量化: 服务发生错误时是否可用; 是否可以从错误中恢复。
相关建议
- 服务冗余与负载均衡避免单点故障。
- 容灾应对紧急情况。
- 高负载/性能测试。
- 建立服务器/业务监控。
易用性
易用性是用户使用产品的难易程度的度量。 通常用于描述用户体验的前端领域。 不过服务端也有易用性的概念。
服务端的易用性, 深层次上体现在接口、库方法是否容易学习和使用、是否容易被滥用、误用;浅层次上体现在代码排版是否悦目易读、代码风格是否一致美观。
相关建议
- 知识与信息的准确性和丰富度,符合或超出期望。
- 操作的引导设计、正确性与舒适度。
- 外观与设计的实用美观,赏心悦目。
- 使用动画或添加趣味内容消弱等待时间的影响。
- 使用变化效果提示用户请求已经发送,正在处理。
弹性伸缩
服务可以通过简单地部署在更多机器上提升服务能力(吞吐量),可以通过减少机器部署来降低成本。
- 水平扩展: 增加多个逻辑单元资源并使他们作为一个整体在工作, 比如集群、分布式、负载均衡;
- 垂直扩展: 在同一个逻辑单位添加资源以增加容量, 比如增加CPU、内存、磁盘空间。
- 量化:并发请求处理量与机器数的比率。
相关建议
- 使用容器部署和容器编排系统。比如 Docker & K8S。
- 需要从架构设计上进行支持。
- 提供无状态服务。
可移植性
软件能够在多种运行环境、平台上一致性地正确运行而只需要做出少量修改或无需修改。
- 量化: 从一种语言移植到另一种语言或从一个平台移植到另一个平台所需要的代码行。
相关建议
- 使用可移植语言与平台,使用跨平台SDK。
- 在实现功能和服务时尽可能遵循标准接口和行为,减少非标准行为的使用。
- 在底层提供接口统一不同平台的差异性;在高层使用统一接口进行处理,避免处理平台细节。
- 遵循可移植的标准。
可运维性
服务发布后,管理大规模服务的运行、排查/诊断/解决问题的难度和速度、处理用户反馈的工作量。
- 服务发布后,可能产生的问题有哪些? 怎么排查? 服务的运行规模有多大? 并发程度如何?
- 在服务开发上,需要避免的是低级问题的产生,减少低级错误导致的工作量。
- 对于疑难问题,则需要提供关键的日志信息/运维文档便于排查。
- 对于大规模服务的运行,通常的方法是监控、报警以及可视化。
- 量化: 参考运维的量化标准。
可定制/热升级
是否容易针对客户的特殊需求进行定制;服务升级时是否可以尽量不影响到用户的正常使用。
相关建议
- 采用插件机制,模块化系统服务组件;
- 每个独立组件有一个标识, 标识其是否可显示/加载;
- 通过配置文件指定启动或初始化时可以显示/加载的模块和组件;
- 使用定时Job或有时限的缓存机制来加载配置文件;
- 使用观察者模式检测配置的变化并重新加载配置到应用中;
- 当检测配置变化时,可以发送外部命令给应用系统触发其重新加载配置;
- 提供界面修改配置文件参数并实时更新应用和配置文件(可能有安全隐患);
- 使用全局单例管理者来负责根据配置文件的变化动态显示/隐藏和动态加载/卸载模块;
- 使用SPI机制灵活替换和切换具有相同功能的模块, 使用抽象抽取其公共使用接口。
互操作性
软件与其他系统交互的难易程度。主要包括通信方式、版本兼容性。