CleanCode

您所在的位置:网站首页 函数combin CleanCode

CleanCode

2024-01-08 05:17| 来源: 网络整理| 查看: 265

函数 1.短小,函数应该更短小。

每个函数很短,每个函数只说一件事,每个函数都依序把你带到下一个函数。

public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) { if (isTestPage(pageData)) { includeSetupAndTeardownPages(pageData, isSuite); } return pageData.getHtml(); }

注意代码块和缩进。 if语句、else语句、while语句等,其中的代码块应该只有一行。可以用函数放置代表背后意思,这样容易阅读并保持函数短小。

2.只做一件事

函数应该做一件事,做好这件事。

重构后的代码:将设置和拆解包纳到测试页面上。

背后三个步骤:

1)判断是否是测试页面; 2)如果是,则容纳进设置和分拆步骤 3)渲染成HTML

这三个步骤均在该函数名下的同一抽象层上,本质上是一件事。 重构前代码混乱,包含多个不同抽象层级的步骤。

要判断函数是否不止做了一件事,看看能否再拆出一个函数,而这个函数不仅只是单纯地重新诠释其实现。

3.每个函数一个抽象层级

函数中的语句都要在同一抽象层级上。

getHtml()等位于较高抽象层的概念; String pagePathName = PathParser.render(pagePath)位于中间抽象层, 还有 .append(“\n”)等位于相当低的抽象层概念。

函数中混杂不同抽象层级,往往让人迷惑,无法判断某个表达式是基础概念还是细节。

自顶向下读代码:向下规则

让每个函数后面都跟着位于下一抽象层级的函数。

4.switch语句

写出短小的switch语句很难,因为switch天生要做N件事情。通过多态可以确保每个switch都埋藏在较低的抽象层级,并永不重复。

如下代码,依赖于雇员类型的操作。

public Money calculatePay(Employee e) throws InvalidEmployeeTye { switch(e.type) { case COMMISSIONED: return calculateCommissionPay(e); case HOURLY: return calculateHourlyPay(e); case SALARIED: return calculateSalariedPay(e); default: throw new InvalidEmployeeType(e.type); } }

该函数的问题: 首先,太长,当出现新的雇员类型,会变得更长; 其次,不止做了一件事情; 第三,违反了SRP单一职责原则,有好几个修改它的理由 第四,违反了OCP开闭原则,每当添加新类型,都必须修改。

该函数最麻烦的可能是导出皆有类似结构的函数,例如,可能有:

isPayday(Empolyee e, Date date),或 deliveryPay(Employee e, Money pay) 等等。

该问题的解决方案,将switch语句埋到抽象工厂底下,不让任何人看到。 该工厂使用switch语句为Employee的派生物创建适当的实体。 不同的函数如 calculatePay、isPayday和deliveryPay等,由 Employee接口多态地接受派遣。

public abstract class Employee { public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay); } public interface EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; } public class EmployeeFactoryImpl implements EmployeeFactory{ public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType { switch(e.type) { case COMMISSIONED: return new CommissionedEmployee(r); case HOURLY: return new HourlyEmployee(r); case SALARIED: return new SalariedEmployee(r); default: throw new InvalidEmployeeType(r.type); } } 5.使用描述性的名称

给每个私有方法取个具有描述性的名称,描述函数做的事情。不要害怕长名称,长但具有描述性的名称比短却令人费解更好。不要害怕花时间取名字。 命名方式要保持一致,使用与模块名一脉相承的短语、名词和动词给函数命名。 使用类似的措辞,依序讲出一个故事。

6.函数参数

最理想的参数数量是零,其次是一,再次是二。应尽量避免三。有足够的的特殊理由才能用三个以上的参数。 从概念阅读、代码测试的角度,参数少更方便。

(1)单参数 传入操作单参数,将其转换再输出; 传入事件(event),有输入参数而无输出参数。程序将函数看做一个事件,使用该参数修改系统状态。 如果函数要对输入参数进行转换操作,转换结果应体现为返回值。 StringBuffer transform(StringBuffer in) 比 void transform(StringBuffer in)更合适。

(2)标识参数 不要向函数传入布尔值。这样函数并非只做了一件事,而应该将函数一分为二,针对两种情况进行操作处理。

(3)二参数

若两个参数并非有任何关联组合,可能二参数比一参数还难懂,省略掉不必要的参数。忽略掉的部分就是本不应该写进去的。 即便是 assertEquals(expected, actual)这种二元函数,也有可能搞错位置。 一般期望的值在前,实际的值在后,遵守约定。 使用二元函数要付出代价,尽量将其转换为一元函数。 可以把一个参数写成类成员变量,从而无需传递, 可以把方法作用在其中一个参数对应的类上,从而只需要另一个参数; 可以分离出新类,在构造器中调用一个参数。

(4)三参数

三参数的排序、琢磨问题很复杂,尽量不要采用三参数。

(5)参数对象

如果函数看来需要两个、三个或三个以上参数,说明其中一些参数应该封装为类了。

即 Circle makeCircle(double x, double y, double radius); vs Circle makeCircle(Point center, double radius); x和y本身就是自己名称所属的某个概念的一部分。

(6)参数列表

向函数传入数量可变的参数,比如 String.format方法:

String.format("%s worked %.2f hours.", name, hours); 这里可变参数可以理解为一个参数,即该函数是二元函数。

public String format(String format, Object... args); 有可变参数的函数可以是一元、二元甚至三元,不要超过3。

(7)动词与关键词

给函数取个好名字,才能更好理解函数的意图,以及参数的顺序和意图。 单参数,函数和参数应该形成良好的动词/名词对应形式。 比如: write(name) ,不管name是什么,都要被 write,也可以更细致 writeField(name),name是一个field。

assertEqual改为 assertExpectedEqualsActual(expected, actual)

7.无副作用

有时候,函数看起来是只做一件事,但还是容易隐藏着做了一些事,比如对自己类中的变量做出改动,甚至改变全局变量。

比如,以下代码:

public class UserValidator { private Cryptographer cryptographer; public boolean checkPassword(String userName, String password) { User user = UserGateway.findByName(userName); if(user != User.NULL) { String codePhrase = user.getPhraseEncodedByPassword(); String phrase = cryptographer.decrypt(codePhrase, password); if("Valid Password".equals(phrase)) { Session.initialize(); return true; } } return false; } }

这里的副作用就是 Session.initialize()的调用,这本来是个 check函数,但未暗示会初始化该次对话。由此,当误信了函数名而检查用户有效时,会造成抹除现有回话的风险。 这一副作用造成了一次时序性耦合,checkPassword只能在特定时刻调用,初始化会话是安全的时候调用,如果在不合适的时候调用,会话数据会丢失。 可改名为 checkPasswordAndInitializeSession(),但这样仍然违反了“只做一件事”的规则。

输出参数 参数一般被看做为函数的输入。 有的参数被用作输出而非输入,例如: appendFooter(s) 不清楚这个函数是把s添加在什么东西后面,还是它把什么东西添加了s后面,s是输入还是输出函数,需要重点看函数签名。

public void appendFooter(StringBuffer report)

非OOP编程时,很多时候需要输出参数; 而OOP,因为this也有输出函数的意思。最好修改为:report.appendFooter()

主体在report上,而非传入report参数,因为本质就是输出转化后的report。

总体而言,避免使用输出参数。 如果函数必须要修改某种状态,最好修改所属对象的状态。

8.分隔指令与询问

函数要么做什么事,要么回答什么是,两者不可兼得。 函数应该修改某西乡的状态或返回该对象的有关信息,两样都做会导致混乱。 举例:

public boolean set(String attribute, String value);

该函数设置某个指定属性,如果成功返回true,不存在那个属性返回false。 这样导致了以下语句:

if(set("username","unclebob))...

这是在问username属性值是否已设置为unclebob吗?还是在问username属性值是否成功设置为unclebob呢?很难判断,因为set是动词还是形容词不清楚。

作者本意,set是动词,但在if语句的上下文中,更像形容词。 这个语句读出来更像是**“如果username属性值之前已被设置为uncleob”而不是“设置username属性值为unclebob,看看是否可行,然后…”**

因此,可将set函数重命名为 setAndCheckIfExists,但这对提高if语句可读性帮助不大。真正的解决方案是把指令与询问分隔开,防止混淆。

if(attributeExists("username")) { setAttribute("username","unclebob"); .. } 9.使用异常替代返回错误码

从指令式函数返回错误码轻微违反了指令与询问分割的规则,还鼓励了在if语句判断中把指令当做表达式使用。

if(deletePage(page) == E_OK) 这不会引起动词/形容词混淆,但容易导致更深层次的嵌套结构,当返回错误码时,就是在要求调用者立刻处理错误。

if(deletePage(page) == E_OK) { if(registry.deleteReference(page.name) == E_OK) { if(configKeys.deleteKey(page.name.makeKey() == E_OK)) { logger.log("page deleted"); } else { logger.log(".."); }else { logger.log("...") } } else { logger.log("..."); return E_EORROR; } }

如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化。

try{ deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } catch(Exception e) { logger.log(e.getMessage())A; }

(1)抽离Try/Catch代码块

try/catch代码块丑陋不堪,他们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把try和catch代码块的主体抽离出来,另外形成函数。

public void delete(Page page) { try{ deletePageAndAllReferences(page); } catch(Exception e) { logError(e); } } private void deletePageAndAllReferences(Page page) throws Exception { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } private void logError(Exception e) { logger.log(e.getMessage()); }

delete函数只与错误处理有关,很容易理解之后就忽略掉。 deletePageAndAllReference函数只与安全删除一个page有关,错误处理可以忽略掉。

​ (2) 错误处理就是一件事

函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事,如果关键字try在某个函数中存在,它应该是这个函数的第一个单词,而且在catch/finally代码块中不该有其他内容。

(3)Error.java 依赖磁铁

返回错误码通常暗示某处有个类或枚举,定义了所有错误码。这样的类就像是一块依赖磁铁,所有这些其他的类都需要重新编译和部署。这对Error类造成了负面压力。

public enum Error { OK, INVALID, NO_SUCH, LOCKED, OUT_OF_RESOURCES, WAITING_FOR_EVENT; }

使用异常替代错误码,新异常可以从异常类派生出来,无需重新编译或重新部署。

10.别重复自己

不要重复自己的代码,代码臃肿,修改出错的机会更大。

11.结构化编程

每个函数、函数中的每个代码块都应该有一个入口、一个出口。 遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且不能有goto语句。 这更适合大函数,小函数可以有不符合。 如果函数保持短小,偶尔出现return、break、continue没有坏处,甚至比单入单出更有表达力。

12.如何写出这样的函数

初稿也许粗陋无序,反复斟酌推敲,直至自己满意。 结合单元测试,分解函数,修改名称,消除重复,缩短和重新安置方法,拆分类等等。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3