一文详解|如何写出优雅的代码
一、好代码的定义
谈到好代码,我的第一想法就是优雅,那我们如何该写出好的代码,让阅读的人感受到优雅呢?首先简单探讨一下优雅代码的定义。 关于好代码的定义,各路大神都给出了自己的定义和见解- 整洁的代码如同优美的散文。—— Grady Booch
- 任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。—— Martin Fowler
首先要达成一致,我们写的代码,除了用于机器执行产生我们预期的效果之外,更多的时候是给人读的,可能是后续的维护人员,更多时候是一段时间后的作者本人, 因此优雅面向不同的用户有两层含义的解读。
1. 对人而言,代码的整洁,清晰的逻辑;2. 对机器而言,准确性、执行性能、异常处理机制等;
这次,我们就来聊一聊,什么代码是优雅的代码,怎样写出优雅的代码。二、代码整洁
public List<int[]> getItem() {List<int[]> list1 = new ArrayList<int[]>();for (int[] x: theList)if (x[0] == 4)list1.add(x);return list1;}
public List<Cell> getFlaggedCells() {List<Cell> flaggedCells = new ArrayList<Cell>();for (Cell cell : gameBoard)if (cell.isFlagged())flaggedCells.add(cell);return flaggedCells;}
1. 一些被注释掉的代码//something code//something code2. 位置标记//beginsometing code;//end3. 签名标记/** add by xiaoli*/4. 非公用方法的javadoc/*** doSomething*/private void doSomething(){}5. 日志式注释/** add xx* update sometimes* update sometimes* update sometimes*/6. 误导性注释//此处怎样xx
3.1 务必要短小
方法应该有多短小?没有明确约束,idea也不会限制你,但通常我们的方法不该长于一屏,至少多于一屏或者横向外溢到屏幕以外最直观的就会造成可读性体验差,读了下面忘记上面,左右拖拽等。对大多数笔记本来说一屏大概就30行左右。短小精简的方法要比30行短很多,比如:public String renderPageWithSetupAndTeardowns(Page page, boolean isSuite) throws Exception{if(isTestPage(page)){includeSetupAndTeardownPages(page,isSuite);}return page.getHtml();}
3.2 只做一件事
一事精,便可动人。这个普世法则甚至适用于各种场合。 像设计原则的单一职责模式,让类只有一个职责。如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致逻辑混乱,设计耦合。当一个职责发生变化时,可能会影响其它的职责。 另外,多个职责耦合在一起,会影响复用性。针对方法而言更是如此。方法作为程序的原子单元,保持单一会有效提升复用性。 那怎么判断一个方法是否只做了一件事。 最简单的规则就是看看该方法是否能在拆出一个方法,且拆出去的方法是不同于该方法的诠释和实现。 但是要注意同一方法的逻辑层级务必要一致。3.3 抽象层级一致
抽象层级一致也是对方法只做一件事的更高要求,抽象层级不一致的代码一定是做了多件事。 我们读代码通常是自顶向下阅读,我们想让每个方法后面都跟着位于下一层级的方法,这样我们可以依着抽象层级向下阅读了。 我们也需要这样阅读代码,先有整体在展示细节,这种叫向下规则。这也是保持方法短小,确保只做一件事的诀窍。 一旦方法中混杂不同的抽象层级,会让人很迷惑,因为没办法这个方法中判断某个表达式是基础概念还是细节,更恶劣的是,一旦细节与基础概念混杂,更多的细节就会纠缠不清,举例子我们想写一个冰冻大象的需求://把大象装进冰箱public void frozenElephant(){//1. 捕捉大象//2. 运输大象//3. 打开冰箱//4. 放入大象//5. 关闭冰箱}
public void frozenElephant(){//1. 捕捉大象catchElephant();//2. 运输大象transportElephant();//将大象放入冰箱putElephantInRefrigerator();}public void catchElephant(){}public void transportElephant(){}public void putElephantInRefrigerator(){//打开冰箱//放入大象//关闭冰箱}
if(deletePage() == OK){if(registry.deleteReference(page.name) == OK){if(configKeys.deleteKey(page.name.makeKey) == OK){logger.log("page deleted")}else{logger.log("configKey not deleted")}}else{logger.log("deleteReference from registry failed")}}else{logger.log("delete failed")return Error;}
try{deletePage(page);registry.deleteReference(page.name);configKeys.deleteKey(page.name.makeKey);}catch(Exception e){logger.log(e.getMessage());}
3.5 使用第三方库
比如Lombok组件通过注解的方式,在编译时自动为属性生成构造器、getter/setter、equals、hashcode、toString方法 举例如下: 比如Apache Commons系列组件给我们提供了关于字符串、集合、IO操作等工具方法。这些组件是个大宝库,提供了不少轮子:| beanUtils | JavaBean进行各种操作,克隆对象、属性等等 |
| codec | 处理常用的编码方法的工具类包,例如DES、SHA1、MD5、Base64等. |
| collections | java集合框架操作 |
| configuration | java应用程序的配置管理类库 |
| io | io工具的封装 |
| lang | Java基本对象方法的工具类包 如StringUtils、ArrayUtils等等. |
| logging | 提供的日志接口 |
| net | 提供了客户端和服务器端的数据验证框架 |
三、代码重构
重构是对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。1.1 重复的代码
1. 最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”。 这时候需要做的就是采用提炼函数提炼出重复的代码,然后让这两个地点都调用被提炼出来的那一段代码
2. 如果重复代码只是相似而不是完全相同,需要先尝试用移动语句重组代码顺序,把相似的部分放在一起以便提炼。
1.2 过长的函数
遵循这样一条原则: 每当感觉需要以注释来说明点什么的时候,就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名,可以对一组甚至短短一行代码做这件事。 哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,就要毫不犹豫地那样做,关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。1. 百分之九十九的场合里,要把函数变短,只需使用提炼函数。 找到函数中适合集中在一起的部分,将它们提炼出来形成一个新函数。
2. 如果函数内有大量的参数和临时变量,最终就会把许多参数传递给被提炼出来的新函数,导致可读性几乎没有任何提升。 此时可以经常运用以查询取代临时变量来消除这些临时元素。 引入参数对象和保持对象完整则可以将过长的参数列表变得更简洁一些。
1.3 数据的可变性
对数据的修改经常导致出乎意料的结果和难以发现的bug。 在一处更新数据,却没有意识到软件中的另一处期望着完全不同的数据,于是出现难以预料的bug,往往比较难排查(需要排查数据流转的整体链路),这就需要一些方法用于约束对数据的更新,降低数据可变性的风险。
1. 可以用封装变量来确保所有数据更新操作都通过很少几个函数来进行,使其更容易统一监控和演进
2. 如果一个变量在不同时候被用于存储不同的东西, 可以使用拆分变量将其拆分为各自不同用途的变量,从而避免危险的更新操作。
1.4 模块单一职责
所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。 但是经常出现一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于与所处模块内部的交流,这就是模块功能不单一的典型情况。1. 总看到某个函数为了计算某个值,从另一个对象那儿调用半打的取值函数。 如果这个函数需要跟这些数据待在一起,那就使用移动功能把它移过去。
2. 一个函数往往会用到几个模块的功能,那么它究竟该被置于何处呢? 原则是: 判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。 如果先以提炼函数将这个函数分解为数个较小的函数并分别置放于不同类中,上面的步骤就会比较容易完成。
2.1 Extract Method
2.2 Inline Method
2.3 Replace Temp with Query
2.4 Inline Temp
2.5 Introduce Explaining Variable
2.6 Split Temporary Variable
2.7 Remove Assignments to Parameters
2.8 Replace Method with Method Object
四、小结
关于代码的逻辑和重构都是很基础的东西,在写代码之前我们就要思考如何做到整洁、优雅,并一直遵循这些经验来编写代码,所谓的“代码感”就自然而然的滋养而出, 要时刻提醒自己,仅仅编写出可运行的代码是远远不够的! 要以一个分享者的角度去写代码,再换位成一个阅读者的视角去审视自己的代码,如果能做了自己心里那一关,那一切就OK了!12月20日19:00,阿里巴巴研究员、阿里云云原生应用平台总经理丁宇带领Serverless团队与业内大咖共同探讨Serverless未来趋势和产品发展新方向。快来直播间围观吧!
点击阅读原文查看详情。