目录
整洁代码重要性
有意义的命名
函数
注释
格式
对象和数据结构
错误处理
边界
单元测试
类
系统
迭进
总结
推荐一本书:罗伯特 c. 马丁的《代码整洁之道》。
组内最近在强调研发意识,即对线上有一颗敬畏之心:
营地意识:让代码比你来的时候更干净,警惕破窗效应;
信息同步:变更同步关键角色,相信群众力量;
风险意识:提高风险评估意识,凡事留个后路;
刨根问题:问题追查到底,而非点到为止;
自食其力:切勿依赖测试同学兜底。
本文旨在讨论营地意识,此篇为理论篇,分别从命名、函数、注释、格式、对象和数据结构、错误处理、边界、单元测试、类、系统等多个方面进行阐述。
下篇地址为:>>>
整洁代码重要性
1. 糟糕代码会毁了公司,代码整洁不仅关乎效率,还关乎生存;赶上ddl/做得快的唯一方法是保持代码整洁。
2. 假如你是位医生,病人请求做手术前别洗手,因为会花费太多时间,医生会选择拒绝遵从,因为了解疾病和感染的风险。同理,程序员遵从不了解混乱风险的产品经理的意愿,是不专业的。
3. 什么是整洁的代码?bjarne说整洁的代码只做好一件事情,完善的错误处理;dave说易修改、“尽量少”以及字面上表达其含义;ron说不要重复代码,只做一件事。
有意义的命名
变量、函数、参数、类和封包均需命名。
1. 名副其实:体现本意的名称更容易理解和修改;使用读得出来的名称;使用可搜索的名称:长名胜于短名。
优化前:
//读者很容易有以下问题:
//1.list1是什么类型的东西?
//2.list[0]是什么意义?
//3.4的意义是什么?
//4.怎么使用返回的列表?
public list
list
for (int[] x : thelist)
if (x[0] == 4)
list1.add(x);
return list1;
}
优化后:
//1.盘名为gameboard,而非thelist的单元格列表;
//2.不用int数组表示单元格,而是另一个类;
//3.类中包含一个名副其实的函数isflagged(),掩盖魔术数。
public list
list
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
// 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
if (employees != null) {
for(employee e : employees) {
totalpay = e.getpay();
}
}
//重构后
//使用collections.emptylist();返回一个预定义的不可变列表,可用于达到这种目的。避免空指针异常。
list
for(employee e : employees) {
totalpay = e.getpay();
}
public list
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
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)。
关于类:类应当短小:单一权责原则,内聚性高,保持内聚性就会得到许多短小的类。
关于迭代:四条原则:运行所有测试;不可重复;表达程序员的意图;尽可能减少类和方法数量。