菜鸟笔记
提升您的技术认知

代码整洁之道(上篇)-ag真人游戏

目录

整洁代码重要性

有意义的命名

函数

注释

格式

对象和数据结构

错误处理

边界

单元测试

系统

迭进

总结

推荐一本书:罗伯特 c. 马丁的《代码整洁之道》。

组内最近在强调研发意识,即对线上有一颗敬畏之心:

营地意识:让代码比你来的时候更干净,警惕破窗效应;
信息同步:变更同步关键角色,相信群众力量;
风险意识:提高风险评估意识,凡事留个后路;
刨根问题:问题追查到底,而非点到为止;
自食其力:切勿依赖测试同学兜底。
本文旨在讨论营地意识,此篇为理论篇,分别从命名、函数、注释、格式、对象和数据结构、错误处理、边界、单元测试、类、系统等多个方面进行阐述。

下篇地址为:>>>

整洁代码重要性
1. 糟糕代码会毁了公司,代码整洁不仅关乎效率,还关乎生存;赶上ddl/做得快的唯一方法是保持代码整洁。

2. 假如你是位医生,病人请求做手术前别洗手,因为会花费太多时间,医生会选择拒绝遵从,因为了解疾病和感染的风险。同理,程序员遵从不了解混乱风险的产品经理的意愿,是不专业的。

3. 什么是整洁的代码?bjarne说整洁的代码只做好一件事情,完善的错误处理;dave说易修改、“尽量少”以及字面上表达其含义;ron说不要重复代码,只做一件事。

有意义的命名
变量、函数、参数、类和封包均需命名。

1. 名副其实:体现本意的名称更容易理解和修改;使用读得出来的名称;使用可搜索的名称:长名胜于短名。

优化前:

//读者很容易有以下问题:
//1.list1是什么类型的东西?
//2.list[0]是什么意义?
//3.4的意义是什么?
//4.怎么使用返回的列表?
public list gettheme() {
  list list1 = new arraylist();
  for (int[] x : thelist) 
    if (x[0] == 4) 
      list1.add(x);
  return list1;
}
优化后:

//1.盘名为gameboard,而非thelist的单元格列表;
//2.不用int数组表示单元格,而是另一个类;
//3.类中包含一个名副其实的函数isflagged(),掩盖魔术数。
public list getflaggedcells()  {
  list flaggedcells = new arraylist();
  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 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 employees = getemployees();
if (employees != null) {
  for(employee e : employees) {
    totalpay = e.getpay();
  }
}
 
//重构后
//使用collections.emptylist();返回一个预定义的不可变列表,可用于达到这种目的。避免空指针异常。
list employees = getemployees();
for(employee e : employees) {
  totalpay = e.getpay();
}
 
public list 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 elements = new linkedlist();
 
  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)。
关于类:类应当短小:单一权责原则,内聚性高,保持内聚性就会得到许多短小的类。
关于迭代:四条原则:运行所有测试;不可重复;表达程序员的意图;尽可能减少类和方法数量。

网站地图