乐趣区

关于代码质量:如何写出好代码-防御式编程指南

引言

在日常工作当中,大家是否有这种感觉:“好”代码和“差”代码,都能够实现产品的需要。然而不同的人写出的代码,在效率、品质、可维护性、可扩展性、可读性等方面千差万别。想法、构思、架构设计得再好,写进去的代码三天两头踩坑,这是夸夸其谈。因而软件的健壮性是掂量一名工程师程度的重要规范。如何晋升软件的健壮性?正当的顶层设计、齐备的测试必不可少,但终其基本是晋升代码品质。

进攻式编程是一种平安编码的思维形式。它被看做是缩小或打消墨菲定律的一种伎俩。在程序员奉为圭臬的著述《Code Complete》(代码大全)里,具体介绍了进攻式编程。同时国内外有很多大厂把进攻式编程作为品质建设的伎俩之一。本文将介绍进攻式编程以及理论场景中如何利用进攻式编程。

墨菲定律
如果有两种或两种以上的形式去做某件事情,而其中一种抉择形式将导致劫难,则必然有人会做出这种抉择。这是一种偏乐观的思维,认为所有可能出问题的坏状况都会产生。那么在抱有该想法去做设计时,就须要对最坏状况做出预测并采取绝对应的措施。同时让应用人员不须要进行简单的思考,通过直觉即可应用某个零碎,即所谓的防呆设计。如 3.5 寸的软盘设计,就设计成只有一种状况能够插入进去。

什么是进攻式编程?

进攻式编程,其核心思想是子程序应该不因传入谬误数据而被毁坏,哪怕是由其余子程序产生的谬误数据。简略来说,就是狐疑所有,认为本身代码之外的环境都是不可信的,在这种状况下,思考代码该怎么写。

进攻式编程和进攻式驾驶

进攻式编程,这一概念来自进攻式驾驶。在进攻式驾驶中要建设这样一种思维,那就是你永远也不能确定另一位司机将要做什么。这样能力确保在其他人做出危险动作时你也不会受到挫伤。你要承当起爱护本人的责任,哪怕是其余司机犯的谬误。

举个理论的场景,以一次 SQL 查问为例:

Class Main {private Connection con = = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
    
    public List<Student> doQuery(String name) {Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT id, grade, name, gender FROM students WHERE name=" + name);
        List<Student> studentList = new ArrayList<>();
        while(rs.next()) {long id = rs.getLong(1);
            long grade = rs.getLong(2);
            String name = rs.getString(3);
            String gender = convertGender(rs.getInt(4));
            Student student = new Student(id, grade, name, gender);
            studentList.add(student);
        }
        return studentList;
    }
    
    
    private String convertGender(int gender) {switch(gender) {
            case 0 : return "male";
            case 1 : return "female";
        }
        return null;
    }
}

上述代码比较简单,看上去也实现了咱们想要的需要:查问合乎名字的所有学生。然而有教训的开发同学,就可能发现不少问题,比方:是否失常建设了数据库连贯?数据库的返回值有没有做校验?以上的这些问题总结下来,咱们能够得出进攻式编程的要害准则。

边界进攻:查看所有的内部输出

在进攻式编程的理念中,所有的内部输出都是不可信的,须要校验是否在可容许的范畴内。这里须要查看的项包含,空指针、数组越界、不非法入参等。特地是当咱们在写一个公共办法时,不确定这个办法会在将来某个时刻,被某个内部零碎调用,做好输出查看既能爱护本身程序运行的健壮性,又能够让内部零碎释怀调用。

在下面的这个 case 中有一个显著的传参破绽,入参 name 有可能会被内部用户应用 SQL 注入攻打,如输出 name = “zhangsan or 1 = 1″,就能够获取所有 students 信息,显然这个是不合乎咱们要求的入参。

另外这个内部输出不光包含传入参数,还包含任何从办法内部获取到的数据,包含数据库查问到的数据等。

异样解决:在正确性和健壮性之间做好取舍

  • 正确性是指:程序永不返回不精确的后果,即便这样做会不返回后果或是间接退出程序。
  • 健壮性是指:零碎在不失常的输出或不失常的外部环境下仍能失常运行,哪怕输入后果是谬误的或者不残缺的。

正确性和健壮性往往是互相矛盾的,当咱们查看出谬误数据后,还须要决定如何解决它。防御性编程不会覆盖谬误,也不会暗藏 bug。这须要在健壮性和正确性之间做衡量。在对异样低容忍度的场景,比方火箭发射系统或医疗系统,正确性要优于健壮性;在电商等生产场景中,要优先思考健壮性,毕竟抉择商品不胜利,从新刷新一下就好了。

在上述 case 中,如果数据库连贯失败,比拟好的做法是重试或者返回错误码,而不是间接让程序退出。

应检尽检:没有齐全牢靠的外部环境

咱们在 coding 时,会有很多内部办法的调用和交互,要对所有的内部调用保持警惕。这些 API 或者三方类库也是人写的,人写的就意味着可能有 bug。一种好的思路是尽可能地依照本身逻辑,对外部调用做检查和异样解决(exception handle)。

在上述 case 中,存在两个中央的问题:

1、调用数据库并没有查看是否胜利;

2、调用完数据库并没有手动开释资源,这很有可能造成内存透露。

显示束缚:简略间接的代码格调

在进攻式编程中,咱们提倡应用“最笨”的形式写代码,尽量少的应用一些语法糖或者隐性规约。

比方很多面试题中都会碰到的 ”a++ = b++”,是进攻式编程当中不提倡的,不如写成 ”a = b; a++; b++”,尽管代码多了两行,然而意义清晰,且容易了解。

另外一种状况是应用显示束缚。比方:多用 const final static,防止应用 select *,字段名前加上表名。在上述的 case 中,”SELECT id, grade, name, gender FROM students WHERE name=” 里的 name 应该加上表名: students.name,因为 name 是 mysql 中的关键字。

缩小依赖:write once, run anywhere

“write once, run anywhere” 是 Sun 公司用来展现 Java 程序设计语言的跨平台个性的口号,这自身就是一种进攻式编程的理念体现,即在代码中缩小对环境的依赖,确保外界环境扭转了,程序仍然能够失常运行。

在咱们的我的项目中,比拟典型的就是国产化问题。国产化问题常常波及到数据库的适配,那么在做程序设计时,咱们就须要思考到业务逻辑和底层数据调用的解耦。

再举一个例子,”i++ != j++” 在不同的编译器中执行后果是不同的,这种也是咱们须要防止的。

傻瓜式正文

代码是给零碎看的,正文是给人看的。要想代码具备比拟好的可浏览性,应该把本人当作傻子去增加正文。(这个和 clean code 的理念不同,clean code 提倡代码即正文,集体认为这是一种理想化的情境)

但正文并不是写废话,好的正文应该呈现在:简单的业务逻辑、非常规的写法、可能有坑的中央、长期解决方案、我的项目当中外围的类或者办法。写正文是一个优良工程师必备的技能,大家能够参考下很多优良我的项目上的正文写法。

契约式编程

契约式编程(Contract Programming),顾名思义,在设计阶段就曾经确定好每个办法的边界,包含每个办法的参数、返回值,以及它们的类型和所有可能的值。这个术语最早由伯特兰·迈耶于 1986 年提出。他设计了 Eiffel 程式语言来实现这种程式设计办法,在《物件导向软体建构》(Object-Oriented Software Construction)一书中,又提出两个后继版本。

契约式编程强调了三个概念,即前置条件,后置条件和不变式。这几个概念实际上是脱胎于 Eiffel 语言的一些个性的,不相熟 Eiffel 的同学会感觉有点艰涩难懂。

  • 前置条件:冀望所有调用它的客户模块都保障肯定的进入条件,比方非 NULL、非 0 等要求;
  • 后置条件:保障退出时给出特定的属性,比方程序退出时会开释数据库连贯;
  • 不变式:在进入时假设,并在退出时放弃一些特定的属性。

契约式编程是比进攻式编程更加乐观的一种编程思路,强调约定和断言,具体想理解契约式编程的同学,能够移步:https://www.eiffel.com/values…

防止适度设计

适度的进攻式编程,也会带来新的问题:

  • 预防不可能会产生的谬误,如下面的 case 所示,对于数据库返回的后果,采纳 rs.next()即可判断是否有值,而不须要对 rs 进行非 null 判断;
  • 过多的进攻式代码,会导致整体程序显得臃肿、难以保护,代码里充斥着大量的判断和非业务代码;程序的性能也会受此影响;
  • 当代码中有十分多的异样捕获和解决时,可能会导致异样被吞掉,没有失常地报进去。

总结

进攻式编程是一种平安的编程思维,实质上是要求开发人员对代码和线上环境报以辩证的态度和敬畏之心。它通过以下路径,从而来晋升零碎健壮性:

  • 进步工程质量——缩小 bug 和问题;
  • 进步源码可读性—— 源码应该变得可读且可了解,并且能禁受 code review;
  • 让软件能通过预期的行为来解决不可预期的用户操作。

作为一名优良的开发者,不能将心愿齐全寄托于测试,测试驱动开发,而是在设计、开发阶段,对系统的异样和边界有充沛的认知和考量,这是进攻式编程带给咱们的思考。

附录:进攻式编程 checkList

个别事宜

  • 子程序是否爱护本人免遭无害输出数据的毁坏?
  • 你用断言来阐明编程假设吗?其中包含了前条件和后条件吗?
  • 断言是否只是用来阐明从不应该产生的状况?
  • 你是否在架构或高层设计中规定了一组特定的错误处理技术?
  • 你是否在架构或高层设计中规定了是让错误处理更偏向于健壮性还是正确性?
  • 你是否建设了隔栏来遏制谬误可能造成的毁坏?是否缩小了其余须要关注错误处理的代码的数量?
  • 代码中用到辅助调试的代码了吗?
  • 如果须要启用或禁用增加的辅助助手的话,是否无需大动干戈?
  • 在进攻式编程时映入的代码量是否合适–既不过多,也不过少?
  • 在开发阶段是否采纳了防御式编程来使谬误难以被忽视?

异样

  • 你在我的项目中定义了一套标准化的异样解决计划吗?
  • 是否思考过异样之外的其余代替计划?
  • 如果可能的话,是否在部分解决了谬误而不是把它当成一个异样抛到内部?
  • 代码中是否防止了在构造函数和析构函数中抛出异样?
  • 所有的异样是否都与抛出它们的子程序处于同一抽象层次上?6). 每个异样是否都蕴含了对于异样产生的所有背景信息?
  • 代码中是否没有应用空的 catch 语句?(或者如果应用空的 catch 语句的确很适合,那么明确阐明了吗?)

平安事宜

  • 查看无害输出数据的代码是否也查看了成心的缓冲区溢出、SQL 注入、HTML 注入、证书溢出一级其余歹意输出数据?
  • 是否查看了所有的谬误返回码
  • 是否捕捉了所有的异样?
  • 出错音讯中是否避免出现有助于攻击者攻入零碎所需的信息?

要点

  • 最终产品代码中对谬误的解决形式要比“垃圾进,垃圾出”简单的多。
  • 进攻式编程技术能够让谬误更容易发现、更容易批改,并缩小谬误对产品代码的毁坏。
  • 断言能够帮忙人尽早发现错误,尤其是在大型零碎和高可靠性的零碎中,以及疾速变动的代码中。
  • 对于如何处理错误输出的决策是一项要害的错误处理决策,也是一项要害的高层设计决策。
  • 异样提供了一种与代码失常流程角度不同的错误处理伎俩。如果留心应用异样,它能够成为程序员们常识工具箱中的一项无益补充,同时也应该在异样和其余错误处理伎俩之间进行衡量比拟。
  • 针对产品代码的限度并不适用于开发中的软件。你能够利用这一劣势在开发中增加有助于更快地排查谬误的代码。

作者简介

云智慧架构部,长期致力于智能运维畛域工程架构的建设和倒退,打造高性能、高可用、高易用的运维工程框架,晋升公司技术水位线。

开源福利

云智慧已开源数据可视化编排平台 FlyFish。通过配置数据模型为用户提供上百种可视化图形组件,零编码即可实现合乎本人业务需要的炫酷可视化大屏。同时,飞鱼也提供了灵便的拓展能力,反对组件开发、自定义函数与全局事件等配置,面向简单需要场景可能保障高效开发与交付。

点击下方地址链接,欢送大家给 FlyFish 点赞送 Star。参加组件开发,更有万元现金等你来拿。

  • GitHub 地址:https://github.com/CloudWise-…
  • Gitee 地址:https://gitee.com/CloudWise/f…
  • 万元现金流动:http://bbs.aiops.cloudwise.co…

微信扫描辨认下方二维码,备注【飞鱼】退出 AIOps 社区飞鱼开发者交换群,与 FlyFish 我的项目 PMC 面对面交换~

退出移动版