Skip to main content

Five Habits of Highly Profitable Software Developers

August 24, 2006

{cs.r.title}







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 href="http://java.sun.com/j2se/javadoc/">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.

Resources

The author would like to thank Gary Brown for teaching him the agile development methodology. He would also like to thank Kelli Moran-Miller and Ethan Vizitei for reviewing this article and providing invaluable feedback.

width="1" height="1" border="0" alt=" " />
Robert J. Miller is an agile web developer at CARFAX, Inc., an MBA student at the University of Missouri-Columbia, and an entrepreneurial scholar at the Missouri Innovation Center.
Related Topics >> Programming   |