简介在日常工作中,你是否有这样的感受:“好”的代码和“坏”的代码都能满足产品的需求。但是不同的人写出的代码在效率、质量、可维护性、可扩展性和可读性等方面差异很大。想法、概念、架构设计得再好,写出来的代码三天之内天天被人踩,简直就是纸上谈兵。因此,软件的健壮性是衡量一个工程师水平的重要标准。如何提高软件的健壮性?合理的顶层设计和完备的测试是必不可少的,但归根结底还是提高代码质量的根本。防御性编程是一种思考安全编码的方式。它被视为减少或消除墨菲定律的一种手段。Defensiveprogramming在《Code Complete》(CodeEncyclopedia)一书中有详细介绍,被程序员奉为标准。同时,国内外很多大厂商都将防御性编程作为质量建设的手段之一。本文将介绍防御性编程以及如何在实际场景中应用防御性编程。墨菲定律如果做某事有两种或两种以上的方法,选择其中一种方法会导致灾难,就会有人做出这种选择。这是一种悲观的思维,认为所有可能出错的坏事都会发生。那么在用这种思路进行设计的时候,需要对最坏的情况进行预测,并采取相应的措施。同时让用户不用复杂的思考就可以直观的使用某个系统,这就是所谓的防呆设计。例如,3.5英寸的软盘设计成只能插入一个盒子。什么是防御性编程?防御性编程的核心思想是子程序不应因传入错误数据而被破坏,即使错误数据是由其他子程序生成的。简单来说就是怀疑一切,认为自己代码之外的环境是不可信的。在这种情况下,考虑如何编写代码。防御性编程和防御性驾驶防御性编程,概念来源于防御性驾驶。防御性驾驶建立在这样一种心态之上,即您永远无法确定其他驾驶员将要做什么。这样可以确保您在他人做出危险动作时不会受到伤害。您有责任保护自己,即使这是另一个司机的错误。举一个现实的场景,以一次SQL查询为例:ClassMain{privateConnectioncon==DriverManager.getConnection(JDBC_URL,JDBC_USER,JDBC_PASSWORD);publicListdoQuery(Stringname){Statementstmt=conn.createStatement();ResultSetrs=stmt.executeQuery("SELECTid,grade,name,genderFROMstudentsWHEREname="+name);ListstudentList=newArrayList<>();while(rs.next()){longid=rs.getLong(1);长级=rs.getLong(2);字符串名称=rs.getString(3);字符串性别=convertGender(rs.getInt(4));Studentstudent=newStudent(id,年级,姓名,性别);studentList.add(学生);}返回学生名单;}privateStringconvertGender(intgender){switch(gender){case0:return"male";案例1:返回“女性”;}返回空值;}}上面的代码比较简单,似乎实现了我们想要的:查询所有匹配名字但有开发经验的同学,可以发现很多问题,比如:数据库连接是否正常建立?数据库的返回值是否经过验证?总结以上问题,我们可以得出防御性编程的关键原则。边界防御:检查所有外部输入在防御性编程的概念中,所有外部输入都是不可信任的,需要检查它们是否在允许的范围内。这里需要检查的项目包括空指针、数组越界、非法入参等等,尤其是我们在写一个public方法的时候,我们不确定这个方法在某些时候会被外部系统调用点在未来。做好输入检查,不仅可以保护我们自己程序运行的健壮性,也可以让外部系统放心调用。在上述案例中,存在明显的参数传递漏洞。参数名可能被外部用户使用SQL注入攻击。比如输入name=“zhangsanor1=1”,就可以得到所有学生的信息。很明显这是不符合我们要求的Input参数。此外,这个外部输入不仅包括传入的参数,还包括从方法外部获得的任何数据,包括从数据库中查询的数据。异常处理:正确性和健壮性之间的权衡正确性意味着程序永远不应该返回不准确的结果,即使这样做不会返回任何结果或只是退出程序。鲁棒性是指在异常输入或异常的外部环境下,即使输出结果错误或不完整,系统仍能正常运行。正确性和健壮性往往是相互矛盾的,当我们检查出坏数据时,我们需要决定如何处理它。防御性编程不会掩盖错误,它不会隐藏错误。这需要在鲁棒性和正确性之间进行权衡。在火箭发射系统或医疗系统等对异常容忍度较低的场景中,正确性优于鲁棒性;在电子商务等消费场景中,应该优先考虑健壮性。毕竟选品不成功,刷新一下就好了。在上面的例子中,如果数据库连接失败,与其直接让程序退出,还不如重试或者返回错误码。检查检查:没有完全可靠的外部环境。我们在编码的时候,会有很多外部方法的调用和交互,所以我们必须警惕所有的外部调用。这些API或者第三方类库也是人写的,也就意味着可能存在bug。一个好的思路是,对于外部调用的异常,尽量按照自己的逻辑去检查和处理。上面的案例,存在两个问题:1、调用数据库没有检查是否成功;2、调用数据库后没有手动释放资源,容易造成内存泄漏。显式约束:简单直接的编码风格在防御性编程中,我们提倡使用“最愚蠢”的方式编写代码,尽可能少地使用语法糖或隐式约定。比如很多面试题中遇到的“a++=b++”,在防御性编程中是不提倡的。最好写成“a=b;a++;b++”。虽然多了两行代码,但是意思很清楚,也很容易理解。.另一种情况是使用显式约束。例如:多使用constfinalstatic,避免使用select*,在字段名前加表名。在上面的例子中,“SELECTid,grade,name,genderFROMstudentsWHEREname=”中的name应该加上表名:students.name,因为name是mysql中的关键字。减少依赖性:编写一次,随处运行“编写一次,随处运行”是Sun用来展示Java编程语言的跨平台特性的口号。环境依赖保证程序在外部环境发生变化时仍能正常运行。在我们的项目中,比较典型的问题就是本地化。本地化问题往往涉及到数据库适配,所以在进行程序设计时,需要考虑业务逻辑与底层数据调用的解耦。再举个例子,“i++!=j++”在不同的编译器中执行结果是不同的,这是我们需要避免的。傻瓜式评论是给系统看的,评论是给人看的。如果你想让代码更具可读性,你应该把自己当傻子来添加注释。(这和cleancode的概念是不一样的,cleancode提倡把代码当作注释,个人觉得这是一种理想化的情况。)但是注释不是废话。好的评论应该出现在:复杂的业务逻辑,非常规的写法,项目中可能存在的坑,临时解决方案,核心类或方法。写评论是一个优秀工程师的必备技能。很多优秀项目的评论写法可以参考。契约编程(ContractProgramming),顾名思义,在设计阶段就已经确定了每个方法的边界,包括每个方法的参数和返回值,以及它们的类型和所有可能的取值。该术语最早由BertrandMeyer于1986年创造。他设计了Eiffel编程语言来实现这种编程方式,并在《物件导向软体建构》(Object-OrientedSoftwareConstruction)一书中提出了两个后续版本。契约式编程强调三个概念,即前置条件、后置条件和不变量。这些概念其实是脱胎于埃菲尔语言的一些特点,不熟悉埃菲尔的同学会觉得有点晦涩难懂。前置条件:期望所有调用它的客户端模块必须保证一定的入口条件,比如非NULL、非零等;后置条件:确保在退出时给出特定的属性,比如程序退出时会释放数据库连接;noVariant:在进入时假设,并在退出时保持一些特定的属性。契约编程是一种比防御性编程更乐观的编程思想,强调约定和断言。想了解更多契约编程的同学可以去:https://www.eiffel.com/values...避免过度设计的防御性编程也会带来新的问题:防止不可能的错误,如上例所示,对于数据库返回的结果,使用rs.next()判断是否有值,不需要对rs做非空判断;过多的防御性代码会使整个程序显得臃肿难维护,代码中充斥着大量的判断和非业务代码;程序的性能也会受此影响;当代码中有大量的异常捕获和处理时,异常可能会被吞掉而不能正常上报。总结防御性编程是一种安全的编程思想,本质上要求开发者有辩证的态度,尊重代码和网络环境。它通过以下方式提高系统的健壮性:提高工程质量——减少错误和问题;提高源代码的可读性——源代码应该变得可读易懂,并经得起代码审查;允许软件通过预期行为来处理意外的用户操作。作为一名优秀的开发人员,不能完全寄希望于测试和测试驱动开发。相反,人们应该在设计和开发阶段充分理解和考虑系统的异常和边界。这就是防御性编程带给我们的。思维。附录:防御性编程checkListgeneralmatter子例程是否保护自己免受有害输入数据的影响?您是否使用断言来说明编程假设?它包括前置条件和后置条件吗?断言只是用来说明某事永远不应该发生吗?您是否在架构或高级设计中指定了一组特定的错误处理技术?您是否在体系结构或高级设计中指定是否支持错误处理而不是健壮性或正确性?你是否建立了围栏来遏制错误可能造成的损害?它是否减少了需要关注错误处理的其他代码量?代码中是用来辅助调试的吗?如果我需要开启或关闭添加的helper,是不是就不用去火了?防御性编程中反映的代码量是否合适——既不过多也不过少?是否在开发阶段使用攻击性编程来让错误更难被忽略?异常你在你的项目中定义了一套标准化的异常处理方案吗?您是否考虑过异常的替代方案?如果可能,错误是否在本地处理而不是作为异常在外部抛出?代码中是否避免了构造函数和析构函数中抛出的异常?所有异常都与抛出它们的子例程处于同一抽象级别吗?6).每个异常是否都包含异常发生的所有背景信息?您没有在代码中使用空的catch语句吗?(或者如果使用空的catch语句确实合适,是否明确说明?)安全问题检查有害输入数据的代码是否也检查故意的缓冲区溢出、SQL注入、HTML注入、证书溢出和其他恶意输入数据?是否检查了所有错误返回码并捕获了所有异常?是否需要帮助攻击者闯入错误消息中避免的系统的信息?最终生产代码中的错误处理比“垃圾输入,垃圾输出”复杂得多。防御性编程技术可以使错误更容易发现、更容易修复,并且对生产代码的破坏更小。断言可以帮助人们及早发现错误,尤其是在大型且高度可靠的系统中,以及快速变化的代码中。如何处理错误输入的决定是一个关键的错误处理决定和一个关键的高层设计决定。异常提供了一种不同于正常代码流的错误处理方式。如果小心使用,异常可以成为程序员知识工具箱的有用补充,并且应该比较异常和其他错误处理方式之间的权衡。产品代码的限制不适用于开发中的软件。您可以利用这一优势在开发中添加有助于更快地排除错误的代码。作者简介云智能架构部长期致力于智能运维领域的工程架构建设与开发,打造高性能、高可用、高易用的运维平台。和维修工程框架,提高公司技术水平。开源福利云智慧开源了数据可视化编排平台FlyFish。通过配置数据模型为用户提供上百种可视化图形组件,零编码实现满足业务需求的炫酷可视化大屏。同时,飞鱼还提供了灵活的扩展能力,支持组件化开发、自定义功能、全局事件配置,能够保障复杂需求场景的高效开发和交付。点击下方地址链接,欢迎大家给飞鱼一个star。参与组件开发,更有万元现金等你拿。GitHub地址:https://github.com/CloudWise-...Gitee地址:https://gitee.com/CloudWise/f...百万现金活动:http://bbs.aiops.cloudwise.co。..微信扫描识别下方二维码,关注【飞鱼】加入AIOps社区飞鱼开发者交流群,与飞鱼项目PMC面对面交流~