美文网首页
5分钟看完代码整洁之道

5分钟看完代码整洁之道

作者: 灯不利多 | 来源:发表于2018-05-17 14:13 被阅读22次

1. 整洁代码

糟糕的代码

糟糕的代码会导致一个系统难以维护,软件故障迟迟无法修复,严重时会毁了一家公司。

混乱的代价

赶上期限的唯一办法就是尽可能保持代码整洁,否则当下一个任务来时你又会被上一次写的糟糕代码给拖后腿

童子军规则

让营地比你来时更干净。每次更新时的代码都比提交时的代码干净,那么代码就不会腐坏。清理也许只是改好一个变量名,拆分一个过长的函数,消除重复的代码,清理一个嵌套语句。

2. 有意义的命名

名副其实

如果名称需要注释来补充,那就不算名副其实。

int d;  // 消逝的时间,以日计

名称 d 没有说明任何东西。应该选择指明了计量对象和计量单位的名称:

int elapsedTimeInDays;
int daysSinceCreation;

下面这段代码难以看出来是做什么的

public List<int[]> getThem() {
  List<int[]> list1 = new ArrayList<int[]>();
  for (int[] x : theList)
    if (x[0] == 4)
      list.add(x);
  return list1;
}

假如我们在开发一款扫雷游戏,盘面是名为 theList 的单元格列表,每个单元格都有状态值,状态值为 4 标识已标记,则修改后的代码如下

public List<int[]> getFlaggedCells() {
  List<int[]> flaggedCells = new ArrayList<int[]>();
  for (int[] cell : gameBoard)
    if (cell[STATUS_VALUE] == FLAGGED)
      flaggedCells.add(cell);
  return flaggedCells;
}

再进一步,不用 int 数组表示单元格, 而是另写一个类,修改后的代码如下

public List<Cell> getFlaggedCells() {
  List<Cell> flaggedCells = new ArrayList<int[]>();
  for (Cell cell : gameBoard)
    if (cell.isFlagged()) 
      flaggedCells.add(cell);
  return flaggedCells;
}

避免误导

当你用小写字母 l 和大写字母 O 作为变量名时,代码看上去是这样的

int a = l;
if (0 == l)
  a = O1;
else 
  l = 01;

做有意义的区分

以数字系列(a1、a2、……、aN)命名只会误导其他人,没有提供作者意图的线索,比如下面的代码

pubic void copyChars(char a1[], char a2[]) {
  for (int i = 0; i < a1.length; i++) {
    a2[i] = a1[i];
  }
}

假如我们是要把源数组赋值给目标数组,那修改后的代码是这样的

pubic void copyChars(char source[], char destination[]) {
  for (int i = 0; i < a1.length; i++) {
    destination[i] = source[i];
  }
}

假设你现在有 Product 类、ProductInfo类 和 ProductData 类,它们虽然名字不同,但意思确实相同的,Info 和 Data 是意义含糊的废话。

不要画蛇添足。Variable 不应该出现在变量名中,Table 不应该出现在表名中。NameString 不比 Name 好,因为 Name 不可能是一个浮点数。

如果缺少明确规定,那 moneyAmount 就和 money 没区别,customerInfo 与 customer 没区别,accountData 与 account 没区别。

假如你正在创建一个抽象工厂,那 ShapeFactory 就比 IShapeFactory 好,因为用户没必要知道这是一个接口,而接口的实现用 ShapeFactoryImpl 就好了。

使用读得出来的名称

你知道下面的代码怎么读吗?

class DtaRcrd102 {
  private Date genymdhs;
  private final String pszqint = "102";
}

再看看这个

class Customer {
  private Date generateionTimstemp;
  private final String recordId = "102";
}

“嘿,老王,看看这条记录,生成时间戳被设置成明天了,不能这样吧?”

使用可搜索的名称

你很难搜索到单字母名称和数字常量。找 MAX_CLASSES_PER_STUDENT 很容易,但想找数字 7 就麻烦了,它很可能是其他变量或常量的一部分。比如下面的代码

for (int j = 0; j < 34; j++) {
  s += (t[j] * 4) / 5;
}

类名

类名应当是有意义的名词,比如 Csutomer、WikiPage 和 AddressParser,避免使用 Manage、Process、Data 或 Info 这样的动词和意义含糊的名词。

方法名

方法名应当是动词或动词短语,如 deletPage 或 save

别耍宝

holyShit 这样的方法名或许会博人一笑,但是没有人能从这个名字看出来它是做什么的。

每个概念对应一个词

fetch、retrieve 和 get 都有获取的意思,整个系统中应该只使用其中一个,否则难以记住哪个类中是哪个方法。

DeviceManager 和 ProtoolConroller 有什么区别呢?为什么不统一用 Manager 或 Controller 呢?一以贯之的命名对用到你代码的程序员来说就是天降福音。

最后的话

有时候我们会怕其他开发者反对重命名。但是如果讨论一下就知道,如果名称改好了,大家真的会感激你。

不妨在你的工作中试试上面这些规则,看看代码可读性是否有所提升。

3. 函数

下面代码想做的事情是测试 HTML 页面(省略前约为 50 行)

// 代码清单 3-1
public static String testtableHTML (
  PageData pageData,
  boolean includeSuiteSetup
) throws Exception {
  WikiPage wikiPage = pageData.getWikipage();
  StringBuffer buffer = new StringBuffer();
  if (pageData.hasAttribute("Test")) {
    if (includeSuiteSetup) {
      WikiPage suiteSetup = 
        PageCrawlerImpl.getInheritedPage(SuiteResponder.SITE_SETUP_NAME, wikipage);
      if (suiteSetup != null) {
        WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup);
        Strubg pagePathName = PathParser.render(pagePath);
        buffer
          .append("!include -setup .")
          .append(pagePathName)
          .append("\n");
      }
      WikiPage setup = PageCrawlerImpl.getInheritedPage("Setup", wikiPage);
      if (setup != null) ...;
      if (pageData.hasAttribute("Test")) ...;
      if (tearDown != null) ...;
      if (includeSuiteSetup) ...;
    }
  }
  return pageData.getHtml();
}

上面这段代码做了太多事情,抽离和重命名的方式重构后,代码如下

public static String renderPageWithSetupAndTeardowns (
  PageData pageData, boolean isSuite) throws Exception {
  boolean isTestPage = pageData.hasAttribute("Test");
  if (isTestPage) {
    WikiPage testPage = pageData.getWikipage();
    StringBuffer newPageContent = nwe StringBuffer();
    includeSetupPages(testPage, newPageContent, isSuite);
    newPageCOntent.append(pageData.getCOntent());
    includeTeardownPages(testPage, newPageContent, isSuite);
    pageData.setContent(newPageContent.toString);
  }
  return pageData.getHtml();
}

函数要短小

比如上面的代码可以缩短成

// 代码清单 3-3
public static String renderPageWithSetupAndTeardowns (
  PageData pageData, boolean isSuite) throws Exception {
  if (isTestPage(pageData))
    includeSetupAndTeardownPages(pageData, isSuite);
  return pageData.getHtml();
}

if 、else 和 while 等语句中的代码块应该只有一行,函数的缩进层级不该多于一层或两层。

函数应该只做一件事

代码清单 3-1 做了好几件事。创建缓存区、获取页面、渲染路径等等。而代码清单 3-3 则只做了一件事,将对测试的设置和拆解包含在页面中。

只做一件事的函数有个特征,就是无法被切分成多个区段。

函数参数

最理想的参数数量是无参数,其次是单参数,再次是双参数,应避免三参数。

参数封装

如果函数需要两个、三个或三个以上,那就说明其中一些参数应该封装成类了。比如下面的代码

图14
Circle makeCircle(double x, double y);
Circle makeCircle(Point center);

动词与关键词

一元函数的函数名和参数应当形成一种良好的动词/名词对形式,比如 write(name) 就很清晰。

无副作用

下面的代码匹配 username 和 password,但是它有副作用,你看得出来吗

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

上面代码的副作用就在于 Session.initialize() 的调用。函数名叫做 checkPassword,但是却进行了会话初始化。因此当你误信它时,就会有抹除会话数据的风险。

你可能想到了把它重名为 checkPasswordAndInitializeSession,但是这样却违反了 “只做一件事” 的规则。

分开操作和判断

如何设置某个属性成功则返回 true,不成功则返回 false,那就会写出这样的语句

public boolean set(String attribute, String value);

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

这是什么意思?问的是 username 是否已经是 unclebob?还是 username 是否设置成功为 unclebob 呢?下面是把操作与判断分开后的代码

if (attributeExists("username")) {
  setAttribute("username", "unclebob");
  ...
}

使用异常替代返回错误码

不使用异常的代码是这样的

if (deletePage(page) == OK) {
  if (registry.deleteReference(page.name) == OK) {
    logger.log("page deleted");
  } else {
    logger.log("deleteReference from registry failed");
  }
} else {
  logger.log("delete failed");
  return ERROR;
}

而使用异常的代码是这样的

try {
  deletePage(page);
  registry.deleteReference(page.naem);
  logger.log("page deleted");
} catch (Exception e) {
  logger.log(e.getMessage());
}

抽离 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);
}

4. 注释

注释不能美化糟糕的代码

写注释的常见动机之一就是解释糟糕的代码。但是糟糕代码最好的解决办法是把代码弄干净。

用代码阐述

你是想看到这样的代码

// 检查雇员是否有权享受员工福利
if ((employee.flags & HOURLY_FLAG) && (employye.age > 65))

还是这个

if (employee.isEligibleForFullBenifits())

好注释

法律信息

版权和著作权声明就有理由放在每个源文件开头处,例如下面的

// Copyright (C) 2003, 2004, 2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public Licence version 2 or later.

警示

用警告的方式告诉其他程序员会出现某种后果的注释也是有用的,比如下面的的注释解释了为什么要关闭某个特定的测试用例

// 除非你闲得慌,否则别运行
public void testWithReallyBifFile() {
  writeLinesToFile(10000000);
  response.setBody(testFile);
  response.readyToTsend(this);
  String responseString = output.toString();
  asert...
}

TODO 注释

有时用 TODO 的行驶在源代码中放置要做的工作,比如下面的注释解释了为什么该函数的实现部分无所作为,应该是怎么样的

// TODO 
// 这个函数在我们检查模型的时候不需要
protected VersionInfo makeVersion() throws Exception {
  return null;
}

公共 API 中的 Javadoc

如果你在编写公共 API ,就该为它编写良好的 Javadoc。但是要注意,和其他注释一样,Javadoc 也可能误导或提供错误信息

坏注释

循规式注释

要求每个函数都有 Javadoc 是多余的,比如下面的代码

/** 
 * @param title CD 标题
 * @param author CD 作者
 */
public void addCd(String title, String author) {
  Cd cd = new Cd();
  cd.title = title;
  cd.author = author;
}

废话注释

一些不言而喻的代码是不需要注释的,比如下面的

// 默认构造器
protected AnnualDateRule() {
  
}

/* 某月天数 */
private int daysOfTheMonth;

注释掉的代码

你注释掉的代码可能别人不敢删,然后就一直留在那里。但是我们已经有像 Git 这种良好的代码管理工具,因此需要注释的代码直接删除即可,它们肯定丢不了。

5. 格式

每个程序员都有自己喜欢的代码风格,但如果在一个团队中工作,则团队成应该使用一种统一的代码风格。

6. 错误处理

别返回 null 值

返回 null 值时有可能导致下面这样的代码,只要一处没检查 null,应用就会崩溃。

public void registerItem(Item item) {
  if (item != null) {
    ItemRegistry registry = persistentStore.getItemRegistry();
    if (registry != null) {
      Item existing = registry.getItme(item.getID());
      if( existing.getBillingPrioed().hasRetailOwner()) {
        existing.register(Item);
      }
    }
  }
}

Java 提供了 emptyList 方法,在需要返回 List 对象的视乎可以这样返回

public List<Employee> getEmployees() {
  if(no employee) return Collections.emptyList();
}

别传递 null 值

比如用下面的方法计算两点的映射

public class MetricsCalculator {
  public dobule projection(Point p1, Point p2) {
    return (p2.x - p1.x) 1.5;
  }
}

这时候如果传入一个 null 值就会抛出空指针异常。null 值几乎永远避免,一般的处理是禁止传入 null 值。

public class MetricsCalculator {
  public double projection(Point p1, Point p2) {
    assert p1 != null : "p1 should not be null";
    assert p2 != null : "p2 should not be null";
    return (p2.x - p1.x) * 1.5;
  }
}

7. 类

类应该短小

单一职责原则

单一职责指的是类或模块应有且只有一条修改的理由。

内聚

类应该只有少量实体变量,类中每个方法都应该操作这些变量中的一个或多个。通常而言,方法操作的变量越多,就越内聚到类上。如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内举行。

但是一般来说,创建这种极大化内聚类是不可取也不可能的。下面有一个 Stack 类的实现,该内有很高的内聚性。

public class Stack {
  private int topOfStack = 0;
  List<Integer> elements = new LinkedList<Integer>();
  
  public int size() {
    return topOfStack;
  }
  
  public void push(int element) {
    topOfStack ++;
    elements.add(element);
  }
  
  public int pop() throws PoppedWhenEmpty {
    if (topOfStakc == 0) 
      throw new PoppedWhenEmpty();
    int element = elements.get(--topOfStack);
    elements.remove(topOfStack);
    return element;
  }
}

为了修改而组织

对于多数系统,修改将一直延续。在整洁的系统中,我们对类加以组织,降低修改的风险。下面是的 Sql 类用来生成 SQL 格式化字符串,但是它还不支持 update 语句,当 Sql 类支持 update 语句时,我们就得对这个类进行修改,随之而来的风险是破坏类中的其他代码。

public class Sql {
  public Sql(String table, Column[] columns){
    ...
  }
  public String create() {
    ...
  }
  public String insert(Object[] fields) {
    ...
  }
}

当新增一条语句时,就要修改 Sql 类,这说明 Sql 违反了 SPR(单一职责)原则。下面的代码把 Sql 中的方法拆分成了几个类。

public abstract class Sql {
  public Sql(String table, Column[] columns){
    ...
  }
}
public class CreateSql extends Sql {
  public CreateSql(String table, Column[] columns) {
    ...
  }
}
public class InsertSql extends Sql {
  public InsertSql(String table, Column[] columns, Object[] fields) {
    ...
  }
}

现在如果需要增加 update 语句,已有的类无需修改,只需要增加新类。

相关文章

  • 一本看错的书20220510

    最近在看《代码整洁之道》,都快看完了,竟然没有一行代码,我开始觉得自己是不是看错书了。 在我的印象中《代码整洁之道...

  • [代码整洁之道]-整洁代码

    前段时间,看了代码整洁之道,顺手做了些笔记,分享给大家,和大家一起探讨整洁代码之道。 1.1要有代码 代码是我们最...

  • 代码整洁之道-<函数>

    代码整洁之道-<函数> 代码整洁之道 一书相关读书笔记,整洁的代码是自解释的,阅读代码应该如同阅读一篇优秀的文章,...

  • 代码整洁之道

    01、有意义的命名 在团队开发中,团队小伙伴编码风格各不相同,一个统一的规范就显得尤为重要,最近在做Code Re...

  • 代码整洁之道

    整洁代码 Leblanc : Later equals never.(勒布朗法则:稍后等于永不) 对代码的每次修改...

  • 代码整洁之道

    海到无边天作岸,山登绝顶我为峰。作为猿类的我们,对自己创造的代码有着一种天生的无比自信。这是好事~可是,对于我们的...

  • 代码整洁之道

    1.一次只做一件事的原则 除了最外边必要的空判断,少用return操作符。原则如下图所示:一次只做一件事情.png...

  • 代码整洁之道

    一.整洁代码 借用一条美国童子军简单军规:让营地笔记来时更干净 二.有意义的命名 2.7避免使用编码编码已经太多,...

  • 代码整洁之道

    大概读了一下《代码整洁之道》这本书,总结如下: 1.变量名:有意义、可读性好 2.避免重复和无意义的条件判断 3....

  • 《代码整洁之道》

    细节之中自有天地,整洁成就卓越代码。 软件专家RoberfC.Marlin在《代码整洁之道》中为你呈现出了革命性的...

网友评论

      本文标题:5分钟看完代码整洁之道

      本文链接:https://www.haomeiwen.com/subject/bxybdftx.html