http://today.java.net/pub/a/today/2006/08/24/five-habits-of-highly-profitable-developers.html
Five Habits of Highly Profitable Software Developers
by Robert J. Miller
08/24/2006
Software developers who have the ability to create and maintain quality software in a team environment are in high demand in today's technology-driven economy. The number one challenge facing developers working in a team environment is reading and understanding software written by another developer. This article strives to help software development teams overcome this challenge.
This article illustrates five habits of software development teams that make them more effective and therefore more profitable. It first will describe the demands the business team puts on its software development team and the software they create. Next it will explain the important differences between state-changing logic and behavior logic. Finally, it will illustrate the five habits using a customer account scenario as a case study.
Demands Placed on Developers by the Business
The business team's job is to determine what new value can be added to the software, while ensuring that the new value is most advantageous to the business. Here, "new value" refers to a fresh product or additional enhancement to an existing product. In other words, the team determines what new features will make the business the most money. A key factor in determining what the next new value will be is how much it will cost to implement. If the cost of implementation exceeds the potential revenue, then the new value will not be added.
The business team demands that the software development team be able to create new value at the lowest possible cost. It also demands that the software development team have the ability to add new value to a product without having the product's implementation costs increase over time. Furthermore, every time the business team requests new value, it demands that value be added without losing any existing value. Over time, the software will accrue enough value that the business team will demand documentation to describe the current value the software provides. Then the business team will use this documentation to help determine what the next new value will be.
Software development teams can best meet these demands by creating easy-to-understand software. Difficult-to-understand software results in inefficiencies throughout the development process. These inefficiencies increase the cost of software development and can include the unexpected loss of existing value, an increase in developer ramp-up time, and the delivery of incorrect software documentation. These inefficiencies can be reduced by converting the business team's demands, even if complex, into simple, easy-to-understand software.
Introducing Key Concepts: State and Behavior
Creating software that is easy to understand starts by creating objects that have state and behavior. "State" is an object's data that persists between method calls. A Java object can hold its state temporarily in its instance variables and can persist it indefinitely by saving it into a permanent data store. Here, a permanent data store can be a database or Web service. "State-changing methods" typically manage an object's data by retrieving it and persisting it to and from a remote data store. "Behavior" is an object's ability to answer questions based on state. "Behavior methods" answer questions consistently without modifying state and are often referred to as the business logic in an application.
Case Study: CustomerAccount object
The following ICustomerAccount interface defines methods an object must implement to manage a customer's account. It defines the ability to create a new active account, to load an existing customer's account status, to validate a prospective customer's username and password, and to validate that an existing account is active for purchasing products.
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();
}
Habit 1: Constructor Performs Minimal Work
The first habit is for an object's constructor to do as little work as possible. Ideally, its constructor will only load data into its instance variables using the constructor's parameters. In the following example, creating a constructor that performs minimal work makes the object easier to use and understand because the constructor performs only the simple task of loading data into the object's instance variables:
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;
}
}
A constructor is used to create an instance of an object. A constructor's name is always the same as the object's name. Since a constructor's name is unchangeable, its name is unable to communicate the work it is performing. Therefore, it is best if it performs as little work as possible. On the other hand, state-changing and behavior method names use descriptive names to convey their more complex intent, as described in "Habit 2: Methods Clearly Convey Their Intent." As this next example illustrates, the readability of the software is high because the constructor simply creates an instance of the object, letting the behavior and state-changing methods do the rest.
Note: The use of "..." in the examples represents code that is necessary to run in a live scenario but is not relevant to the example's purpose.
String username = "robertmiller";
String password = "java.net";
ICustomerAccount ca = new CustomerAccount(username, password);
if(ca.isRequestedUsernameValid() && ca.isRequestedPasswordValid()) {
...
ca.createNewActiveAccount();
...
}
On the other hand, objects with constructors that do more than just load instance variables are harder to understand and more likely to be misused because their names do not convey their intent. For example, this constructor also calls a method that makes a remote call to a database or Web service in order to pre-load an account's status:
//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 {
...
}
A developer can use this constructor and, not realizing it is making a remote call, end up making two remote calls:
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) {
...
}
Or a developer can reuse this constructor to validate a prospective customer's desired username and password and be forced to make an unnecessary remote call since these behavior methods (isRequestedUsernameValid(), isRequestedPasswordValid()) don't need the account status:
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){
...
}
Habit 2: Methods Clearly Convey Their Intent
The second habit is for all methods to clearly convey their intent through the names they are given. For example, isRequestedUsernameValid() lets the developer know that this method determines whether or not the requested username is valid. In contrast, isGoodUser() can have any number of uses: it can determine if the user's account is active, determine if the requested username and/or password are valid, or determine if the user is a nice person. Since this method is less descriptive, it is more difficult for a developer to figure out what its purpose is. In short, it is better for the method names to be long and descriptive than to be short and meaningless.
Long and descriptive method names help developer teams quickly understand the purpose and function of their software. Moreover, applying this technique to test method names allows the tests to convey the existing requirements of the software. For example, this software is required to validate that requested usernames and passwords are different. Using the method name testRequestedPasswordIsNotValidBecauseItMustBeDifferentThanTheUsername() clearly conveys the intent of the test and, therefore, the requirement of the software.
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());
}
}
This test method could have easily been named testRequestedPasswordIsNotValid() or even worse testBadPassword(), both of which would make it hard to determine the precise intention of the test. Unclear or ambiguously named test methods result in a loss of productivity. The loss in productivity can be caused by an increase in ramp-up time used to understand the tests, the unnecessary creation of duplicated or conflicting tests, or the destruction of existing value in the object being tested.
Finally, descriptive method names reduce the need for both formal documentation and Javadoc comments.
Habit 3: An Object Performs a Focused Set of Services
The third habit is for each object in the software to be focused on performing a small and unique set of services. Objects that perform a small amount of work are easier to read and more likely to be used correctly because there is less code to digest. Moreover, each object in the software should perform a unique set of services because duplicating logic wastes development time and increases maintenance costs. Suppose in the future, the business team requests an update to the isRequestedPasswordValid() logic and two different objects have similar methods that perform the same work. In this case, the software development team would spend more time updating both objects than they would have had to spend updating just one.
As the case study illustrates, the purpose of the CustomerAccount object is to manage an individual customer's account. It first creates the account and later can validate that the account is still active for purchasing products. Suppose in the future, this software will need to give discounts to customers who have purchased more than ten items. Creating a new interface, ICustomerTransactions, and object, CustomerTransactions, to implement these new features will facilitate the ongoing goal of working with easy-to-understand software:
public interface ICustomerTransactions {
//State-changing methods
public void createPurchaseRecordForProduct(Long productId)
throws CustomerTransactionsSystemException;
public void loadAllPurchaseRecords()
throws CustomerTransactionsSystemException;
//Behavior method
public void isCustomerEligibleForDiscount();
}
This new object holds state-changing and behavior methods that store customer transactions and determine when a customer gets its ten-product discount. It should be easy to create, test, and maintain since it has a simple and focused purpose. The less effective approach is to add these new methods to the existing ICustomerAccount interface and CustomerAccount object, as seen below:
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();
}
As seen above, allowing objects to become large repositories of responsibility and purpose makes them harder to read and more likely to be misunderstood. Misunderstandings result in a loss in productivity, costing the business team time and money. In short, it is better for objects and their methods to be focused on performing a small unit of work.
Habit 4: State-Changing Methods Contain Minimal Behavior Logic
The fourth habit is for state-changing methods to contain a minimal amount of behavior logic. Intermixing state-changing logic with behavior logic makes the software more difficult to understand because it increases the amount of work happening in one place. State-changing methods typically retrieve or send data to a remote data store and, therefore, are prone to have problems in the production system. Diagnosing a system problem within a state-changing method is easier when the remote call is isolated and it has zero behavior logic. Intermixing also inhibits the development process because it makes it harder to unit test the behavior logic. For example, getPostLogonMessage() is a behavior method with logic based on the accountStatus's value:
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() is a state-changing method that loads the accountStatus's value from a remote data store:
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(1, this.username);
ps.setString(2, this.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) {}
}
}
}
Unit testing the getPostLogonMessage() method can easily be done by mocking the loadAccountStatus() method. Each scenario can then be tested without making a remote call to a database. For example, if the accountStatus is "E" for expired, then getPostLogonMessage() should return "Your purchasing account has expired due to a lack of activity," as follows:
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());
}
The inverse approach is to put both the getPostLogonMessage() behavior logic and the loadAccountStatus() state-changing work into one method. The following example illustrates what not to do:
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(1, this.username);
ps.setString(2, this.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.";
}
}
In this implementation the behavior method getPostLogonMessage() contains zero behavior logic and simply returns the instance variable this.postLogonMessage. This implementation creates three problems. First, it makes it more difficult to understand how the "post logon message" logic works since it is embedded in a method performing two tasks. Second, the getPostLogonMessage() method's reuse is limited because it must always be used in conjunction with the loadAccountStatus() method. Finally, in the event of a system problem the CustomerAccountsSystemOutageException will be thrown, causing the method to exit before it sets this.postLogonMessage's value.
This implementation also creates negative side effects in the test because the only way to unit test this getPostLogonMessage() logic is to create a CustomerAccount object with a username and password for an account in the database with an accountStatus set to "E" for expired. The result is a test that makes a remote call to a database. This causes the test to run slower and to be prone to unexpected failures due to changes in the database. This test has to make a remote call to a database because the loadAccountStatus() method also contains the behavior logic. If the behavior logic is mocked, then the test is testing the mocked object's behavior instead of the real object's behavior.
Habit 5: Behavior Methods Can Be Called in Any Order
The fifth habit is to ensure that each behavior method provides value independent of any other behavior method. In other words, an object's behavior methods can be called repeatedly and in any order. This habit allows the object to deliver consistent behavior. For example, CustomerAccount's isActiveForPurchasing() and getPostLogonMessage() behavior methods both use the accountStatus's value in their logic. Each of these methods should be able to function independently of the other. For instance, one scenario can require that isActiveForPurchasing() be called, followed by a call to 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();
}
A second scenario can require that getPostLogonMessage() is called without isActiveForPurchasing() ever being called:
ICustomerAccount ca = new CustomerAccount(username, password);
ca.loadAccountStatus();
//go to "welcome back" display
...
//show post logon message.
ca.getPostLogonMessage();
The CustomerAccount object will not support the second scenario if getPostLogonMessage() requires isActiveForPurchasing() to be called first. For example, creating the two methods to use a postLogonMessage instance variable so that its value can persist between method calls supports the first scenario but not the second:
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;
}
On the other hand, if both methods derive their logic independently of each other, then they will support both scenarios. In this preferred example, postLogonMessage is a local variable created exclusively by the getPostLogonMessage() method:
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.";
}
}
An added benefit of making these two methods independent of each other is that the methods are easier to comprehend. For example, isActiveForPurchasing() is more readable when it is only trying to answer the "is active for purchasing" question as opposed to when it is also trying to set the "post logon message". Another added benefit is that each method can be tested in isolation, which also makes the tests easier to comprehend:
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());
}
}
Conclusion
Following these five habits will help development teams create software that everyone on the team can read, understand, and modify. When software development teams create new value too quickly and without consideration for the future, they tend to create software with increasingly high implementation costs. Inevitably, bad practices will catch up to these teams when they have to revisit the software for future comprehension and modification. Adding new value to existing software can be very expensive if the software is difficult to comprehend. However, when development teams apply these best practices, they will provide new value at the lowest possible cost to their business team.
posted on 2006-12-15 17:27 liujg 阅读(699) 评论(0) 编辑 收藏