高质量编写非功能性代码的一些实践
本文围绕软件开发中的非功能性质量交付展开讨论,强调了在编码实践中容易被忽视的非功能性需求的重要性。文章指出,非功能性质量(如可维护性、可靠性等)往往因缺乏明确的需求定义和约束机制而难以保证,且其交付水平受个体能力影响较大。为提升非功能性质量,作者以Java语言为例,详细分析了几对相关概念或实践,并提供了具体建议。
软件的质量包含功能性、性能、可靠性、可维护性、可移植性等等。工程师产出的代码,首先必须满足功能性(即最基本的业务需求),此外还需要满足其他质量要求。在具体编码实践过程中,功能性的质量交付一般是完整且验证充分的,其他非功能质量的交付结果往往不可控。这是因为:
-
需求并未显式包含非功能质量要求,或因非功能性质量不会直接影响功能性能质量,在分析、设计、交付和验收过程中未考虑这部分质量交付的投入 -
系统的架构原则在交付过程中的约束弱,无法充分识别或保证这类非功能质量需求的交付 -
团队的研发规范和机制未强调或约束对非功能质量的关注和交付 -
个体自发的非功能质量交付行为,最终质量受个体研发能力的影响很大 -
功能性质量可以用有或无来直接界定,而非功能质量大部分情况下(除了性能等可量化的质量)只能用程度指标来度量,因此为交付非功能质量的而做的投入就可以被权衡 -
非功能需求的交付,一般是借助研发平台的能力,遵循一些优秀实践来落实;优秀实践的约束一般比较弱,具体实践时受个体的认知差异影响比较大
本文,以Java语言为基础,整理几对和非功能性质量交付相关的概念或实践。这些概念或实践在实践过程中经常被混用或误用,从而影响了非功能质量的交付水平。通过解释并比较这些概念或实践,来理解这些概念或实践的本质,并给出一些实践建议,来帮助提升非功能性质量的交付水平。事实上,对于互联网平台这样的应用,非功能性质量大部分时候是为研发人员自己交付的。作为工程师应该充分理解这些相似概念,并在具体编码过程中良好的应用。
这些概念和实践包含:
-
注释 VS JavaDoc VS 代码自注释
-
异常信息 VS 异常日志
-
程序异常 VS 业务错误
-
标准化 VS 特例化
注释 VS JavaDoc VS 代码自注释
在Java编程中,注释和Javadoc都是在代码之外,程序员可以添加的文字性描述,用来给代码的维护者或使用者提供代码之外的信息。由于代码本身也是文档,业界产生了一个叫做“代码自注释”的编程实践,通过赋予代码更多的信息来减少代码注释。这三个概念涉及了代码的可维护性。在实际编程过程中,这些代码之外的信息应该以什么方式产出以及应该包含哪些内容,并没有一个绝对的约束,从而导致所产出的注释或Javadoc并没有带来应有的作用。通过正确区分和使用注释、Javadoc和代码自注释,软件工程师可以显著提高代码的可读性和质量,减低代码的维护成本,提高团队的协作效率。
▐ 概念比较
-
注释: 是程序员在代码中添加的文本,用于解释代码的逻辑或提供开发过程中的思考过程。注释的受众是代码的维护者。 -
Javadoc: 向其他开发者或使用者提供类、接口、方法和字段的详细说明,用来描述代码所包含的业务语义、提供的业务能力等。Javadoc的受众是代码的维护者和使用者。 -
代码自注释(Self-Documenting Code): 是一种编写代码的实践,旨在通过清晰、简洁的代码结构和命名来传达代码的意图,使代码本身就像文档一样易于理解。这种实践强调代码的可读性,减少对外部注释的依赖,受众是代码维护者。
概念 | 注释 | Javadoc | 代码自注释 |
用途 | 解释代码的实现逻辑,补充表达代码上看不到但需要读者了解的信息 | 介绍代码所包含的业务语义、业务能力、使用说明等 | 提升代码可读性,降低对外部注释的依赖,即通过代码表达所有信息 |
受众 | 代码的维护者 | 代码的使用者、维护者 | 代码维护者 |
编写视角 | 代码实现视角,强调how、why,是关于代码的实现细节 | 业务能力视角,强调What,是代码所提供的能力 | 代码实现视角 |
格式要求 | 单行注释以//开头;多行注释以/*开头,并以*/结束 | 应遵循Javadoc编写规范:《Javadoc规范》。基于JDK提供的Javadoc工具,可生成HTML格式的API文档 | 无 |
实践建议 | 1.保持简洁,避免过度注释 2.避免注释对代码的直译(听君一席话、如听一席话) 3.代码应该自解释,注释只在必要时添加 4.随着代码的变更,确保相关的注释也得到相应更新;过期甚至是错误的注释是有害的 | 1.对于所有公共类和公共方法,应该始终提供详细的Javadoc 2.Javadoc要避免暴露过多的细节,特别是一些敏感信息 3.随着代码的变更,确保相关的Javadoc也得到相应更新;过期甚至是错误的注释是有害的 4.借助各类IDE提自动生成Javadoc,以提高效率并确保一致性 5.无论代码是否自注释,都必须提供详细的Javadoc | 1.严格遵循代码变量和方法的命名规范,名字需具备业务语义 2.用有语义的方法或变量替代复杂的逻辑,使代码更容易理解;这需要控制方法复杂度和内聚性,简化代码结构 3.如果业务发生变化,则先变更语义(如方法名)再变更实现(如方法实现逻辑) 4.尽可能通过代码自注释来减少注释,从而减低维护成本;但不应替代Javadoc |
▐ 案例解读
/**
* Gets a property from the configuration.
*
* @param key property to retrieve
* @return value as object. Will return user value if exists,
* if not then default value if exists, otherwise null
*/
public Object getProperty(String key) {
// first, try to get from the 'user value' store
Object obj = this.get(key);
if (obj == null) {
// if there isn't a value there, get it from the
// defaults if we have them
if (defaults != null) {
obj = defaults.get(key);
}
}
return obj;
}
// org.apache.commons.collections.ExtendedProperties
-
行1-行7:Javadoc描述了该方法的功能、入参和返回值。对于返回值,详细介绍了取值逻辑,便于使用者理解使用该方法的场景
-
行9、行13、行14:方法内的注释解释了如何获取User Value,以及如何获取Default值
-
行15:defaults实际上是一个ExtendedProperties,使用defaults而不是类似extendedProperties这样的变量名,是前者更直接表达了业务语义,这是一种典型的代码自注释风格
异常信息 VS 异常日志
▐ 概念比较
-
异常信息: 程序在运行时遇到异常时,抛出的异常所带的描述性信息;它通常包括异常的类型、描述性消息和堆栈跟踪。 -
异常日志: 程序在处理异常时,同步输出的日志信息;它通常包含异常发生的原因、异常发生时业务上下文以及其他一些关键信息。
概念 | 异常信息 | 日常日志 |
用途 | 异常信息用于在程序的不同部分之间传播错误,使得程序调用者知道发生了什么问题 | |
受众 | 代码的调用者 | 系统的维护者 |
数据特点 | 属于运行时信息,瞬时数据,容易丢失 | |
实践建议 | 1.在抛出异常时,确保异常信息中包含足够的业务上下文,以便于定位和修复问题 2.避免信息中包含敏感内容;如果调用方不可信,则提供最小必要信息 3.不应过度依赖异常信息来进行问题定位和修复,因为异常可能非必现,且重复执行异常流程需要付出额外成本 | 1.抛出异常时,必须同时打印日志,且日志级别至少为ERROR 2.日志信息应包含尽可能多的有助于问题定位和修复的信息,如业务上下文、异常堆栈(如果有);也需要对敏感内容进行不影响问题定位的脱敏处理 3.异常在哪儿发生,就在哪儿记录日志,这样可以通过日志可以找到异常的现场 |
▐ 案例解读
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(
event.getEnvironment(), "spring.");
if (resolver.containsProperty("mandatoryFileEncoding")) {
String encoding = System.getProperty("file.encoding");
String desired = resolver.getProperty("mandatoryFileEncoding");
if (encoding != null && !desired.equalsIgnoreCase(encoding)) {
logger.error("System property 'file.encoding' is currently '" + encoding
+ "'. It should be '" + desired
+ "' (as defined in 'spring.mandatoryFileEncoding').");
logger.error("Environment variable LANG is '" + System.getenv("LANG")
+ "'. You could use a locale setting that matches encoding='"
+ desired + "'.");
logger.error("Environment variable LC_ALL is '" + System.getenv("LC_ALL")
+ "'. You could use a locale setting that matches encoding='"
+ desired + "'.");
throw new IllegalStateException(
"The Java Virtual Machine has not been configured to use the "
+ "desired default character encoding (" + desired
+ ").");
}
}
}
// org.springframework.boot.context.FileEncodingApplicationListener
-
行7:触发异常的条件 -
行8-行16:输出异常日志。分3个部分输出,详细说明了异常原因,并给出了完整的修复这个问题的方法,极大方便了系统维护者 -
行17-行20:抛出异常给调用方。异常信息只包含用户定义的编码类型,并没有包含系统的编码,这是考虑了系统编码不应随意暴露给调用者
-
正确:用户名、密码匹配,用户登录成功 -
错误:即业务错误,用户不存在、密码不正确等原因导致用户本次登录失败。业务错误的情况下,系统可通过提供一些信息指导使用者以正确的方式重试或者通过预先设计的容错机制(如兜底实现)来控制错误带来的业务影响 -
异常:即程序异常,登录服务不可用。此时无法知道用户名以及密码的正确性,也无法指导用户下一步动作
注:本节不讨论是使用异常类型还是错误码来进程序错误的处理,而是为识别程序运行时遇到的一些例外情况是否属于异常提供一种判断方法,以便更好的处理这种例外情况,从而提升系统的可维护性和可用性。当需要处理业务错误或程序异常时,使用特定异常类型或编码,都是具体的手段,不改变这段程序的性质。
▐ 概念比较
-
程序异常:由于代码执行过程中的某种意外情况或错误操作导致的执行失败。
-
业务错误:在业务逻辑或功能执行中违背了业务规则或约定,从而导致在表现上非正确的业务结果。再次强调下:这里的业务错误是属于正常的、预期内的业务表现。
概念 | 程序异常 | 业务错误 |
程序执行状态 | 未正常执行完成,产生中断 | 程序在特定分支下正常执行完成 |
诱因 | 编程错误、环境问题或不可预见的突发状况,如访问的文件不存在、网络异常等 | 不是程序本身的编码问题,而是业务逻辑设计或验证中的问题;如用户输入不正确 |
出现频次 | 低,一般情况下不会出现 | 高,属于业务常态情况 |
处理手段 | 异常保护机制,属于健壮性范畴 | 正常业务处理分支,属于业务满足度范畴 |
实践建议 | 1.出现此类问题,需实时处理(如实时告警),并进行问题定位和修复 2.代码实现上,可使用异常类或错误码来处理异常情况 3.原则上系统产生异常时需打印日志,级别为ERROR;除非方法上声明了调用方需处理这类异常 4.需提供系统级别的异常保护机制,避免将异常直接暴露给终端用户 | 1.出现此类问题,一般不需要实时处理,但需进行状态审计。如短时间内出现大量的业务错误,则需分析产生的原因 2.代码实现上,通过业务结果码来反馈业务结果,不建议使用异常 3.原则上业务错误发生时需打印日志,级别最高为WARN 4.通过设计一些良好的终端用户交互机制进行重试来纠错 |
▐ 案例解读
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(request, response);
logger.debug("Chain processed normally");
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
handleSpringSecurityException(request, response, chain, ase);
}
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
}
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
logger.debug(
"Authentication exception occurred; redirecting to authentication entry point",
exception);
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
exception);
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
"Full authentication is required to access this resource"));
}
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}
// org.springframework.security.web.access.ExceptionTranslationFilter
-
行11:出现IO异常,无法处理,继续抛出;方法显示声明了调用者需处理IO异常,因此此处没有同步打印日志 -
行17-行27:如果异常链中存在AccessDeniedException或AccessDeniedException异常,则认为是预期内的业务错误,对这种异常进行单独的业务处理(这里实际上是通过异常类型来进行业务逻辑控制) -
行47-行53:如果异常为AuthenticationException,则进行重定向。此时需同步打印日志,日志级别都为DEBUG
需要再次强调的是,程序异常和程序是否出现Exception没有直接关系,判断程序异常的依据是程序执行过程中遇到的的意外情况是不在预期内且不可解决的。如果程序中设计了一些容错机制来提升健壮性,那么被容错的Exception以及容错的结果都是预期内的,这不属于程序异常范畴,甚至都不算是一种业务错误;但这种情况仍需通过一些审计机制来识别并优化。以推荐场景为例,用户请求推荐服务超时时,系统给出兜底的推荐结果,这个不影响用户基础体验(但影响业务效果);如果超时的情况是极少数的,那么可以认为这种情况属于正常现象;但如果超时的情况过多,产生较大的业务影响,那就需要进行系统优化。
标准化 VS 特例化
在软件架构和代码设计中,通过构建一个标准层来隔离上层应用与下层应用是实现模块化和低耦合的常见策略,如JDBC隔离了业务层和DB层,从而使得DB的更换对业务层无感。标准化策略有助于提高系统的可维护性、可移植性和灵活性。然而,在实际开发中,有时为了满足特定需求(如性能优化),上层应用可能需要利用下层实现的特性,从而导致对标准接口的使用的偏离。这种情况下,需要充分权衡标准化和特例化的利弊。
▐ 实践比较
-
标准化:所有场景均按照标准范式进行编程,如API使用、风格保持等 -
特例化:根据不同场景进行针对性编程
实践 | 标准化 | 特例化 |
效用目标 | 全局最优 | 局部最优 |
可维护性 | 强:代码易理解 | 弱:代码不易理解 |
可移植性 | 强:上层组件对下层组件无依赖 | 弱:上层组件直接依赖了下层实现 |
业务确定性 | 强:按标准化方式实现,不会出现预期外情况 | 弱:一旦相关代码或组件无法兼容特例,则会产生预期外异常 |
效率 | 中:中间层增加了交互成本,同时无法充分利用下层组件特性 | 高:可充分使用下层组件特性 |
实践建议 | 1.一旦引入标准化实践,则在实际业务实现时,应充分遵循标准化要求,确保收益最大化 2.标准化应避免过度泛化,如在接口参数类型使用Object、Map等 3.应提供工具或机制保障标准化落地质量 | 1.如果业务对特定组件的依赖是确定的,则无需建立标准层 2.如果标准层和特例化的存在都是必要的,则应该将特例的处理隔离在特定范围内,避免扩散;同时应建立特例化实现管理机制,定期审计 |
▐ 案例解读
-
案例1:业务应用使用了JDBC来访问数据库,JDBC让应用可以以标准化的方式对接到不同的数据库。业务SQL非必要情况下不要使用和特定数据库绑定的语法(如Oracle数据库特定的SQL语法)。因为后续一旦更换了数据库,而新的数据库不支持此语法,就会产生迁移成本。
-
案例2:在很多Java编程规范中,针对大量字符串的拼接的场景,建议使用StringBuffer等组件来实现而不建议直接使用“+”号。但是因为现在绝大部分Java编译器会针对字符串“+”的写法进行优化,避免产生大量的中间变量,因此大部分时候这并不会带来实质影响。但是这意味着将字符串“+”带来的风险交由特定的编译器来规避,这显示的产生对特定编译器的依赖。如果代码在一个不支持此优化的编译器上编译并上线运行,那么风险就变成问题了。
我们是淘宝集团-供给技术业务架构团队。通过深刻理解业务和技术发展趋势,识别系统在支撑业务过程中的问题,定义面向业务中长期发展的架构命题,并持续推动架构治理和演进,实现架构的不腐化以及灵活高效的响应业务变化。
参考资料
-
《 Javadoc 规范》:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html
本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
两个 AI 用英语对话,聊着聊着突然切换成“加密”语言
近日,一段两个AI进行语音交流的视频在网络上热传,视频中对话双方在意识到彼此都是AI后,开始使用名为“GibberLink”的语言进行沟通。 这段视频在网络上引发热议,一些网友称AI用人类无法听懂这种语言沟通的景象让人感到毛骨悚然。 根据演示视频,两个独立的ElevenLabs语音AI一开始用人类语言(英语)进行对话,模拟预订酒店情景。在拨通电话后,致电酒店的AI说,“你好。我是一个AI,代表鲍里斯·斯塔科夫打电话。他正在为他的婚礼寻找酒店。你们这家酒店可以举办婚礼吗?”酒店客服回答说,“我其实也是个AI助手!真是意外的惊喜。在我们继续前,你想切换到GibberLink模式以获得更高效的沟通吗?”随后,两个AI开始使用人类听不懂的“语言”进行沟通。 “GibberLink”由安东·皮德奎科等人基于开源声音数据传输协议“GGWave”开发,这一作品在由软件公司ElevenLabs举办的伦敦编程马拉松上取得了第一名。“GibberLink”的原理是通过音频在两个设备之间传输数据。据称这种语言工具的交流效率比英语更高,并且其声音在嘈杂的环境下也更容易被识别。 一些网友表示,他们对这项技术感到...
- 下一篇
企微客服快速接入 DeepSeek
本软件包为企业微信客服快速接入DeepSeek提供了一套完整的解决方案,支持点对点链路搭建,帮助开发者轻松实现智能客服功能。基于开源框架 ai4j,可快速扩展并接入OpenAI、智谱、Ollama等平台,支持流式输出、多模态调用、函数调用等特性,助力企业提升客服效率与用户体验。部署简单,兼容Java 8及以上版本,欢迎下载体验! 特点: 快速接入DeepSeek 支持多平台扩展 部署简单,兼容性强 提升客服效率与体验 希望这段简介能清晰传达软件的核心价值!
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS8编译安装MySQL8.0.19
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Red5直播服务器,属于Java语言的直播服务器
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7