关于code:为啥你写的代码总是这么复杂

8次阅读

共计 4225 个字符,预计需要花费 11 分钟才能阅读完成。

摘要:有句话说得很好,“代码品质决定生存品质”,当你把软件的复杂性升高了,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 时,往往也波及到模块 2、模块 3、… 的改变;后者会让软件难以了解,定位一个 bug,甚至是仅仅读懂一段代码都须要破费大量的工夫。

软件的复杂性往往随同着如下几种症状:

霰弹式批改(Change amplification)。当只须要批改一个性能,但又不得不对许多模块作出改变时,咱们称之为霰弹式批改。这通常是因为模块之间耦合过重,相互依赖太多导致的。比方,有一组 Web 页面,每个页面都是一个 HTML 文件,每个 HTML 都有一个背景属性。因为各个 HTML 的背景属性都是离开定义的,因而如果须要把背景色彩从橙色批改为蓝色时,就须要改变所有的 HTML 文件。

认知累赘(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)设计成通用的接口。前者实现起来更不便,而且齐全能够满足以后的需要,但可扩展性低,属于战术编程;后者则须要花工夫对系统进行形象,但可扩展性高,属于策略编程。通用的接口意味着该接口实用的场景不止一个,典型的就是“一个接口,多个实现”的模式。

有些程序员可能会反驳,在无奈预知将来变动的状况下,通用就意味着适度设计。适度通用的确属于适度设计,但对接口进行适度的形象并不是,相同它能够使零碎更有层次感,可维护性也更高。

  • 暗藏细节

在进行模块设计时,还要学会辨别对于调用者而言,哪些信息是重要的,哪些信息是不重要的。暗藏细节指的就是只给调用者裸露重要的信息,把不重要的细节暗藏起来。暗藏细节一则使模块接口更简略,二则使零碎更易保护。

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

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

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

3、对于多线程应用程序,重要的细节:如何创立一个线程;不重要的细节:多核 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 缩小了,零碎可维护性更高了,天然也就带来了更好的生存品质。

模块设计是升高软件复杂度最无效的伎俩,学会应用“策略编程”的办法,并坚持下去。咱们经常提倡“一次把事件做对”,但这对于模块设计而言并不实用,简直没有人能够第一次就把一个模块设计成完满的模样。二次设计是一个十分无效的手法,与其在零碎腐化之后再花大量工夫进行重构或重写,还不如在第一次实现模块设计后,再花点工夫进行二次设计,多问问本人:是否有更简略的接口?是否有更通用的设计?是否有更简洁高效的实现?

“ 罗马不是一天建成的 ”,升高软件的复杂性也一样,贵在保持。

点击关注,第一工夫理解华为云陈腐技术~

正文完
 0