如何写出健壮的代码

1次阅读

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

简介: 对于代码的健壮性,其重要性显而易见。那么如何能力写出强壮的代码?阿里娱乐技术专家长统将从进攻式编程、如何正确应用异样和 DRY 准则等三个方面,并联合代码实例,分享本人的认识心得,心愿对同学们有所启发。


你不可能写出完满的软件。因为它未曾呈现,也不会呈现。

每一个司机都认为本人是最好的司机,咱们在鄙视那些闯红灯、乱停车、胡乱变道不遵守规则的司机同时,更应该在行驶的过程中防卫性的驾驶,小心那些忽然冲进去的车辆,在他们给咱们造成麻烦的时候避开他。这跟编程有极高的相似性,咱们在程序的世界里要一直的跟别人的代码接合(那些不合乎你的高标准的代码),并解决可能无效也可能有效的输出。有效的的输出就如同一辆桀骜不驯的大卡车,这样的世界进攻式编程也是必须的,但要驶得万年船咱们可能连本人都不能信赖,因为你不晓得冲出去的那辆是不是你本人的车。对于这点咱们将在进攻式编程中探讨。

没人能否定异样解决在 Java 中的重要性,但如果不能正确应用异样解决那么它带来的危害可能比益处更多。我将在正确应用异样中探讨这个问题。

DRY,Don’t Repeat Yourself. 不要反复你本人。咱们都晓得反复的危害性,但反复时常还会呈现在咱们的工作中、代码中、文档中。有时反复感觉上是不得不这么做,有时你并没有意识到是在反复,有时却是因为懈怠而反复。

好借好还再借不难。这句俗话在编程世界中同样也是至理名言。只有在编程,咱们都要治理资源:内存、事物、线程、文件、定时器,所有数量无限的事物都称为资源。资源应用个别遵循的模式:你调配、你应用、你回收。

进攻式编程

进攻式编程是进步软件品质技术的无益辅助伎俩。进攻式编程的次要思维是:程序 / 办法不应该因传入谬误数据而被毁坏,哪怕是其余由本人编写办法和程序产生的谬误数据。这种思维是将可能呈现的谬误造成的影响管制在无限的范畴内。

一个好程序,在非法输出的状况下,要么什么都不输入,要么输入错误信息。咱们往往会查看每一个内部的输出(所有内部数据输出,包含但不仅限于数据库和配置核心),咱们往往也会查看每一个办法的入参。咱们一旦发现非法输出,依据进攻式编程的思维一开始就不引入谬误。

应用卫语句

对于非法输出的查看咱们通常会应用 if…else 去做判断,但往往在判断过程中因为参数对象的层次结构会一层一层开展判断。

public void doSomething(DomainA a) {if (a != null) {
        assignAction;
    if (a.getB() != null) {
      otherAction;
      if (a.getB().getC() instanceof DomainC) {doSomethingB();
        doSomethingA();
        doSomthingC();}
    }
  }
}

上边的嵌套判断的代码我置信很多人都见过或者写过,这样做尽管做到了根本的进攻式编程,然而也把俊俏引了进来。《Java 开发手册》中倡议咱们碰到这样的状况应用卫语句的形式解决。什么是卫语句?咱们给出个例子来阐明什么是卫语句。

public void doSomething(DomainA a) {if (a == null) {return ; //log some errorA}
    if (a.getB() == null) {return ; //log some errorB}
    if (!(a.getB().getC instanceof DomainC)) {return ;//log some errorC}
    assignAction;
    otherAction
    doSomethingA();
    doSomethingB();
    doSomthingC();}

办法中的条件逻辑使人难以看清失常的分支执行门路,所谓的卫语句的做法就是将简单的嵌套表达式拆分成多个表达式,咱们应用卫语句体现所有非凡状况。

应用验证器 (validator)

验证器是我在开发中的一种实际,将合法性检查与 OOP 联合是一种十分微妙的体验。

public List<DemoResult> demo(DemoParam dParam) {Assert.isTrue(dParam.validate(),()-> new SysException("参数验证失败 -" + DemoParam.class.getSimpleName() +"验证失败:" + dParam));
    DemoResult demoResult = doBiz();
    doSomething();
    return demoResult;
}

在这个示例中,办法的第一句话就是对验证器的调用,以取得以后参数是否非法。

在参数对象中实现验证接口,为字段配置验证注解,如果须要组合验证复写 validate0 办法。这样就把合法性验证逻辑封装到了对象中。

public class DemoParam extends BaseDO implements ValidateSubject {@ValidateString(strMaxLength = 128)
    private String aString;
    @ValidateObject(require = true)
    private List<SubjectDO> bList;
    @ValidateString(require = true,strMaxLength = 128)
    private String cString;
    @ValidateLong(require = true)
    private Long dLong;
    @Override
    public boolean validate0(ValidateSubject validateSubject) throws ValidateException {if (validateSubject instanceof DemoParam) {DemoParam param = (DemoParam)validateSubject;
            return StringUtils.isNotBlank(param.getAString())
                   && SubjectDO.allValidate(param.getBList());
        }
        return false;
    }
}

应用断言

当呈现了一个从天而降的线上问题,我置信很多搭档的心中肯定闪现过这样一个念头。” 这不迷信啊 … 这不可能产生啊…”,” 计数器怎么可能为正数 ”,” 这个对象不可为 null”,但它就是实在的产生了,它就在那。咱们不要这样骗本人,特地是在编码时。如果它不可能产生,用断言确保它不会产生。

应用断言的重要准则就是,断言不能有副作用,也绝不能把必须执行的代码放入断言。

断言不能有副作用,如果我每年减少谬误查看代码却制作了新的谬误,那是一件令人难堪的事件。举一个背面例子:

while (iter.next() != null) {assert(iter.next()!=null);
    Object next = iter.next();
    //...
}

必须执行的代码也不能放入断言,因为生产环境很可能是敞开 Java 断言的。因而我更喜爱应用 Spring 提供的 Assert 工具,这个工具提供的断言只会返回 IllegalStateException,如果须要这个异样不能满足咱们的业务需要,咱们能够从新创立一个 Assert 类并继承 org.springframework.util.Assert,在新类中新增断言办法以反对自定义异样的传入。

public class Assert extends org.springframework.util.Assert {public static <T extends RuntimeException> void isTrue(boolean expression, Supplier<T> tSupplier) {if (!expression) {if (tSupplier != null) {throw tSupplier.get();
            }
            throw new IllegalArgumentException();}
    }
}
Assert.isTrue(crParam.validate(),()-> new SysException("参数验证失败 -" + Calculate.class.getSimpleName() +"验证失败:" + crParam));

有人认为断言仅是一种调试工具,一旦代码公布后就应该敞开断言,因为断言会减少一些开销(渺小的 CPU 工夫)。所以在很多工程实际中断言的确是敞开的,也有不少大 V 有过这样的倡议。Dndrew Hunt 和 David Thomas 拥护这样的说法,在他们书里有一个比喻我认为很形象。

在你把程序交付使用时敞开断言就像是因为你已经胜利过,就不必保护网取走钢丝。
——《The pragmatic Programmer》

处理错误时的要害抉择

进攻式编程会预设错误处理。

在谬误产生后的后续流程上通常会有两种抉择,终止程序和持续运行。

  • 终止程序,如果呈现了十分重大的谬误,那最好终止程序或让用户重启程序。比方,银行的 ATM 机呈现了谬误,那就敞开设施,以避免取 100 块吐出 10000 块的喜剧产生。
  • 持续运行,通常也是有两种抉择,本地解决和抛出谬误。本地解决通常会应用默认值的形式解决,抛出谬误会以异样或者错误码的模式返回。

在处理错误的时候咱们还面临着另外一种抉择,正确性和健壮性的抉择。

  • 正确性,抉择正确性意味着后果永远是正确的,如果出错,宁愿不给出后果也不要给定一个不精确的值。比方用户资产类的业务。
  • 健壮性,健壮性意味着通过一些措施,保障软件可能失常运行上来,即便有时候会有一些不精确的值呈现。比方产品介绍超过页面展现范畴

无论是应用卫语、断言还是预设错误处理都是在用抱着对程序世界的敬畏态度在小心的驾驶,时刻提防着别人更提防着本人。

北京第三区交通委提醒您,路线千万条,平安第一条,行车不标准,亲人两行泪。

正确应用异样

查看每一个可能的谬误是一种良好的实际,特地是那些意料之外的谬误。

十分棒的是,Java 为咱们提供了异样机制。如果充分发挥异样的长处,能够进步程序的可读性、可靠性和可维护性,但如果使用不当,异样带来的负面影响也是十分值得咱们留神并防止的。

只在异常情况下应用异样

在《The pragmatic Programmer》和《Effective Java》中作者都有这样的观点。

我认为这有两重意思。一重意思如何解决辨认异常情况并解决他,另一重意思是只在异常情况下应用异样流程。

那什么是异常情况,又该如何解决?这个问题无奈在代码模式上给出规范的答案,齐全看理论状况,要对每一个谬误了然于胸并查看每一个可能产生的谬误,并辨别谬误和异样。

即使同样是关上文件操作,读取 ”/etc/passwd” 和读取一个用户上传的文件,同样是 FileNotFoundException,如何解决齐全取决于理论状况,Surprise!前者间接读取文件出现异常间接抛出让程序尽早解体,而后者应该先判断文件是否存在,如果存在但呈现了 FileNotFoundException 则再抛出。

public static void openPasswd() throws FileNotFoundException {FileInputStream fs = new FileInputStream("/etc/passwd");
    }

读取 ”/etc/passwd” 失败,Surprise!

public static boolean openUserFile(String path) throws FileNotFoundException {File f = new File(path);
        if (!f.exists()) {return false;}
        FileInputStream fs = new FileInputStream(path);
        return true;
    }

在文件存在的状况下读取文件失败,Surprise!

再啰嗦一遍,是不是异常情况关键在于它是不是给咱们一记 Surprise!,这就是本节结尾查看每一个谬误是一种良好的实际想要表白的。

应用异样来管制失常流程的背面例子我就偷懒借用一下《Effective Java Second Edition》里的例子来阐明好了。

Integer[] range ={1,2,3};
//Horrible abuse of exceptions.Don't ever do this!
try {
  int i=0;
  println(range[i++].intValue());
} catch (ArrayIndexOutOfBoundsException e) {}

这个例子看起来基本不晓得在干什,这段代码其实是在用数组越界异样来管制遍历数组,这个脑洞开的十分高明。如何正确遍历一个数组我想不须要再给出例子,那是对读者的亵渎。

那为什么有人这么开脑洞呢?因为这个做法希图应用 Java 错误判断机制来进步性能,因为 JVM 对每一次数组拜访都会查看越界状况,所以他们认为查看到的谬误才应该是循环终止的条件,然而 for-each 循环对曾经查看到的谬误熟视无睹,将其暗藏了,所以用应该防止应用 for-each。

对于这个脑洞的起因 Joshua Bloch 给出了三点反驳:

  • 因为异样机制的设计初衷是用于不失常的情景,所以很少会有 JVM 实现试图对它们进行优化,使得与显示测试一样疾速。
  • 把代码放在 try-catch 块中反而阻止了古代 JVM 实现原本可能要执行的某些特定优化。
  • 对数组进行遍历的规范模式并不会导致冗余的查看。有些古代的 JVM 实现会将他们优化掉。

还有一个例子是我已经遇到的,然而因为年代久远曾经找不到我的项目地址了。我一个敌人已经给我看过一个 github 上的 MVC 框架我的项目,尽管时隔多年但令我印象粗浅的是这个我的项目应用自定义异样和异样码来管制 Dispatcher,把异样当成一种不便的后果传递形式来应用,当成 goto 来应用,这太可怕了。不过 try-catch 形式从字节码表述上来看,的确是一种 goto 的表述。这样的形式咱们最好想都不要想。

这两个例子次要就是为了阐明,异样应该只用于异样的状况下;永远不应该用在失常的流程中,不论你的理由看着如许的聪慧。这样做往往会画蛇添足,使得代码可读性大大降落。

受检异样和非受检异样

已经不止一次的见过有人提倡将零碎中的受检异样都包装成非受检异样,对于这个倡议我并不以为然。因为 Java 的设计者其实是心愿通过辨别异样品种来领导咱们编程的。

Java 一共提供了三类可抛出构造 (throwable),受检异样、非受检异样 (运行时异样) 和谬误 (error)。他们的界线我也常常傻傻的分不清,不过还是有迹可循的。

  • 受检异样:如果冀望调用者可能适当的复原,比方 RMI 每次调用必须解决的异样,设计者是冀望调用者能够重试或别的形式来尝试复原;比方上边提到的 FileInputStream 的构造方法,会抛出 FileNotFoundException,设计者或者心愿调用者尝试从其余目录来读取该文件,使得程序能够继续执行上来。
  • 非受检异样和谬误:表明是编程谬误,往往属于不可复原的情景,而且是不应该被提前捕捉的,应该疾速抛出到顶层处理器,比方在服务接口的基类办法中对立解决非受检异样。这种非受检异样往往也阐明了在编程中违反了某些约定。比方数组越界异样,阐明违反了拜访数组不能越界的前提约定。

总而言之,对于可复原的状况应用受检异样;对于程序谬误应用非受检异样。因而你本人程序外部定义的异样都应该是非受检异样;在面向接口或面向二方 / 三方库的办法尽量应用受检异样。

说到面向接口或面向二 / 三方库,你可能碰到的就是一辆失控的汽车。搞清楚你所调用的接口或者库里的异常情况也是咱们可能码出强壮代码的一个强力保障。

不要疏忽异样

这个倡议不言而喻,但却经常被违反。当一个 API 的设计者申明一个办法将抛出异样的时候,通常都是想要阐明某件事产生了。疏忽异样就是咱们通常说的吃掉异样,try-catch 但什么也不做。吃掉一个异样就好比毁坏了一个报警器,当劫难真正降临没人搞清楚产生了什么。

对于每一个 catch 块至多打印一条日志,阐明异常情况或者阐明为什么不解决。

这个不言而喻的倡议同时实用于受检异样和非受检异样。

DRY (Don’t Repeat Yourself)

DRY 准则最先在《The pragmatic Programmer》被提出,现在曾经被业界宽泛的认知,我置信每个软件工程师都意识它。我想有很多人对它的意识含混不清仅仅是不要有反复的代码;也有些人对此准则等闲视之形象什么的都是浪费时间疾速上线是大义;也有人誓死捍卫这个准则不能忍耐任何反复。明天咱们来谈谈这个相熟又生疏的话题。

DRY 是什么

DRY 的准则是“零碎中的每一部分,都必须有一个繁多的、明确的、权威的代表”,指的是(由人编写而非机器生成的)代码和测试所形成的零碎,必须可能表白所应表白的内容,然而不能含有任何反复代码。当 DRY 准则被胜利利用时,一个零碎中任何单个元素的批改都不须要与其逻辑无关的其余元素产生扭转。此外,与之逻辑上相干的其余元素的变动均为可预感的、平均的,并如此放弃同步。

这段定义来自于中文维基百科,但这个定义仿佛与 Andrew Hunt 和 David Thomas 给出的定义有所出入。寻根溯源在《The pragmatic Programmer》作者是这样定义这个准则的:

EVERY PIECE OF KNOWLEDGE MUST HAVE A SINGLE, UNAMBIGUOUS, AUTHORITATIVE REPRESENTATION WITHIN A SYSTEM.

零碎中的每一项常识都必须具备繁多、无歧义、权威的示意。

作者所提倡禁止的是常识 (knowledge) 的反复而不是单纯的代码上的反复。那什么是常识呢?我斗胆给一个本人的了解,常识就是零碎中对于一个逻辑的解释 / 定义,零碎中的逻辑都是要对外输入或者让外界感知的。逻辑的定义 / 解释包含代码和写在代码上的文档还有宏观上实现。咱们要防止的是在改变时的一个逻辑的时候须要去批改十处,如果漏掉了任何一处就会造成 bug 甚至线上故障。变更在软件开发中又是一个常态,在互联网行业中更是如此,而在一个到处是反复的零碎中保护变更是十分艰巨的。

没有文档比谬误的文档更好

编写代码时同时编写文档在少数程序员看来是一个好习惯,但有相当一部分程序开发人员又没有这样的习惯,这一点反而使得代码更干 (dry)——有点好笑。因为底层常识应该放在代码中,底层代码应该是职责繁多、逻辑简略的代码,在底层代码上增加正文就是在做反复的事件,就有可能因为对于常识的过期解释,而读正文比读代码更容易,可怕的事件往往就这样产生;把正文放在更下层的简单的简单逻辑中。满篇的正文并不是好代码,也不是好习惯,好的代码是不须要正文的。

CP 大法,禁止!

每个我的项目都有工夫压力,这往往是引诱咱们应用 CP 大法最重要起因。然而 ” 欲速则不达 ”,你兴许当初省了十分钟,当前却须要花几个小时解决各种各样的线上问题。因为变更是常态,咱们当初留下的一个坑队友可能会帮你挖的更深更大一些,而后咱们就掉进了本人挖的坑,咱们还会抱怨猪队友,到底谁才是猪队友。这其实是我带过的一个团队里实在产生的事件。

把常识的解释 / 定义放在一处!

PS:感受一下程序员的冷风趣。违反 DRY 准则的代码,程序员称之为 WET 的,能够了解为 Write Everything Twice(任何货色写两遍),We Enjoying Typing(咱们享受敲键盘)或 Waste Everyone’s Time(节约所有人的工夫)。

对于 DRY 准则的争执

DRY 准则提出以来始终以来都存在着一些争议和探讨,有粉也有黑。如果有一个百分比,对于这条准则我会抉择 95% 遵从。

《The pragmatic Programmer》通知咱们 Once and only once。

《Extreme Programing》又通知咱们 You aren’t gonna need it (YAGNI),指的是你自认为有用的性能,实际上都是用不到的。这里如同呈现了一个问题,DRY 与 YAGNI 不齐全兼容。DRY 要求花精力去形象谋求通用,而 YAGNI 要求快和省,你花精力做的形象很可能用不到。

这个时候咱们的第三抉择是什么?《Refactoring》提出的 Rule Of Three 像是一个很好的折中计划。它的涵义是,第一次用到某个性能时,你写一个特定的解决办法;第二次又用到的时候,你拷贝上一次的代码;第三次呈现的时候,你才着手 ” 抽象化 ”,写出通用的解决办法。这样做有几个理由:

省事

如果一种性能只有一到两个中央会用到,就不须要在 ” 抽象化 ” 下面消耗工夫了。

容易发现模式

“ 抽象化 ” 须要找到问题的模式,问题呈现的场合越多,就越容易看出模式,从而能够更精确地 ” 抽象化 ”。比方,对于一个数列来说,两个元素不足以判断出法则:

1,2,_,_,_,_

第三个元素呈现后,法则就变得较清晰了:

1,2,4,_,_,_

避免适度冗余

如果一种性能同时有多个实现,治理起来十分麻烦,批改的时候须要批改多处。在理论工作中,反复实现最多能够容忍呈现一次,再多就无奈承受了。

我认为以上三个准则都不能当做银弹,还是要依据理论状况做出正确的抉择。

DRY 准则实践上来说是没有问题的,但在理论利用时切忌死搬教条。它只能起指导作用,没有量化规范,否则的话实践上一个程序每一行代码都只能呈现一次才行,这是十分荒诞的。

Rule of Three 不是反复的代码肯定要呈现三次才能够进行形象,我认为三次不应该成为一个度量规范,对于将来的预判和对于我的项目走向等因素也应该放在是否形象的思考中。

PS:王垠已经写过一篇《DRY 准则的危害》有趣味的敌人能够读一读:如何评估王垠最新文章,《DRY 准则的危害》?
(https://www.zhihu.com/question/31278077)

后记

准则不是银弹,准则是沙漠中的绿洲亦或是沙漠中空中楼阁中的绿洲。面对所谓的准则要求咱们每一个人都有辨识能力,不自觉听从先哲大牛,要具备独立思考的能力。具备辨识和思考能力首先就须要有足够多的输出和足够多的实际。

参考
[1]《The pragmatic Programmer:From Journeyman to Master》
作者:Andrew Hunt、David Thomas
[2]《Effective Java Second Edition》
作者:Joshua Bloch
[3]《Java 开发手册》
[4]中文维基百科
[5]代码的形象三准则 - 阮一峰
http://www.ruanyifeng.com/blog/2013/01/abstraction_principles.html

正文完
 0