为啥你写的代码老是这么复杂?

摘要:有句话说得很好,“代码质量决定生活质量”,当你把软件的复杂性下降了,bug减小了,系统可维护性更高了,天然也就带来了更好的生活质量。

本文分享自华为云社区《写出的代码复杂度过高?看下专家怎么说》,原文做者:元闰子 。程序员

前言

在进行软件开发时,咱们经常会追求软件的高可维护性,高可维护性意味着当有新需求来时,系统易扩展;当出现bug时,开发人员易定位。而当咱们说一个系统的可维护性太差时,每每指的是该系统太过复杂,致使给系统增长新功能时容易出现bug,而出现bug以后又难以定位。数据库

那么,软件的复杂性又是如何定义的呢?编程

John Ousterhout给出的定义以下:安全

Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.

可见,软件的复杂性是一个很泛的概念,任何使软件难以理解和难以修改的东西,都属于软件的复杂性。为此,John Ousterhout提出了一个公式来度量一个系统的复杂性:多线程

式中,pp表示系统中的模块,c_{p}cp​表示该模块的认知负担(Cognitive Load,即一个模块难以理解的程度),t_{p}tp​表示在平常开发中在该模块花费的开发时间。架构

从公式上看,一个软件的复杂性由它的各个模块的复杂性累加而成,而 模块复杂性 = 模块认知负担 * 模块开发时间,也就是模块的复杂性即和模块自己有关,也跟在该模块上花费的开发时间有关。须要注意的是,若是一个模块很是难以理解,可是后续开发过程当中几乎没有涉及到它,那么它的复杂性也是很低的。函数

致使软件复杂的缘由

致使软件复杂的缘由能够细分出不少种来,而归纳起来莫过于两种:依赖(dependencies)  隐晦(obscurity)。前者会让修改起来很费劲并且容易出现bug,好比当修改模块1时,每每也涉及到模块二、模块三、... 的改动;后者会让软件难以理解,定位一个bug,甚至是仅仅读懂一段代码都须要花费大量的时间。测试

软件的复杂性每每伴随着以下几种症状:编码

霰弹式修改(Change amplification)。当只须要修改一个功能,但又不得不对许多模块做出改动时,咱们称之为霰弹式修改。这一般是由于模块之间耦合太重,相互依赖太多致使的。 好比,有一组Web页面,每一个页面都是一个HTML文件,每一个HTML都有一个背景属性。因为各个HTML的背景属性都是分开定义的,所以若是须要把背景颜色从橙色修改成蓝色时,就须要改动全部的HTML文件。url

认知负担(Cognitive load)。当咱们说一个模块隐晦、难以理解时,它就有太重的认知负担,这种状况下每每须要读者花费大量时间才能明白该模块的功能。好比,提供一个不带任何注释的calculate接口,它有2个int类型的入参和一个int类型的返回值。从该函数的签名上看,调用者根本没法得知函数的功能是什么,他只能经过花时间去阅读源码来肯定函数功能后才敢去调用该函数。

int calculate(int val1, int val2);

不肯定性(Unknown unknowns)。相比于前两种症状,不肯定性的破坏性更大,它一般指一些在开发需求时,你必须注意的,可是又无从得知的点。它经常是由于一些隐晦的依赖致使的,会让你在开发完一个需求以后感受内心很没谱,隐约以为本身的代码哪里有问题,但又不清楚问题在哪,只能祈祷在测试阶段可以暴露而不要漏洞商用阶段。

如何下降软件的复杂性

对 “战术编程” Say No!

不少程序员在进行特性开发或bug修复时,关注点每每是如何简单快速让程序跑起来,这就是典型的战术编程(Tactical programming)方法,它追求的是短时间的效益——节省开发时间。战术编程最广泛的体现就是在编码以前没有进行模块设计,想到哪里就写到哪里。战术编程在系统前期可能会比较方便,一旦系统庞大起来、模块之间的耦合变重以后,添加或修改功能、修复bug都会变得步履维艰。随着系统变得愈来愈复杂,最后不得不对系统进行重构甚至重写。

与战术编程相对的就是战略编程(Strategic programming),它追求的是长期的效益——增长系统可维护性。仅仅是让程序跑起来还不足以知足,还须要考虑程序的可维护性,让后续在添加或修改功能、修复bug时都可以快速响应。由于考虑的点比较多,也就注定战略编程须要花费必定的时间去进行模块设计,但相比于战术编程后期致使的问题,这一点时间也是彻底值得的。

让模块更“深”一点!

一个模块由接口(interface)和实现(implementation)两部分组成,若是把一个模块比喻成一个矩形,那么接口就是矩形顶部的边,而实现就是矩形的面积(也能够把实现当作是模块提供的功能)。当一个模块提供的功能必定时,深模块(Deep module)的特色就是矩形顶部的边比较短,总体形状高瘦,也即接口比较简单;浅模块(Shallow module)的特色就是矩形顶部的边比较长,总体形状矮胖,也即接口比较复杂。

模块的使用者每每只看到接口,模块越深,模块暴露给调用者的信息就越少,调用者与该模块的耦合性也就越低。所以,把模块设计得更“深”一点,有助于下降系统的复杂性。

那么,怎样才能设计出一个深模块呢?

  • 更简单的接口

简单的接口比简单的实现更重要,更简单的接口意味着模块的易用性更好,调用者使用起来更方便。而简单的实现 + 复杂的接口这种形式,一方面影响了接口的易用性,另外一方面则加深了调用者与模块的耦合。所以,在进行模块设计时,最好遵照“把简单留给别人,把复杂留给本身”的原则。

异常也属于接口的一部分,在编码过程当中,应该杜绝没通过处理,就随意将异常往上抛的现象,这样只会增长系统的复杂性。

  • 更通用的接口

在设计接口时,你每每有两种选择:(1)设计成专用的接口;(2)设计成通用的接口。前者实现起来更方便,并且彻底能够知足当前的需求,但可扩展性低,属于战术编程;后者则须要花时间对系统进行抽象,但可扩展性高,属于战略编程。通用的接口意味着该接口适用的场景不止一个,典型的就是“ 一个接口,多个实现 ”的形式。

有些程序员可能会反驳,在没法预知将来变化的状况下,通用就意味着过分设计。过分通用确实属于过分设计,但对接口进行适度的抽象并非,相反它可使系统更有层次感,可维护性也更高。

  • 隐藏细节

在进行模块设计时,还要学会区分对于调用者而言,哪些信息是重要的,哪些信息是不重要的。隐藏细节指的就是只给调用者暴露重要的信息,把不重要的细节隐藏起来。隐藏细节一则使模块接口更简单,二则使系统更易维护。

如何判断细节对于调用者是否重要?如下有几个例子:

一、对于Java的Map接口,重要的细节:Map中每个元素都是由<Key, Value>组成的;不重要的细节:Map底层是如何存储这些元素、如何实现线程安全等。

二、对于文件系统中的read函数,重要的细节:每次读操做从哪一个文件读、读多少字节;不重要的细节:如何切换到内核态、如何从硬盘里读数据等。

三、对于多线程应用程序,重要的细节:如何建立一个线程;不重要的细节:多核CPU如何调度该线程。

进行分层设计!

设计良好的软件架构都有一个特色,就是层次清晰,每一层都提供了不一样的抽象,各个层次之间的依赖明确。无论是经典的Web三层架构、DDD所提倡的四层架构以及六边形架构,抑或是所谓的Clean Architecture,都有着鲜明的层次感。

在进行分层设计时,须要注意的是,每一层都应该提供不一样的抽象,并要尽可能避免在一个模块中出现大量的Pass-Through Mehod。好比在DDD的四层架构中,领域层提供了对领域业务逻辑的抽象,应用层提供了对系统用例的抽象,接口层提供了对系统访问接口的抽象,基础设施层则提供对如数据库访问这类的基础服务的抽象。

所谓的Pass-Through Mehod是指那些“在函数体内直接调用其余函数,而自己只作了极少的事情”的函数,一般其函数签名与被其调用的函数签名很相似。Pass-Through Mehod所在的模块一般都是浅模块,让系统增长了无谓的层次和函数调用,会使系统更加复杂:

public class TextDocument ... {
  private TextArea textArea;
  private TextDocumentListener listener;
  ...
  public Character getLastTypedCharacter() {
    return textArea.getLastTypedCharacter();
  }
  public int getCursorOffset() {
    return textArea.getCursorOffset();
  }
  public void insertString(String textToInsert, int offset) {
    textArea.insertString(textToInsert, offset);
  }
  ...
}

学会写代码注释!

注释是软件开发过程当中的性价比极高的一种手法,它只须要花费20%的时间,便可获取80%的价值。它能够提升晦涩难懂的代码的可读性;能够起到隐藏代码复杂细节的做用,好比接口注释能够帮助开发者在没有阅读代码的状况下快速了解该接口的功能和用法;若是写的好,它还能够改善系统的设计

具体如何写好代码注释,参考《如何写出优秀的代码注释?》一文。

总结

软件的复杂性是咱们程序员在平常开发中所必须面对的东西,学会如何 “弄清楚什么是软件复杂性,找到致使软件复杂的缘由,并利用各类手法去打败软件的复杂性” 是一门必备的能力。有句话说得很好,“代码质量决定生活质量”,当你把软件的复杂性下降了,bug减小了,系统可维护性更高了,天然也就带来了更好的生活质量。

模块设计是下降软件复杂度最有效的手段,学会使用“战略编程”的方法,并坚持下去。咱们经常提倡“一次把事情作对”,但这对于模块设计而言并不适用,几乎没有人能够第一次就把一个模块设计成完美的模样。二次设计是一个很是有效的手法,与其在系统腐化以后再花大量时间进行重构或重写,还不如在第一次完成模块设计后,再花点时间进行二次设计,多问问本身:是否有更简单的接口?是否有更通用的设计?是否有更简洁高效的实现?

"罗马不是一天建成的",下降软件的复杂性也同样,贵在坚持。

 

点击关注,第一时间了解华为云新鲜技术~

相关文章
相关标签/搜索