Five Habits of Highly Profitable Software Developers
by Robert J. Miller
08/24/2006
翻译:Coody Sk8er  http://www.blogjava.net/chords
原文地址:
http://today.java.net/pub/a/today/2006/08/24/five-habits-of-highly-profitable-developers.html



当今技术引领经济社会大量的需要能够在团队环境中开发出稳定质量的软件开发人员。在团队开发的环境中,开发者面对的挑战就是读懂别的开发者写的软件。本文将文章尽力帮助软件开发团队来克服这样的困难。

本文为了阐明了五个让开发团队变得比以往更加高效的好习惯,首先将介绍公司业务对开发团队以及他们开发出软件的需求,接下来会解释状态改变逻辑和行为逻辑之间重要的区别,最后会通过顾客账号这么一个案例来阐述这五个习惯。

业务带给开发人员的需求

公司业务团队的工作就是在决定将哪些对公司业务最有利的新价值可以被加到软件中。这里的“新价值”指的是新产品或者是对现有产品的强化。换句话说就是,业务团队决定什么将给公司带来最多的钱。决定了下个新价值是什么的关键因素是实现它的成本。如果实现的成本超过了潜在收益,那么这个新价值就不会被加到软件中来。

业务团队要求开发团队能够尽可能低成本的,并且是在规定时间内以及在不失去原有价值的情况下创造新价值。当软件增加了一定价值后,业务团队会要求一份描述现有软件所能提供的价值的文档。这个文档将帮助他们决定下一个新价值是什么。

软件开发团队通过创造出容易理解的软件来满足商业团队的需求。难以理解的软件带来的后果就是整个开发过程的低效率。低效率会造成软件开发成本的增加,引起一些预料不到的现有价值的损失,开发周期滚雪球般越拖越长以及交付错误的软件文档。通过改变业务团队的需求,甚至将复杂的软件转变成简单、容易理解的软件,就可以提高开发过程的效率。


介绍关键概念:状态和行为

开发容易理解的软件可以从创建有状态和行为的对象开始。“状态”是对象在调用方法前后所保存的数据。一个JAVA对象的实例变量可以暂时的保持自己的状态,并且可以随时存放到数据存储器里。这里,永久数据存储器可以是数据库或者是Web服务。“状态变更方法”主要管理一个对象的数据存取。“行为”则是一个对象基于状态回答问题的能力。“行文方法”回答问题永远不会修改状态,并且这些方法往往跟一个应用的商业逻辑有关。


案例研究:CustomerAccount对象

下面这个ICustomerAccount接口定义了管理一个客户账号对象必须实现的功能。这个接口定义了可以创建一个新的账号,加载一个已经存在的账号的信息,验证某个账号的用户名和密码,验证购买时这个账号是否是激活的。

public interface ICustomerAccount {
  
//State-changing methods
  public void createNewActiveAccount()
                   
throws CustomerAccountsSystemOutageException;
  
public void loadAccountStatus() 
                   
throws CustomerAccountsSystemOutageException;
  
//Behavior methods
  public boolean isRequestedUsernameValid();
  
public boolean isRequestedPasswordValid();
  
public boolean isActiveForPurchasing();
  
public String getPostLogonMessage();
}


习惯一:构造器尽量少做事

第一个应该养成的喜欢就是让类的构造器尽量的少做些事情。理想的情况就是构造器仅仅用来接受参数给实例变量加载数据。下面一个例子,让构造器做尽可能少的事情会让这个类使用起来比较简单,因为构造器只是简单的给类中的实例变量赋值。

public class CustomerAccount implements ICustomerAccount{
  
//Instance variables.
  private String username;
  
private String password;
  
protected String accountStatus;
  
  
//Constructor that performs minimal work.
  public CustomerAccount(String username, String password) {
    
this.password = password;
    
this.username = username;
  }

}


构造器是用来创建一个类的实例。构造器的名字永远是跟这个类的名字是一样的。既然构造器的名字无法改变,那么它就不能表达出它做的事情的含义。所以,最好是尽可能的让构造器少做点事。另一个方面,状态变更方法和行为方法会通过自己的名字来表达出自己复杂的工作,在“习惯二:方法名要清晰的表现意图”中会详细讲到。下一个例子表明,很大程度上是因为构造器十分的简单,更多的让状态变更和行为方法来完成其他的部分,使得一个软件具有很高的可读性。


注:例子中“...”部分仅仅是真实情景中必须的部分,跟本文要阐述的问题没有关系。

String username = "robertmiller";
String password 
= "java.net";
ICustomerAccount ca 
= new CustomerAccount(username, password);
if(ca.isRequestedUsernameValid() && ca.isRequestedPasswordValid()) {
   
   ca.createNewActiveAccount();
   
}

相反的,如果构造器除了给实例变量赋值以外的事情,将会使代码很难让人理解,并且有可能被误用,因为构造器的名字没有说明要做的意图。例如,下面的代码将调用数据库或者Web服务来预加载账号的状态:

//Constructor that performs too much work!
public CustomerAccount(String username, String password) 
                  
throws CustomerAccountsSystemOutageException {

  
this.password = password;
  
this.username = username;
  
this.loadAccountStatus();//unnecessary work.
}

//Remote call to the database or web service.
public void loadAccountStatus() 
                  
throws CustomerAccountsSystemOutageException {
  
}

别人可能在不知道会使用远程调的情况下使用这个构造器,从而导致了以下个远程调用:

String username = "robertmiller";
String password 
= "java.net"
try {
  
//makes a remote call
  ICustomerAccount ca = new CustomerAccount(username, password);
  
//makes a second remote call
  ca.loadAccountStatus();
}
 catch (CustomerAccountsSystemOutageException e) {
  
}

或者使开发人员重用这个构造器来验证用户名和密码,并且被强制的进行了远程调用,然而这些行为方法(isRequestedUsernameValid(), isRequestedPasswordValid())根本不需要账户的状态:

String username = "robertmiller";
String password 
= "java.net";
try {
  
//makes unnecessary remote call
  ICustomerAccount ca = new CustomerAccount(username, password);
  
if(ca.isRequestedUsernameValid() && ca.isRequestedPasswordValid()) {
    
    ca.createNewActiveAccount();
    
  }

}
 catch (CustomerAccountsSystemOutageException e){
  
}


习惯二:方法名要清晰的表现意图

第二个习惯就是要让所有的方法名字清晰的表现本方法要做什么的意图。例如isRequestedUsernameValid()让开发人员知道这个方法时用来验证用户名是否正确的。相反的,isGoodUser() 可能有很多用途:用来验证账号是否是激活的,用来验证用户名或者密码是否正确,或者是用来搞清楚用户是不是个好人。方法名表意不清,这就很难让开发者明白这个方法到底是用来干什么的。简单的说,长一点并且表意清晰的方法名要比又短又表意不明的方法名好。

表意清晰的长名字会帮助开发团队快速的理解他们软件的功能意图。更大的优点在于,给测试方法也起个好名字会让软件现有的要求更加的清晰。例如,本软件要求验证请求的用户名和用户密码是不同的。使用名为testRequestedPasswordIsNotValidBecauseItMustBeDifferentThanTheUsername()的方法清晰的表达出了方法的意图,也就是软件要达到的要求。

import junit.framework.TestCase;

public class CustomerAccountTest extends TestCase{
  
public void testRequestedPasswordIsNotValid
        BecauseItMustBeDifferentThanTheUsername()
{
    String username 
= "robertmiller";
    String password 
= "robertmiller";
    ICustomerAccount ca 
= new CustomerAccount(username, password);
    assertFalse(ca.isRequestedPasswordValid());
  }

}


这个方法简单的被命名为testRequestedPasswordIsNotValid(),或者更糟的是testBadPassword()。这两个名字都让人很难搞清楚这个方法是用来测试什么的。不清楚或者是模棱两可的测试方法名会带来生产力的损失。从而导致花费来越多的时间来理解测试,不必要的重复测试,或者是破坏了被测试的类。

最后,明了的方法名还能减少文档和注释的工作量。


习惯三:一个对象只进行一类服务。

第三个喜欢就是对象只关心处理一小类独立的服务。一个对象只处理一小部分事情将使得代码更好读好用,因为每个对象代码量很少。更糟糕的是,重复的逻辑将花费很多时间和成本去维护。设想一下,业务部门将来要求升级一下isRequestedPasswordValid()里的逻辑,然而有两个不同的对象却有着功能完全一样但是名字不一样的方法。这种情况下,开发团队要花费更多的时间去更新两个对象,而不是一个。

这个案例表明了CustomerAccount类的目的就是管理一个客户的帐号。它首先创建了一个帐号,然后严整这个帐号能否用来购买产品。假设软件要给所有购买过10件物品以上的客户打折。再创建一个接口叫ICustomerTransactions和一个叫CustomerTransactions的类,这样会让代码更加易懂,并且实现目标。

public interface ICustomerTransactions {
  
//State-changing methods
  public void createPurchaseRecordForProduct(Long productId)
                     
throws CustomerTransactionsSystemException;
  
public void loadAllPurchaseRecords()
                     
throws CustomerTransactionsSystemException;
  
//Behavior method
  public void isCustomerEligibleForDiscount();
}

这个新的类里面有状态变更和行为方法,可以储存客户的交易并且判断是否能够打折。这个类创建起来十分简单,方便测试以及稳定,因为它专注心这一个目标。一个低效率的方法是如同下面的例子一样在ICustomerAccount接口和CustomerAccount类加上新的方法:

public interface ICustomerAccount {
  
//State-changing methods
  public void createNewActiveAccount()
                   
throws CustomerAccountsSystemOutageException;
  
public void loadAccountStatus()
                   
throws CustomerAccountsSystemOutageException;
  
public void createPurchaseRecordForProduct(Long productId)
                   
throws CustomerAccountsSystemOutageException;
  
public void loadAllPurchaseRecords()
                   
throws CustomerAccountsSystemOutageException;
  
//Behavior methods
  public boolean isRequestedUsernameValid();
  
public boolean isRequestedPasswordValid();
  
public boolean isActiveForPurchasing();
  
public String getPostLogonMessage();
  
public void isCustomerEligibleForDiscount();
}

 

就像是上面所看到的一样,这样使得类具有太多职责,难以读懂,甚至更容被易误解。代码被误解的后果就是降低生产力,费时费力。总的来说,最好让一个对象和它的方法集中处理一个小的工作单元。


习惯四:状态变更方法少含有行为逻辑

第四个习惯是让状态变更方法少含有行为逻辑。混合了状态变更逻辑和行为逻辑的代码让人很难理解,因为在一个地方处理了太多的事情。状态变更方法涉及到远程调用来存储数据的话很容易产生系统问题。如果远程方法是相对独立的,并且方法本身没有行为逻辑,这样诊断起状态改变方法就会十分容易。另外一个问题是,混合了行为逻辑的状态代码很难进行单元测试。例如,getPostLogonMessage() 是一个依靠accountStatus的值的行为:

public String getPostLogonMessage() {
  
if("A".equals(this.accountStatus)){
    
return "Your purchasing account is active.";
  }
 else if("E".equals(this.accountStatus)) {
    
return "Your purchasing account has " +
           
"expired due to a lack of activity.";
  }
 else {
    
return "Your purchasing account cannot be " +
           
"found, please call customer service "+
           
"for assistance.";
  }

}


loadAccountStatus()是一个使用远程调用来加载 accountStatus值的状态改变方法。

public void loadAccountStatus() 
                  
throws CustomerAccountsSystemOutageException {
  Connection c 
= null;
  
try {
    c 
= DriverManager.getConnection("databaseUrl""databaseUser"
                                    
"databasePassword");
    PreparedStatement ps 
= c.prepareStatement(
              
"SELECT status FROM customer_account "
            
+ "WHERE username = ? AND password = ? ");
    ps.setString(
1this.username);
    ps.setString(
2this.password);
    ResultSet rs 
= ps.executeQuery();
    
if (rs.next()) {
      
this.accountStatus=rs.getString("status");
    }

    rs.close();
    ps.close();
    c.close();  
  }
 catch (SQLException e) {
    
throw new CustomerAccountsSystemOutageException(e);
  }
 finally {
    
if (c != null{
      
try {
        c.close();
       }
 catch (SQLException e) {}
    }

  }

}


单元测试 getPostLogonMessage()  方法十分简单,只用loadAccountStatus()方法就行了。每个场景都可以在使用远程调用连接数据库的情况下进行测试。例如,如果 accountStatus 的值是E,代表过期,则getPostLogonMessage() 会如下代码显示一样返回 "Your purchasing account has expired due to a lack of activity"

public void testPostLogonMessageWhenStatusIsExpired(){
  String username 
= "robertmiller";
  String password 
= "java.net";
 
  
class CustomerAccountMock extends CustomerAccount{
        
    
public void loadAccountStatus() {
      
this.accountStatus = "E";
    }

  }

  ICustomerAccount ca 
= new CustomerAccountMock(username, password);
  
try {
    ca.loadAccountStatus();
  }
 
  
catch (CustomerAccountsSystemOutageException e){
    fail(
""+e);
  }

  assertEquals(
"Your purchasing account has " +
                     
"expired due to a lack of activity.",
                     ca.getPostLogonMessage());
}

下面这个反例将getPostLogonMessage() 的行为逻辑和loadAccountStatus()的状态转变都放到了一个方法里,我们不应该这么做:

public String getPostLogonMessage() {
  
return this.postLogonMessage;
}

public void loadAccountStatus() 
                  
throws CustomerAccountsSystemOutageException {
  Connection c 
= null;
  
try {
    c 
= DriverManager.getConnection("databaseUrl""databaseUser"
                                    
"databasePassword");
    PreparedStatement ps 
= c.prepareStatement(
          
"SELECT status FROM customer_account "
        
+ "WHERE username = ? AND password = ? ");
    ps.setString(
1this.username);
    ps.setString(
2this.password);
    ResultSet rs 
= ps.executeQuery();
    
if (rs.next()) {
      
this.accountStatus=rs.getString("status");
    }

    rs.close();
    ps.close();
    c.close();  
  }
 catch (SQLException e) {
    
throw new CustomerAccountsSystemOutageException(e);
  }
 finally {
    
if (c != null{
      
try {
        c.close();
       }
 catch (SQLException e) {}
    }

  }

  
if("A".equals(this.accountStatus)){
    
this.postLogonMessage = "Your purchasing account is active.";
  }
 else if("E".equals(this.accountStatus)) {
    
this.postLogonMessage = "Your purchasing account has " +
                            
"expired due to a lack of activity.";
  }
 else {
    
this.postLogonMessage = "Your purchasing account cannot be " +
                            
"found, please call customer service "+
                            
"for assistance.";
  }

}

这个实现了一个没有包含任何行为逻辑的getPostLogonMessage()行为方法,并且简单的返回一个实例变量this.postLogonMessage。这么做有三个问题:第一,很难让人明白"post logon message"这个嵌入到一个方法中的逻辑式怎么完成两个任务的。第二,getPostLogonMessage()方法很难被重用,因为它总是和loadAccountStatus()方法相关联。最后,CustomerAccountsSystemOutageException异常将会抛出,导致了在给this.postLogonMessage赋值前就退出方法了。

这个实现同样创造了负面效应,因为只有创建一个存在于数据库的CustomerAccount对象,并且将账号状态设置成E才能进行对getPostLogonMessage()逻辑的单元测试。结果式这个测试要进行远程调用。这会导致测试的很慢,而且在改变数据库内容的时候很容易出意想不到的问题。由于 loadAccountStatus()方法包含了行为逻辑,测试必须进行远程调用。如果行为逻辑测试失败了,测的只是那个失败的对象行为,而不是真正的对象的行为。


习惯五:可以任意次序调用行为方法

第五个习惯是要保证每个行为方法之间保持着独立。换句话说,一个对象的行为方法可以被重复或任何次序来调用。这个习惯能让对象实现稳定的行为。比如,CustomerAccount's isActiveForPurchasing()和getPostLogonMessage() 行为方法都要用到accountStatus的值。这两个方法必须在功能上相互独立。例如,有一个情景要求调用isActiveForPurchasing(),接着又调用了getPostLogonMessage():

ICustomerAccount ca = new CustomerAccount(username, password);
ca.loadAccountStatus();
if(ca.isActiveForPurchasing())
  
//go to "begin purchasing" display
  
  
//show post logon message.
  ca.getPostLogonMessage();
}
 else {
  
//go to "activate account" display  
  
  
//show post logon message.
  ca.getPostLogonMessage();     
}


 一个发送的情节会要求调用getPostLogonMessage()之前不必调用isActiveForPurchasing():

ICustomerAccount ca = new CustomerAccount(username, password);
ca.loadAccountStatus();
//go to "welcome back" display 

//show post logon message.
ca.getPostLogonMessage();


如果要求调用getPostLogonMessage()之前必须调用isActiveForPurchasing()方法,CustomerAccount 对象将无法支持第二个情景。如果两个方法使用了 postLogonMessage 实例变量来存放两个方法所需要的值,那么这将支持第一个情景,但不支持第二个:

public boolean isActiveForPurchasing() {
  
boolean returnValue = false;
  
if("A".equals(this.accountStatus)){
    
this.postLogonMessage = "Your purchasing account is active.";
    returnValue 
= true;
  }
 else if("E".equals(this.accountStatus)) {
    
this.postLogonMessage = "Your purchasing account has " +
                            
"expired due to a lack of activity.";
    returnValue 
= false;

  }
 else {
    
this.postLogonMessage = "Your purchasing account cannot be " +
                            
"found, please call customer service "+
                            
"for assistance.";
    returnValue 
= false;
  }

  
return returnValue;
}

public String getPostLogonMessage() {
  
return this.postLogonMessage;
}

然而,如果两个方法的逻辑推理是相互独立的,那么就可以支持第二个情景了。在下面的一个例子中,postLogonMessage是getPostLogonMessage()创建的一个局部变量。

public boolean isActiveForPurchasing() {
  
return this.accountStatus != null && this.accountStatus.equals("A");
}

public String getPostLogonMessage() {
  
if("A".equals(this.accountStatus)){
    
return "Your purchasing account is active.";
  }
 else if("E".equals(this.accountStatus)) {
    
return "Your purchasing account has " +
           
"expired due to a lack of activity.";
  }
 else {
    
return "Your purchasing account cannot be " +
           
"found, please call customer service "+
           
"for assistance.";
  }

}

让这两个方法之间相互独立的另一个好处是更容易理解。例如,isActiveForPurchasing()如果只是用来回答如“能否购买”的问题会显得可读性更佳,如果是用来解决“显示登陆消息”就不那么好了。另一个好处就是测试是独立的,让测试更加简单和容易理解:

public class CustomerAccountTest extends TestCase{
  
public void testAccountIsActiveForPurchasing(){
    String username 
= "robertmiller";
    String password 
= "java.net";

    
class CustomerAccountMock extends CustomerAccount{
      
      
public void loadAccountStatus() {
        
this.accountStatus = "A";
      }

    }

    ICustomerAccount ca 
= new CustomerAccountMock(username, password);
    
try {
      ca.loadAccountStatus();
    }
 catch (CustomerAccountsSystemOutageException e) {
      fail(
""+e);
    }

    assertTrue(ca.isActiveForPurchasing()); 
  }
 
  
  
public void testGetPostLogonMessageWhenAccountIsActiveForPurchasing(){
    String username 
= "robertmiller";
    String password 
= "java.net";

    
class CustomerAccountMock extends CustomerAccount{
      
      
public void loadAccountStatus() {
        
this.accountStatus = "A";
      }

    }

    ICustomerAccount ca 
= new CustomerAccountMock(username, password);
    
try {
      ca.loadAccountStatus();
    }
 catch (CustomerAccountsSystemOutageException e) {
      fail(
""+e);
    }

    assertEquals(
"Your purchasing account is active.",
                              ca.getPostLogonMessage());
  }

}


总结

上述的五种习惯会帮助开发团队创造出方便阅读、理解和修改的软件。如果开发团队仅仅是想快速的创造价值而不考虑将来的规划,他们软件的实现将会耗费越来越多的成本。当这些开发团队要审查软件来理解和修改时,不可避免的会遭到自己写的坏代码的报复。如果软件十分难以理解,在增加新价值的时候会花费巨大的代价。然而,一旦开发团队将良好的习惯运用到开发实践中,他们会以最低的成本为业务提供新价值。