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.
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:
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.
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.
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.
Long method names
2006-08-25 05:29:23 panurgy
[Reply | View]
The problem with very long "descriptive" method names is that over time, the actual work being done by the method may be altered, but the method name can't be changed because lots of other deployed code calls that method.
Suppose your code has a method like public void startUpAllServices throws ServiceException, and two years later the design changes such that there are "core" services, and "optional" services. The problem now is that all of the existing/deployed code uses this method to start up everything, and if one of the optional services fails to start (and throws a ServiceStartupException), then everything is toast. So the developers are forced to change this method to start only the "core" services (and mark the method as being deprecated) for backward compatibility.
Long method names are nice, until you're locked into a method name that no longer accurately matches its purpose. That's why Javadoc is a better tool for explaining what a method does. Give methods a "brief" name, and then Javadoc it.
Long method names
2006-08-28 07:15:50 herorev
[Reply | View]
It shouldn't be that hard to rename something. Code should be flexible and able to support change, right? I think more emphasis should be put on making identifiers more flexible to change and not so carved in stone. Many IDEs fail badly on this aspect.
Long method names
2006-08-25 06:42:51 wipu
[Reply | View]
I strongly disagree with you.
"the actual work being done by the method may be altered, but the method name can't be changed because lots of other deployed code calls that method"
So you are proposing that the contract of the method should deliberately be left vague so that its behaviour can be easily changed afterwards without need to modify the callers of the method?
"Javadoc is a better tool for explaining what a method does."
What a nightmarish API results from these principles. Every time you download a new library version, the compiler is happy, but you need to read the whole javadoc to find out whether the assumptions you made when you wrote your code are still valid.
No, the method name should clearly say what it does, and javadoc should only contain higher level explanations (when needed). Then, if the method behaviour needs to be altered, it is first deprecated, and a new method with a new name is created. Then, possibly, the deprecated method is deleted later.
Java API violates these principles
2006-08-25 03:15:01 jayaprabhakar
[Reply | View]
This is a nice article.
Java API specs explicitly violates a few of the principles.
Like, 'ResultSet.next()' contains both state changing and behaviour logic.
Iterator.remove() method tightly coupled with Iterator.next()...
Should a CustomerAccount know how to load oneself?
2006-08-24 18:43:22 cwilkes
[Reply | View]
I'm confused as to the methods isRequestedUsernameValid() and isRequestedPasswordValid() on a CustomerAccount. A better way might be to put into another "service" interface like this:
interface ICustomerAccountService
public ICustomerAccount getCustomerAccount(String userName, String password) throws NoSuchUserException, BadPasswordException
and then you can make the CustomerAccount object more like a java bean, instead of this (eventual) mishmash of ways of knowing how to load itself and validate its own password.
Should a CustomerAccount know how to load oneself?
2006-08-25 07:53:15 ferr0084
[Reply | View]
This is certainly a viable option but it's not object oriented. When you separate the data from the logic you get what's called an anemic domain model (http://www.martinfowler.com/bliki/AnemicDomainModel.html) which results in a procedural style design. I know this because I deal with half a million lines of this %(@*&! every day.
Habits 1-4 vs Clear and Clean
2006-08-24 16:51:47 folletto
[Reply | View]
Nice explanation, however I think that it will be more interesting if it was starting from the concept behind and not from the habit deriving from that concept.
For example:
Habit 1: Constructor Performs Minimal Work
Concept: clean and clear code
Habit: minimal work
You'll see that performing minimal work is just one way to have a clean and clear code.
So, I may as well:
do complex operations, since the method name isn't the only name there. We have also the object name, so the object IS descriptive of what the object itself does;
assign just variables AND call other private function, well named and well defined.
For example, a "Book" class could have a constructor accepting the book name. So the constructor could perform the assignment of the private variable "name" AND query the DB getting the book data, using a private function "getBookFromDB" or anything else. This code will be both clean and clear.
Also, this class will be cleaner to be used, since the code using it doesn't need to perform an operation that will be performed for sure each time we need a book. And this point is related to "code usability" issues, rarely understood by programmers.
Habits 1-4 vs Clear and Clean
2006-08-28 22:00:58 robertjmiller
[Reply | View]
Cool idea, thanks for the feedback.
Weird choice of names
2006-08-24 16:34:17 dserodio
[Reply | View]
"Behaviour Methods" and "State Methods"? Why not stick to the more commonly used query and command? Other than that, nice article.
Weird choice of names
2006-08-30 12:59:33 mariogleichmann
[Reply | View]
+1
Seems your mixing service with domain entity
2006-08-24 13:12:01 matthewadams
[Reply | View]
I generally agree with the statements being made in the article. However, I think that the article could be a bit clearer by separating what seems to be an amalgam of the service from the domain entity.
For example, I would call the "ICustomerAccount" the "ICustomerAccountService" with implementation CustomerAccountService. The method on ICustomerAccountService would be "public long createNewActiveAccount() throws CustomerAccountsSystemOutageException;" which returns the id of the newly created customer account for later retrieval, unless a DTO of some kind or a detached CustomerAccount entity itself could be returned instead, which leads me to my next point.
I would provide another object representing the domain entity, namely "CustomerAccount", that is persistent in some way which need not be detailed in the article. To make minimal changes to your example, its variables & methods would be:
protected String username;
protected String password; // char[] is more secure
protected String status;
// getter methods, setter methods
Now, with the entity, you can take one of two approaches: (1) ensure the entity is always in a valid state or (2) allow the entity to be in an invalid state and add a "public boolean isValid()" method to the entity.
If you choose the former (ensure that your entity is always in a valid state), then the entity's constructor should call the setUsername() and setPassword() methods, which should then do the validation on the values and throw if they are invalid, and the constructor should also set the status field to a reasonable default value. This has an impact on Habit #1, "Constructor Performs Minimal Work"; it's just that the amount of minimal work is a little bit more in this case.
If you choose the latter (allow entities to be in invalid states), then the constructor should just slam the values in and some object, whether it be this domain object or some validation helper object, should provide validation methods that a client can call or that the domain object can call in its isValid() method. This takes Habit #1 to an extreme.
The reason that I say all of this is because of Habit #3: "An Object Performs a Focused Set of Services". The ICustomerAccount interface and accompanying implementations don't do that -- it looks like a combination of a service object and a domain entity.
Habit #4 is a bit tough: "State-Changing Methods Contain Minimal Behavior Logic". There are several different kinds of logic, and I challenge anyone to satisfactorily define the term "business logic" or "behavior logic". Nonetheless, in an entity, "behavior logic" is often "state-changing" logic, whether it has to do with changing its primitive- and/or system-type fields, or changing the collaborations it has with other entities. In a service, "behavior logic" often means "workflow logic". My advice there is to keep the logic restricted to a scope that is appropriate for that particular service object. In the case of ICustomerAccountService, there should only be logic concerning customer accounts. Anything that happens within the enterprise in response to creations, changes, or deletions of customer objects (like credit approvals, notifications, etc) should be handled elsewhere and initiated through either some kind of event-based system (read: ESB) or in the workflow logic of some higher level service object that is specifically responsible for this workflow. I tend to prefer event-based systems a bit more here for their flexibility, but the trade-off is often complexity.
I am complete agreement with Habits 2 & 5. Always name your methods clearly, and don't presume that your clients know in what order methods must be called.
--matthew
Seems your mixing service with domain entity
2006-08-28 22:31:33 robertjmiller
[Reply | View]
Thanks for your feedback. Based on your metaphors, I prefer, in most cases, to have the service object and a domain entity be one class. Reason being that it is less objects for a new developer to read and understand. With that said, the tricky part is keeping that single object from becoming unwieldy. Adhering to my own habits is a daily challenge and they don't work in every situation. Therefore, I like to think of them as a guide that developers (me included) can keep in mind when developing software.
I agree with your response to Habit #4, '"behavior logic" is often "state-changing" logic'. How much logic exists in remote calls to a database or how much logic the database performs is a conundrum I didn't want to tackle in this article. With that said, there are many ways to do it and each problem has its own set of options. Most importantly, I'm encouraging developers to separate methods with remote calls from local business logic methods whenever possible.
I like your ESB idea. My teammates and I have implemented a system like that before but I am not confident that we did a good job. Warranted complexity in one set of objects tends to give developers the bad idea that all of the objects in the system need to fit into a more complex design. Therefore, when in doubt, I encourage other developers to keep it simple by having each object manage its own state changing and behavior methods.
--Rob
Seems your mixing service with domain entity
2006-08-30 10:54:28 matthewadams
[Reply | View]
Based on your metaphors, I prefer, in most cases, to have the service object and a domain entity be one class. Reason being that it is less objects for a new developer to read and understand.
So, are you saying that you combine them only because new developers can understand one object more easily than they can a layered design? It took me longer to understand the nature of your object because it was both a service object and an entity. The separation of service v. entity classes is a well documented idea, for example, in the "session facade" pattern where EJB session beans are service objects fronting and delegating work to entities.
In fact, I go one further than that pattern suggests, and separate the EJB session bean-specific parts from the service logic, where you end up with a session bean that delegates to a POJO service object, which delegates to entities. This provides for a much more flexible design, because you can use your POJO service classes & entites outside of the EJB container quite easily, for example, in a Spring context or simple servlet container.
This kind of layering has driven the success of frameworks like Spring & EJB 3.0. To wholly discount this in the name of fewer objects seems to be misguided -- the combined object will be much more likely to be unwieldly than objects whose scope is more restricted.
--matthew
Seems your mixing service with domain entity
2006-08-31 12:58:50 robertjmiller
[Reply | View]
Two years ago, I was "Johnny Layers" and "Johnny Use Every Design Pattern Possible" whenever I started a project. Since then I have bought into agile methodologies. Now, I create new software systems by starting with objects that simply have state and behavior. Over time, as the system grows and expands in responsibility, I look for opportunities to apply the design patterns you described above. I prefer to apply a desing pattern at the moment when the software needs it, not a minute before and not a minute after. The anti-pattern to using design patterns is to over use design patterns resulting in a system that is too complex for its requirements.
With that said, whenever I start a web project I start with an MVC framework instead of using Servlets and JSP. So that makes me a walking contradiction right? Maybe, maybe not. If a new requirement on the system is going to need to integrate multiple components (a database, a web service, and a file system while responding to requests over the web) then I am going to evaluate design patterns that can be leveraged to solve this problem. However, if my new requirement is less complicated then I won't start with a design pattern. And when in doubt, I'll just follow my five habits and see where the requirements take me.
Therefore, based on your feedback (which I greatly appreciate) I would now say that my five habits are especially valuable when creating or modifying components in a team environment with at least one team member maintaining an architect's perspective. The architect(s) monitor(s) the overall design of the software to help the other team members apply design patterns that will help keep the system's design flexible and the team agile.
These five habits are something to think about when developing software. They are not steadfast rules that must be followed in every case. If they spark design discussions amongst your team members then I would consider them valuable.
thanks, rob miller
The profitable developer will avoid horrible boilerplate
2006-08-24 09:49:51 mernst
[Reply | View]
this.accountStatus = (String)jdbc.queryForObject(
"SELECT status FROM customer_account "+
"WHERE username = ? AND password = ?",
new Object[] { this.username, this.password },
String.class);
The profitable developer will avoid horrible boilerplate
2006-08-24 12:22:40 robertjmiller
[Reply | View]
You are correct. However, in this article I didn't want to tackle the topic of persistence frameworks. That discussion is worthy of a separate article. Instead, I chose basic JDBC statements to keep the example simple so that a higher percentage of readers would understand it.
Thanks.
order the source file carefully
2006-08-24 07:14:14 malcolmdavis
[Reply | View]
Its amazing that people still put private variables at the top of files as this article demonstrates, and the lack of separation between methods. If this article is an example of doing things the right way, then items should flow from public to private to help with readability. If Im working from a service/interface perspective with your class, I do not care to see the private methods/variables. The private methods/variables might change, that is why they are private. The fact of the matter, how a username is stored is of little importance, (it might be a String, char, MyString, or something else entirely different, the point is, it does not matter, and maybe shouldnt be in the example at all).
order the source file carefully
2006-08-25 06:50:21 wipu
[Reply | View]
I think the common practice of putting fields first and methods second and ignoring visibility is good.
First: you should use the navigation functionality of your IDE so you really don't need to read the java files sequentially. For example, in Eclipse, either ctrl-click a reference or press ctrl-o to navigate to a field or a method in the current file.
Or, as a user of a class, you don't need to read the definition of the class at all. Just ctrl-space to see what visible fields and methods it provides.
Second: if visibility affects a field's or a method's position, you'll get ugly differences in the version control.
order the source file carefully
2006-08-24 16:29:08 headius
[Reply | View]
I disagree with splitting up where variables are located based on visibility. I would agree with ordering them appropriately within the same area of a class. All visibilities of methods may access the same set of variables, and if you have those variables spread across a class determining how they are used without a fancier IDE is extremely difficult. Imagine if you split up public, private, protected, and package-visibility variables throughout the file...when encountering a given variable in a method's implementation, where do you look?
The internals of a class are not part of its public API, so requiring such contortions of variables only complicates maintaining that class. I would rather you non-IDE, non-JavaDoc users have to scroll down a bit to get to the public methods than every developer who follows me having to skip around the file looking for variables.
order the source file carefully
2006-08-24 12:27:12 robertjmiller
[Reply | View]
You have a valid point from the service/interface perspective. I probably didn't make my thoughts clear enough. The purpose of making the software easier to read and understand is so that another developer will be more successful when modify existing objects. In that case, the private member variables are important.
Thanks.
order the source file carefully
2006-08-24 07:56:08 jattardi
[Reply | View]
I have to disagree with you, malcolmdavis. If you're working with a class from a service/interface perspective, you should be going by the API documentation/Javadoc, not looking through the source.
Organizing things in the way discussed in the article makes code much easier to maintain with different people in a team.
order the source file carefully
2006-08-24 07:43:11 ilazarte
[Reply | View]
out of curiousity which IDE do you use?
order the source file carefully
2006-08-24 07:56:29 jattardi
[Reply | View]
Not everybody uses IDEs...
order the source file carefully
2006-08-24 17:16:01 smellslikecoffee
[Reply | View]
let me guess, you use notepad and javac? and shell scripts instead of ant?
order the source file carefully
2006-08-25 07:35:58 jattardi
[Reply | View]
Not quite. I use jEdit, and Ant. Most IDEs are so bloated and slow (cue Eclipse, IntelliJ, NetBeans here).