"Function should do one thing. They should do it well. They should do it only. "(函数只应该做一件事情,把一件事情做好,而且只由它来做这一件事情)
1.短小
-
1.函数的第一规则是要短小,第二条规则是还要更短小 。20行封顶最佳。
-
2.if语句、else语句、while语句等,其中的代码块应该只有一行,该行大抵是一个函数调用语句 ,块内调用的函数应拥有较具说明性的名称。(从而增加了文档上的价值)
-
3.函数不应该大到足以容纳嵌套结构,所以,函数的缩进层级不该多于一层或两层 。
2.只做一件事
-
1.函数应该做一件事。做好这件事,只做这一件事 。 刚开始学习编程讲函数就是用来做一件事的,可是真正做到的大概就微乎其微了。
-
2.如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。
-
3.要判断函数是否不止做了一件事,就是看看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现
-
4.只做一件事的函数无法被合理地切分为多个区段。
3.每个函数一个抽象层级
-
1.要确保函数只做一件事,函数中的语句都要在同一抽象层级上 。
-
2.自顶向下读代码:向下规则,让代码拥有自顶向下的阅读顺序,让每个函数后面都跟着下一抽象层级的函数(最下面有优美的代码)。
4.switch语句
-
1.写出短小的switch语句很维,写出只做一件事的switch语句也很难,Switch天生要做N件事 。
-
2.将switch语句埋到抽象工厂底下,不让任何人看到。
-
3.如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍。
示例:
public Money calculatePay(Employee e) throws InvalidEmployeeType{
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
该函数(包括它调用的其他函数)问题:
- 第一,它太长,当出现新的雇员类型时还会变得更长。
- 第二,它明显做了不止一件事。
- 第三,它违反了单一权责原则(Single Responsibility Principle, SRP), 因有好几个修改它的理由。
- 第四,它违反了开放闭合原则(Open Closed Principle, OCP), 因为每当添加新类型时,就必须修改之。不过,最麻烦的是每个调用的函数中都会有类似结构的函数,如:isPayday(Employee e, Date date)或deliverPay(Employee e, Money pay)。
解决方案是多态,将switch语句埋到抽象工厂底下 。calculatePay、isPayday和deliverPay等函数则由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 (r.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.使用描述性的名称
-
1.沃德原则:“如果每个例程都让你感到深合已意,那就是整洁代码”。
-
2.函数越短小,功能越集中,就越便于取个好名字。
-
3.别害怕长名称,长而具有描述性的名称,要比短而令人费解的名称好 。
-
4.命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。
如includeSetupAndTeardownPages、includeSetupPages、includeSuiteSetupPage和includeSetupPage。这些名称使用了类似的措辞,依序讲述了一个故事。
6.函数参数
-
1.最理想的参数数量是零,有足够的理由才能用三个以上参数。
-
2.事件:在这种形式中,有输入参数而无输出参数,程序将函数看作一个事件,使用该参数修改系统状态。
-
3.对于转换,使用输出参数而非返回值令人迷惑,如果函数要对输入参数进行转换操作,转换结果就该体现为返回值。
-
4.向函数传入布尔值会使方法签名立刻变得复杂起来,大声宣布函数不止做一件事。
-
5.如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。
-
6.有可变参数的函数可能是一元、二元甚至三元,超过这个数量就可能要犯错了。
-
7.对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式。
7.无副作用
- 1.函数承诺只做一件事,但还是会做其他被藏起来的事,会导致古怪的时序性耦合及顺序依赖
自己写的一个函数名叫做 requestOrganList() ,意为请求机构列表信息,在处理响应结果的时候顺便增加了一个默认选择第一个机构的功能。多亏今天遇到的 bug 让我发现了这个问题。
- 2.参数多数会被自然而希地看作是函数的输入。
8.分割指令与询问
函数要么做什么事,要么回答什么事,但二者不可得兼。
set 和 get 大概是最能说明的两个函数了。
示例
bool set(int x, int val){
if (vis[x]) {
return 0;
} else {
vis[x] = val;
return 1;
}
}
int main(){
if (set(5, 1)) {
printf("error\n");
} else {
printf("succsee\n");
}
return 0;
}
但是这样这个函数其实是做了两个事情,判断和插入。其实应该修改为这样的形式:
bool isVisisted(int x){
return vis[x];
}
void set(int x, int val){
vis[x] = val;
}
int main(){
if(isVisisted(5)){
printf("error\n");
} else {
set(5, 1);
printf("success\n");
}
return 0;
}
9.使用异步替代返回错误码
- 1.从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓励了在if语句判断中 ==把指令当作表达式== 使用。
if (deletePage(page) == E_OK)
- 2.try/catch代码块把错误处理与正常流程混为一谈,最好把try和catch代码块的主体部分抽离出来,另外形成函数 .错误处理就是一件事,处理错误的函数不该做其他事 。
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("configKey not deleted");
}
} else {
logger.log("deleteReference from registry failed");
}
} else {
logger.log("delete failed");
return E_ERROR;
}
改为:
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
logger.log(e.getMessage());
}
- 3.如果使用异常代替返回错误码,错误处理代码就能从主路径中分离出来,得到简化
- 4.依赖磁铁(dependency magnet):其他许多类都得导入和使用它。
public enum Error{
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_ENVENT;
}
- 5.当返回错误码时,就是在要求调用者立刻处理错误。
优势
- 错误不可忽略,必须显式处理
- 不需要额外的措施就能传播到上层
- 可以携带更丰富的信息
- 常错误码在不同的库中有不同的定义,必须手动转换成统一的形式,而异常不需要这样的转换
10.别重复自己
重复可能是软件中一切邪恶的根源,许多原则与实践规则都是为控制与消除重复而创建
11.结构化编程
-
1.每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或者continue语句,而且永永远远不能有任何的goto语句
-
2.只有在大函数中这些规则才会有明显好处,因为,只要函数保持短小,偶尔出现的return、break或continue语句没有坏处,goto语句尽量避免
12.如何写出这样的函数
-
1.打磨代码,分解函数、修改名称、消除重复
-
2.缩短和重新安置方法、拆散类、保持测试通过
13.小结
编程艺术是且一直就是语言设计的艺术。
真正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。
14.附录
package fitnesse.html;
import fitnesse.responders.run.SuiteResponder;
import fitnesse.wiki .;
public class SetupTeardownIncluder{
private PageData pageData;
private boolean isSuite;
private WikiPage testPage;
private StringBuffer newPageContent;
private PageCrawler pagecrawler;
public static String render(pageData) throws Exception {
return render(pageData, false);
}
public static String render(PageData pageData, boolean isSuite)throws Exception{
return new SetupTeardownIncluder(pageData).render(isSuite);
}
private SetupTeardownIncluder(PageData pageData){
this.pageDatapageData;
testPage=pageData.getWiki Page);
pageCrawler=testPage.get PageCrawler();
newPageContent new StringBuffer();
}
private String render(boolean isSuite)throws Exception {
this.isSuite isSuite;
if (isTestPage())
includeSetupAndTeardownPages();
return pageData.getHtml();
}
private boolean isTestPage)throws Exception{
return pageData.hasAttribute("Test");
}
private void includeSetupAndTeardownPages()throws Exception{
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}
private void includeSetupPages throws Exception{
if(isSuite)
includeSuiteSetupPage);
includeSetupPage();
}
private void includeSuiteSetupPage()throws Exception{
include(SuiteResponder.SUITE_SETUP_NAMB,"-setup");
}
private void includeSetupPage()throws Exception{
include("SetUp","-setup");
}
private void include PageContent(throws Exception {
newPageContent.append(pageData.getcontent());
}
private void includeTeardownPages(throws Exception {
includeTeardownPage();
if (isSuite)
includeSuiteTeardownPage();
}
private void includeTeardownPage(throws Exception {
include("TearDown", "-teardown");
}
private void includeSuiteTeardownPage()throws Exception {
include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
}
private void updatePageContent)throws Exception{
pageData.setContentnewPageContent.tostring());
}
private void include(String pageName,string arg)throws Exception{
WikiPage inheritedPage findInheritedPag(pageName);
if(inheritedPage=null){
string pagePathName-getPathNameFor(inheritedPage);
buildIncludeDirective(page PathName,arg):
}
}
private WikiPage findInheritedPage(String pageName)throws Exception {
return PageCrawlerImpl.getInheritedPa(pageName, testPage);
}
private String getPathName ForPage(WikiPage page)throws Exception {
WikiPagePath pagePath pageCrawler.getFullPath(page);
return PathParser.render(pagePath);
}
private void buildIncludeDirective(String pagePathName,String arg){
newPagecontent
.append("n! include")
.append(arg)
.append(".")
.append(page PathName)
.append("\n");
}
}
15.参考文献
cloud.tencent.com/developer/a… www.zybuluo.com/king/note/6… theprimone.top/2019/04/08/… www.jianshu.com/p/c4aab60da…
关注公众号“程序员面试之道”
回复“面试”获取面试一整套大礼包!!!
本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!