# clean code -- 代码整洁之道

# 1. clean code (整洁代码)

# 2. Meaningful Names (有意义的命名)

# 2.1. Use Intention-Revealing Names (使用透露意图的名称)

The name of a variable, function, or class, should answer all the big questions:

  • why it exists
  • what it does
  • how it is used

bad:

int d; // elapsed time in days

If a name requires a comment, then the name does not reveal its intent.

good:

int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

What is the purpose of this code?

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

The code implicitly requires that we know the answers to questions such as:

  1. What kinds of things are in theList?
  2. What is the significance of the zeroth subscript of an item in theList?
  3. What is the significance of the value 4?
  4. How would I use the list being returned?

Say that we’re working in a mine sweeper game

public List<int[]> getFlaggedCells() {
  List<int[]> flaggedCells = new ArrayList<int[]>();

  for (int[] cell : gameBoard)
    if (cell[STATUS_VALUE] == FLAGGED)
      flaggedCells.add(cell);

  return flaggedCells;
}

We can go further and write a simple class for cells instead of using an array of ints.

 public List<Cell> getFlaggedCells() {
  List<Cell> flaggedCells = new ArrayList<Cell>();

  for (Cell cell : gameBoard)
    if (cell.isFlagged())
      flaggedCells.add(cell);

  return flaggedCells;
 }

# 2.2. Avoid Disinformation (避免误导)

Programmers must avoid leaving false clues that obscure the meaning of code.

不要使用专有名称

  • 如 Unix 平台的专有名词: hp, aix, sco

不要使用特殊含义的名称

  • List,列表的意思
  • accountList --> accountGroup / bunchOfAccounts / accounts

不要使用相似度很小的名称

  • XYZControllerForEfficientHandlingOfStrings
  • XYZControllerForEfficientStorageOfStrings
  • 极端的例子
    int a = l;
    if ( O == l )
      a = O1;
    else
      l = 01;
    

# 2.3. Make Meaningful Distinctions (有意义的区分)

you can’t use the same name to refer to two different things in the same scope, you might be tempted to change one name in an arbitrary way:

  • misspelling
  • number series
  • noise words

misspelling:

  • class: klass

number series:

  • a1, a2, .. aN

    public static void copyChars(char a1[], char a2[]) {
      for (int i = 0; i < a1.length; i++) {
        a2[i] = a1[i]; 
      }
    }
    
  • This function reads much better when source and destination are used for the argument names

noise words:

  • Product: ProductInfo , ProductData. Info and Data are indistinct noise words like a, an, and the.
  • The word variable should never appear in a variable name.
  • The word table should never appear in a table name.
  • How is NameString better than Name? Would a Name ever be a floating point number?
  • Imagine finding one class named Customer and another named CustomerObject.

# 2.4. Use Pronounceable Names (使用可以读出来的名字)

If you can’t pronounce it, you can’t discuss it without sounding like an idiot.

bad:

class DtaRcrd102 {
  private Date genymdhms; 
  private Date modymdhms;
  private final String pszqint = "102";
  /* ... */
};

good:

class Customer {
  private Date generationTimestamp; 
  private Date modificationTimestamp;
  private final String recordId = "102";
  /* ... */
};

# 2.5. Use Searchable Names (使用可搜索的名称)

Single-letter names and numeric constants have a particular problem in that they are not easy to locate across a body of text. (单字符名称和数字常量很难被搜索到)

bad:

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

good:

int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;

for (int j=0; j < NUMBER_OF_TASKS; j++) {
  int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
  int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK);
  sum += realTaskWeeks;
}

# 2.6. Avoid Encodings (避免使用编码)

Encoding type or scope information into names simply adds an extra burden of deciphering. (将类型或作用域信息编进名称里面只会增加解码负担)

# 2.6.1. Hungarian Notation (匈牙利命名法)

早期编译器不做类型检查,需要把类型编进名称来记住变量的类型,以 Hungarian Notation(HN)为代表。

现代编译器有强大的类型系统,HN 这种类型编码形式就没必要了。

PhoneNumber phoneString; 
// name not changed when type changed!

# 2.6.2. Member Prefixes (成员前缀)

没必要添加 m_ 成员类型前缀, 应当把类和函数写得足够小,以消除对 m_ 的需要,此外可使用可以通过高亮或颜色来标记成员的编辑器。

bad:

public class Part { 
  private String m_dsc; // The textual description
  void setName(String name) {
    m_dsc = name;
  } 
}

good:

public class Part { 
  String description;
  void setDescription(String description) { 
    this.description = description;
  } 
}

# 2.6.3. Interfaces and Implementations (接口和实现)

有时也会遇到使用编码的情况,比如接口和实现:

bad:

IShapeFactory   ShapeFactory

good:

ShapeFactory   ShapeFactoryImp

# 2.7. Avoid Mental Mapping (避免思维映射)

Professionals use their powers for good and write code that others can understand.

# 2.7.1. Class Names

Classes and objects should have noun or noun phrase names like Customer, WikiPage, Account, and AddressParser.

Avoid words like Manager, Processor, Data, or Info in the name of a class.

A class name should not be a verb.

# 2.7.2. Method Names

Methods should have verb or verb phrase names like postPayment, deletePage, or save.

accessors(访问器): get + name

mutators(修改器): set + name

predicates(断言): is + name

eg:

String name = employee.getName();

customer.setName("mike");

if (paycheck.isPosted())...

# 2.7.3. Overloaded constructors

When constructors are overloaded, use static factory methods with names that describe the arguments.

Consider enforcing their use by making the corresponding constructors private.

bad:

Complex fulcrumPoint = new Complex(23.0);

good:

Complex fulcrumPoint = Complex.FromRealNumber(23.0);

# 2.7.4. Don't Be Cute (别抖机灵)

不要为了好玩而起一个别人很难理解的名字。

不要用俗语或俚语。

bad:

HolyHandGrenade()
whack()
eatMyShorts()

good:

DeleteItems()
kill()
abort()

# 2.7.5. Pick One Word per Concept (一个概念一个词)

对于一个抽象概念,坚持使用一个词

fetch, retrieve, get 所有的类中使用一个即可,不要存在多个

controller, manager, driver 使用一个即可

# 2.7.6. Don’t Pun (不要使用双关语)

比如, add 方法,有的表示将数值相加,有的表示将新元素添加到集合, 这种同一个方法名表示多个意思的就是双关

# 2.7.7. Use Solution Domain Names (使用解决方案相关的名称)

只有程序员才会读你的代码,尽可能使用程序员能懂的名称,如:

  • computer science (CS) terms, e.g. JobQueue
  • algorithm names
  • pattern names , e.g. AccountVisitor
  • math terms

# 2.7.8. Use Problem Domain Names (使用业务相关的名称)

如果不能使用程序员熟悉的术语,就采用业务相关的名称,至少可以去请教业务代表。

# 2.7.9. Add Meaningful Context (添加有意义的上下文)

尽量将名称放置于类、函数、名称空间,给名称提供上下文(语境)。

如果都没有,则添加前缀。

bad: firstName, lastName, state

good: addrFirstName, addrLastName, addrState

bad: (Variables with unclear context.)

private void printGuessStatistics(char candidate, int count) {
  String number;
  String verb;
  String pluralModifier;

  if (count == 0) {
    number = "no";
    verb = "are";
    pluralModifier = "s";
  } else if (count == 1) {
    number = "1";
    verb = "is";
    pluralModifier = "";
  } else {
    number = Integer.toString(count);
    verb = "are";
    pluralModifier = "s";
  
  }
  String guessMessage = String.format(
    "There %s %s %s%s", verb, number, candidate, pluralModifier
  );

  print(guessMessage);
}

good: (Variables have a context.)

public class GuessStatisticsMessage {
  private String number;
  private String verb;
  private String pluralModifier;

  public String make(char candidate, int count) {
    createPluralDependentMessageParts(count);

    return String.format( "There %s %s %s%s", verb, number, candidate, pluralModifier );
  }

  private void createPluralDependentMessageParts(int count) {
    if (count == 0) {
      thereAreNoLetters();
    } else if (count == 1) {
      thereIsOneLetter();
    } else {
      thereAreManyLetters(count);
    }
  }

  private void thereAreManyLetters(int count) {
    number = Integer.toString(count);
    verb = "are";
    pluralModifier = "s";
  }
  private void thereIsOneLetter() {
    number = "1";
    verb = "is";
    pluralModifier = "";
  }
  private void thereAreNoLetters() {
    number = "no";
    verb = "are";
    pluralModifier = "s";
  }
}

# 2.7.10. Don’t Add Gratuitous Context (不要随意添加上下文前缀)

比如有个应用的名称为:Gas Station Deluxe

不要给所有的类添加 GSD 前缀

accountAddresscustomerAddress 用于 Address 类的实例的名称,而不要用于类名。

区分如下地址:

  • MAC addresses : MAC
  • port addresses : PostalAddress
  • Web addresses : URI

# 3. Functions (函数)

HtmlUtil.java

bad:

public static String testableHtml(
  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.SUITE_SETUP_NAME, wikiPage
      );
      if (suiteSetup != null) {
        WikiPagePath pagePath =
        suiteSetup.getPageCrawler().getFullPath(suiteSetup);
        String pagePathName = PathParser.render(pagePath);
        buffer.append("!include -setup .")
          .append(pagePathName)
          .append("\n");
      }
    }
    WikiPage setup =
      PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
    if (setup != null) {
      WikiPagePath setupPath =
      wikiPage.getPageCrawler().getFullPath(setup);
      String setupPathName = PathParser.render(setupPath);
      buffer.append("!include -setup .")
        .append(setupPathName)
        .append("\n");
    }
  }
  buffer.append(pageData.getContent());
  if (pageData.hasAttribute("Test")) {
    WikiPage teardown =
      PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
    if (teardown != null) {
      WikiPagePath tearDownPath =
      wikiPage.getPageCrawler().getFullPath(teardown);
      String tearDownPathName = PathParser.render(tearDownPath);
      buffer.append("\n")
        .append("!include -teardown .")
        .append(tearDownPathName)
        .append("\n");
    }

    if (includeSuiteSetup) {
      WikiPage suiteTeardown =
        PageCrawlerImpl.getInheritedPage(
          SuiteResponder.SUITE_TEARDOWN_NAME,
          wikiPage
        );
      if (suiteTeardown != null) {
        WikiPagePath pagePath =
        suiteTeardown.getPageCrawler().getFullPath (suiteTeardown);
        String pagePathName = PathParser.render(pagePath);
        buffer.append("!include -teardown .")
          .append(pagePathName)
          .append("\n");
      }
    }
  }
  pageData.setContent(buffer.toString());
  return pageData.getHtml();
}

good: (一般般)

public static String renderPageWithSetupsAndTeardowns(
  PageData pageData, boolean isSuite ) throws Exception {
  boolean isTestPage = pageData.hasAttribute("Test");
  if (isTestPage) {
    WikiPage testPage = pageData.getWikiPage();
    StringBuffer newPageContent = new StringBuffer();
    includeSetupPages(testPage, newPageContent, isSuite);
    newPageContent.append(pageData.getContent());
    includeTeardownPages(testPage, newPageContent, isSuite);
    pageData.setContent(newPageContent.toString());
  }
  return pageData.getHtml();
}

# 3.1. small (小)

函数尽可能小:

  • 不超过 20 行
  • if/else/while 等语句,代码块应该只有一行
  • 缩进层级不超过 一层或两层
  • 没有嵌套结构

再次重构:

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

# 3.2. Do One Thing (只做一件事)

function 将大象装进冰箱() {
  打开冰箱门();
  将大象放进冰箱();
  关上冰箱门();
}

function 将大象放进冰箱() {
  // ...
}

函数是什么?

  • 函数名称 就是一个抽象概念(将大象装进冰箱)
  • 函数体 就是将当前抽象概念分解为多个下一级的抽象概念
  • 可类比大纲,一级一级的分解

判断一个函数是否只做一件事:

  • 如果一个函数只执行比函数名称低一级的步骤,那么该函数只执行一件事
  • 如果你能从中提取另一个函数,其名称不仅仅是对其实现的重述,则不是做一件事

# 3.3. One Level of Abstraction per Function (一个函数就是一个抽象层级)

为了确保函数只做一件事,需要让函数体的每个语句都处于同一抽象层级。

不要将不同抽象层级的语句混合在一起,否则读者很难判断一个表达式是某个抽象还是细节。

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

To include the setups and teardowns, we include setups, then we include the test page content, and then we include the teardowns.
  
  To include the setups, we include the suite setup if this is a suite, then we include the regular setup.

  To include the suite setup, we search the parent hierarchy for the “SuiteSetUp” page and add an include statement with the path of that page.

  To search the parent. . .

# 3.4. Switch Statements

It’s hard to make a small switch statement. It’s also hard to make a switch statement that does one thing. By their nature, switch statements always do N things.

Unfortunately we can’t always avoid switch statements, but we can make sure that each switch statement is buried in a low-level class and is never repeated.We do this, of course, with polymorphism.

bad:

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

public boolean isPayday(Employee e, Date date) throws InvalidEmployeeType {
  switch (e.type) {
    case COMMISSIONED: return isPaydayOfCommissioned(e);
    case HOURLY: return isPaydayOfHourly(e);
    case SALARIED: return isPaydayOfSalaried(e);
    default: throw new InvalidEmployeeType(e.type);
  }
}

public void deliverPay(Employee e, Money pay) throws InvalidEmployeeType {
  switch (e.type) {
    case COMMISSIONED: return deliverCommissionedPay(e);
    case HOURLY: return deliverHourlyPay(e);
    case SALARIED: return deliverSalariedPay(e);
    default: throw new InvalidEmployeeType(e.type);
  }
}

There are several problems with this function:

  • it’s large, and when new employee types are added, it will grow.
  • it very clearly does more than one thing.
  • it violates the Single Responsibility Principle7 (SRP) because there is more than one reason for it to change.
  • it violates the Open Closed Principle (OCP) because it must change whenever new types are added.

good:

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);
    }
  }
}
本章目录