目录
整洁代码重要性
有意义的命名
函数
注释
格式
对象和数据结构
错误处理
边界
单元测试
类
系统
迭进
总结
推荐一本书:罗伯特 C. 马丁的《代码整洁之道》。
组内最近在强调研发意识,即对线上有一颗敬畏之心:
营地意识:让代码比你来的时候更干净,警惕破窗效应;
信息同步:变更同步关键角色,相信群众力量;
风险意识:提高风险评估意识,凡事留个后路;
刨根问题:问题追查到底,而非点到为止;
自食其力:切勿依赖测试同学兜底。
本文旨在讨论营地意识,此篇为理论篇,分别从命名、函数、注释、格式、对象和数据结构、错误处理、边界、单元测试、类、系统等多个方面进行阐述。
下篇地址为:>>>
整洁代码重要性
1. 糟糕代码会毁了公司,代码整洁不仅关乎效率,还关乎生存;赶上DDL/做得快的唯一方法是保持代码整洁。
2. 假如你是位医生,病人请求做手术前别洗手,因为会花费太多时间,医生会选择拒绝遵从,因为了解疾病和感染的风险。同理,程序员遵从不了解混乱风险的产品经理的意愿,是不专业的。
3. 什么是整洁的代码?Bjarne说整洁的代码只做好一件事情,完善的错误处理;Dave说易修改、“尽量少”以及字面上表达其含义;Ron说不要重复代码,只做一件事。
有意义的命名
变量、函数、参数、类和封包均需命名。
1. 名副其实:体现本意的名称更容易理解和修改;使用读得出来的名称;使用可搜索的名称:长名胜于短名。
优化前:
//读者很容易有以下问题:
//1.list1是什么类型的东西?
//2.list[0]是什么意义?
//3.4的意义是什么?
//4.怎么使用返回的列表?
public List<int[]> getTheme() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
优化后:
//1.盘名为gameBoard,而非theList的单元格列表;
//2.不用int数组表示单元格,而是另一个类;
//3.类中包含一个名副其实的函数isFlagged(),掩盖魔术数。
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}
2. 避免误导,别用双关语:1.避免留下掩藏代码本意的错误线索。eg:推荐accountGroup;不推荐hp(专有名称)、accountsList(如果非List类型,会误导);2.提防外形相似度高的名称。
3. 做有意义的区分:1.不推荐数字系列命名;2.废话是没有意义区分,ProductData和ProductInfo无区别,variable不应该出现在变量中,table永远别出现在表名中。
4. 避免使用编码:1.成员前缀:作者不推荐,但我觉得成员变量m打头、静态变量s打头,普通变量无打头更具标示性。2.接口与实现:接口用IShapFactory,实现用ShapeFactoryImpl。
5. 类名和方法名:类名是名词或名词短语(eg:Customer、WikiPage、Account等);方法名是动词或动词短语(eg:postPayment、deletePage等)。
6. 添加有语义的语境:几个变量能组合成类的可以抽象为类。
函数
1. 短小:20行封顶最佳,每个函数依序把你带到下一个函数。
2. 只做一件事:应该做一件事,只做一件事,做好一件事;无副作用:函数承诺只做一件事。要么做什么事,要么回答什么事,二者不可兼得。
3. 抽象层级:函数中的语句要在同一个抽象级上。区分较高、中间、较低抽象层级。
4. switch语句:1.switch语句处于较低抽象层级且永不重复;2.可以创建多态对象以尽可能避免switch语句。
5. 命名方式:1.长名称;2.花时间打磨;3.命名方式一致。
6. 函数参数:最理想的是0参数,避免3参数以上。1.单参数的普遍形式:询问该参数的问题(boolean isFileExists(XXX));将该参数操作并转换(InputStream fileOpen(XXX));事件(void passwordAttempt(XXX))。2.双参数的普遍形式:尽可能利用一些机制转换为单参数。比如构建新类、某参数变为成员变量等。3.三参数:排序、琢磨、忽略都很重要,如果函数有很多参数,应该要封装成类了。
7. 抽离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());
}
8. 别写重复代码,先写代码再打磨。
重构前:
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");
}
}
//.....
pageData.setContent(buffer.toString());
return pageData.getHtml();
}
重构一次:
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();
}
最终重构:
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
注释
别给糟糕的代码加注释-重新写吧。
1. 原则:花心思减少注释,持续维护注释,因为不准确的注释远比没注释糟糕得多;用代码去阐释注释;注释可附上文档链接;
//重构前:
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) &&
(employee.age > 65))
//重构后:
if (employee.isEligibleForFullBenefits())
2. 注释的作用是:提供信息、对意图的解释、阐释(正确性)、警示、TODO注释、放大不合理之物重要性。
// 提供信息
// format matched kk:mm:ss EEE, MMM dd, yyyy
Pattern timeMatcher = Pattern.compile(
"\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");
//对意图解释
public int compareTo(Object o)
{
//XXXXX
return 1; // we are greater because we are the right type.
}
//阐释
assertTrue(a.compareTo(b) != 0); // a != b
//警示
// Don't run unless you
// have some time to kill.
public void _testWithReallyBigFile()
{
//XXXXX
}
//TODO注释
//TODO-MdM these are not needed
// We expect this to go away when we do the checkout model
protected VersionInfo makeVersion() throws Exception{
return null;
}
//放大不合理之物的重要性
String listItemContent = match.group(3).trim();
// the trim is real important. It removes the starting
// spaces that could cause the item to be recognized
// as another list.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));
3. 警惕:多余和误导性的注释、循规式注释、日志式注释、废话注释、不明显注释等
//多余和误导性的注释
// Utility method that returns when this.closed is true. Throws an exception
// if the timeout is reached.
public synchronized void waitForClose(final long timeoutMillis) throws Exception{
if(!closed)
{
wait(timeoutMillis);
if(!closed)
throw new Exception("MockResponseSender could not be closed");
}
}
//循规式注释
/**
*
* @param title The title of the CD
* @param author The author of the CD
* @param tracks The number of tracks on the CD
* @param durationInMinutes The duration of the CD in minutes
*/
public void addCD(String title, String author,
int tracks, int durationInMinutes) {
CD cd = new CD();
cd.title = title;
//XXX
}
//废话注释
/** The day of the month. */
private int dayOfMonth;
//能用代码别用注释
//重构前
// does the module from the global list <mod> depend on the
// subsystem we are part of?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))
//重构后
ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))
//不明显联系
/*
* start with an array that is big enough to hold all the pixels
* (plus filter bytes), and an extra 200 bytes for header info
*/
this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];
格式
垂直格式:
1. 最多200~500行,向报纸学习。
2. 概念间垂直方向上的间隔:封包声明、导入声明和每个函数之间,都有空白行隔开(每个空行都是一个线索,标识出新概念);靠近的代码行暗示了它们之间的紧密关系;垂直顺序:被调用的函数应该放在执行调用的函数下面,建立了自顶向下贯穿源代码模块的良好信息流。
横向格式:
1. 一行不超过120个字符,一屏。
2. 水平方向上的区隔与靠近:空格字符将相关性较弱的事物区隔开。赋值操作符周围、参数一一隔开等。
代码示例:
public class WikiPageResponder implements SecureResponder {
protected WikiPage page;
protected PageData pageData;
protected String pageTitle;
protected Request request;
protected PageCrawler crawler;
public Response makeResponse(FitNesseContext context, Request request)
throws Exception {
String pageName = getPageNameOrDefault(request, "FrontPage");
loadPage(pageName, context);
if (page == null)
return notFoundResponse(context, request);
else
return makePageResponse(context);
}
private String getPageNameOrDefault(Request request, String defaultPageName)
{
String pageName = request.getResource();
if (StringUtil.isBlank(pageName))
pageName = defaultPageName;
return pageName;
}
protected void loadPage(String resource, FitNesseContext context)
throws Exception {
WikiPagePath path = PathParser.parse(resource);
crawler = context.root.getPageCrawler();
crawler.setDeadEndStrategy(new VirtualEnabledPageCrawler());
page = crawler.getPage(context.root, path);
if (page != null)
pageData = page.getData();
}
private Response notFoundResponse(FitNesseContext context, Request request)
throws Exception {
return new NotFoundResponder().makeResponse(context, request);
}
private SimpleResponse makePageResponse(FitNesseContext context)
throws Exception {
pageTitle = PathParser.render(crawler.getFullPath(page));
String html = makeHtml(context);
SimpleResponse response = new SimpleResponse();
response.setMaxAge(0);
response.setContent(html);
return response;
}
...
对象和数据结构
1. 数据抽象:隐藏实现关乎抽象,类并不是简单地用取值器和赋值器将其变量推向外界,而是暴露抽象接口,以便用户无需了解数据的实现操作数据本体。
2. 数据、对象的反对称性:过程式代码难以添加新数据结构,因为必须修改所有函数;面向对象代码难以添加新函数,因为需要修改所有类。
//过程式形状代码
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topLeft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
}
public class Geometry {
public final double PI = 3.141592653589793;
public double area(Object shape) throws NoSuchShapeException
{
if (shape instanceof Square) {
Square s = (Square)shape;
return s.side * s.side;
}
else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle)shape;
return r.height * r.width;
}
else if (shape instanceof Circle) {
Circle c = (Circle)shape;
return PI * c.radius * c.radius;
}
throw new NoSuchShapeException();
}
}
//多态式形状代码
public class Square implements Shape {
private Point topLeft;
private double side;
public double area() {
return side*side;
}
}
public class Rectangle implements Shape {
private Point topLeft;
private double height;
private double width;
public double area() {
return height * width;
}
}
public class Circle implements Shape {
private Point center;
private double radius;
public final double PI = 3.141592653589793;
public double area() {
return PI * radius * radius;
}
}
3. 得墨忒定律:模块不应该了解它所操作对象的内部细节,链式调用可能会需要各种判空逻辑。
//重构前
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
//重构后
//我们发现,取得临时目录绝对路径的初衷是为了创建制定名称的临时文件。
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
错误处理
1. 使用异常返回而非返回码
//重构前
public class DeviceController {
...
public void sendShutDown() {
DeviceHandle handle = getHandle(DEV1);
// Check the state of the device
if (handle != DeviceHandle.INVALID) {
// Save the device status to the record field
retrieveDeviceRecord(handle);
// If not suspended, shut down
if (record.getStatus() != DEVICE_SUSPENDED) {
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
} else {
logger.log("Device suspended. Unable to shut down");
}
} else {
logger.log("Invalid handle for: " + DEV1.toString());
}
}
}
//重构后
//采用异常处理
public class DeviceController {
...
public void sendShutDown() {
try {
tryToShutDown();
} catch (DeviceShutDownError e) {
logger.log(e);
}
}
private void tryToShutDown() throws DeviceShutDownError {
DeviceHandle handle = getHandle(DEV1);
DeviceRecord record = retrieveDeviceRecord(handle);
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
}
private DeviceHandle getHandle(DeviceID id) {
...
throw new DeviceShutDownError("Invalid handle for: " + id.toString());
...
}
...
}
2. 缩小异常的具体范围。比如将Exception修改为FileNotFoundException
3. 特例模式:创建一个类或配置一个对象,用于处理特例。
//重构前:
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
m_total += getMealPerDiem();
}
//重构后
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
//异常行为被封装至特例对象中
public class PerDiemMealExpenses implements MealExpenses {
public int getTotal() {
// return the per diem default
}
}
4. 别返回null值、别传递null值。
//重构前:
List<Employee> employees = getEmployees();
if (employees != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}
//重构后
//使用Collections.emptyList();返回一个预定义的不可变列表,可用于达到这种目的。避免空指针异常。
List<Employee> employees = getEmployees();
for(Employee e : employees) {
totalPay += e.getPay();
}
public List<Employee> getEmployees() {
if( .. there are no employees .. )
return Collections.emptyList();
}
边界
使用第三方SDK应对其提供的API做好封装。
单元测试
1. 整洁的测试:测试显然呈现构造-操作-检验模式;分三个环节:构造测试数据、操作测试数据、检验操作是否得到期望的结果。
2. FIRST原则:
快速(Fast):测试应当足够快;
独立(Independent):测试应当相互独立,每次只测试一个概念;
可重复(Repeatable):测试应当在任何环境中重复通过;
自足验证(Self-Validating):测试应该有布尔值输出,无需查看日志文件;
及时(Timely):测试应该及时编写。
类
1. 类的组织:
(1)先公共静态变量,后私有静态变量,私有实体变量;
(2)自顶向下:公共函数应在变量列表之后,被调用的私有工具函数跟在公共函数后面;
(3)做好封装,放开封装是下策。
2. 类应当短小:对于函数,是代码行,对于类,是权责。
(1)单一权责原则:系统应该由许多短小的类而不是少数巨大的类组成,每个小类封装了一个权责,只有一个修改的理由,并与其他少数类一起协同达成期望的系统行为。
(2)内聚:一般来说,创造极大化内聚类是不可取也不可能的,另一方面,我们希望内聚性保持较高未知,内聚性高,意味着类中的方法和变量互相依赖、互相组合成一个逻辑整体。
//一个内聚类
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 (topOfStack == 0)
throw new PoppedWhenEmpty();
int element = elements.get(--topOfStack);
elements.remove(topOfStack);
return element;
}
}
(3)保持内聚性就会得到许多短小的类。以文中一段代码(P136)为例:PrimePrinter类中只有主程序,职责是处理执行环境,如果调用方式有变,它也会变化;RowColumnPagePrinter类是将数字列表格式化到固定行列的页面,若输出格式有变化,它也会变化。PrimeGenrator类懂得如何生成素数列表,如果计算素数算法发生改动,则类会改动。
3. 类应当对扩展开放,对修改封闭。
系统
系统应该是整洁的:将构造和使用分开;工厂模式;依赖注入等。后续会在《大话设计模式》具体讨论。
迭进
1. 四条原则:运行所有测试;不可重复;表达程序员的意图;尽可能减少类和方法数量。
2. 运行所有测试:测试用例越多,系统约会贴近低耦合、高内聚的目标
3. 消除重复:抽离方法;模板模式。
//重构前
public void scaleToOneDimension(
float desiredDimension, float imageDimension) {
if (Math.abs(desiredDimension - imageDimension) < errorThreshold)
return;
float scalingFactor = desiredDimension / imageDimension;
scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f);
RenderedOp newImage = ImageUtilities.getScaledImage(
image, scalingFactor, scalingFactor);
image.dispose();
System.gc();
image = newImage;
}
public synchronized void rotate(int degrees) {
RenderedOp newImage = ImageUtilities.getRotatedImage(
image, degrees);
image.dispose();
System.gc();
image = newImage;
}
//重构后
public void scaleToOneDimension(
float desiredDimension, float imageDimension) {
if (Math.abs(desiredDimension - imageDimension) < errorThreshold)
return;
float scalingFactor = desiredDimension / imageDimension;
scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f);
replaceImage(ImageUtilities.getScaledImage(
image, scalingFactor, scalingFactor));
}
public synchronized void rotate(int degrees) {
replaceImage(ImageUtilities.getRotatedImage(image, degrees));
}
private void replaceImage(RenderedOp newImage) {
image.dispose();
System.gc();
image = newImage;
}
//重构前
public class VacationPolicy {
public void accrueUSDivisionVacation() {
// code to calculate vacation based on hours worked to date
// ...
// code to ensure vacation meets US minimums
// ...
// code to apply vaction to payroll record
// ...
}
public void accrueEUDivisionVacation() {
// code to calculate vacation based on hours worked to date
// ...
// code to ensure vacation meets EU minimums
// ...
// code to apply vaction to payroll record
// ...
}
}
//重构后
abstract public class VacationPolicy {
public void accrueVacation() {
calculateBaseVacationHours();
alterForLegalMinimums();
applyToPayroll();
}
private void calculateBaseVacationHours() { /* ... */ };
abstract protected void alterForLegalMinimums();
private void applyToPayroll() { /* ... */ };
}
public class USVacationPolicy extends VacationPolicy {
@Override protected void alterForLegalMinimums() {
// US specific logic
}
}
public class EUVacationPolicy extends VacationPolicy {
@Override protected void alterForLegalMinimums() {
// EU specific logic
}
}
3. 表达力:选择比较好的名称;保持函数和类的短小;多重构;编写好的单元测试。
4. 尽可能少的类和方法:优先级较低。
总结
重要的九条建议:
关于命名:类名和方法名:类名是名词或名词短语(eg:Customer、WikiPage、Account等);方法名是动词或动词短语(eg:postPayment、deletePage等)。添加有语义的语境:几个变量能组合成类的可以抽象为类。
关于函数:短小精悍:20行封顶最佳,每个函数依序把你带到下一个函数。只做一件事:应该做一件事,只做一件事,做好一件事。无副作用:函数承诺只做一件事。要么做什么事,要么回答什么事,二者不可兼得。
关于注释:花心思减少注释,不准确的注释远比没注释糟糕得多;用代码去阐释注释;注释可附上文档链接。
关于格式:垂直方向最多200~500行,向报纸学习。垂直方向顺序:被调用的函数应该放在执行调用的函数下面,建立了自顶向下贯穿源代码模块的良好信息流。水平方向一行不超过120个字符。
关于对象和数据结构:数据、对象的反对称性:过程式代码难以添加新数据结构,因为必须修改所有函数;面向对象代码难以添加新函数,因为需要修改所有类。
关于错误处理:别返回null值、别传递null值。创建一个类或配置一个对象,用于处理特例。
关于边界:FIRST原则:快速(Fast);独立(Independent);可重复(Repeatable);自足验证(Self-Validating);及时(Timely)。
关于类:类应当短小:单一权责原则,内聚性高,保持内聚性就会得到许多短小的类。
关于迭代:四条原则:运行所有测试;不可重复;表达程序员的意图;尽可能减少类和方法数量。