|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| Why do today what you can put off till tomorrow?
Roy
W. Miller (mailto:rmiller@rolemodelsoft.com?cc=&subject=Just-in-time
design) Software Developer, RoleModel Software, Inc. 1 July
2003
People who aren't
familiar with XP are bothered by the concept of just-in-time
(JIT) design -- designing and implementing what you know you need right
now and not worrying about future design issues until absolutely
necessary. While this approach might seem unwise or even reckless, XP
advocate Roy Miller wraps up his series by showing you how safe and easy
JIT design is -- and how it just might revolutionize the way you write
code. Share your thoughts on this article with the author and other
readers in the accompanying discussion forum. (You can
also click Discuss at the top or bottom of the article to access the
forum.)
In Extreme Programming Explained (see Resources),
Kent Beck described a "simple" design as one that:
- Passes all the tests
- Communicates developer intent clearly
- Contains no duplicate code
- Has the minimum number of classes and methods necessary to
accomplish the first three
That definition is good, but it also makes some assumptions. Most
importantly, it assumes you have programmer tests -- and that they
all pass all the time. XP demands that you write tests
before you write code, so a simple design is one that grows over
time in a test-first manner. This approach implies that the design doesn't
emerge fully formed from a design phase, which in turn begs the question:
if you're designing as you go, where do you start? That question
resurfaces at multiple points throughout the project -- every time you
need to make a design decision.
The answer is that you design just what you need to complete the
customer stories for the current iteration -- not a bit more. That goes
for business logic and infrastructure, too. If a user needs to create
employees in the system for this iteration, then build the business
objects and the infrastructure you need to support adding employees.
Don't add the capability to remove an employee yet. Don't even
think about that piece of the design for now. Remember the YAGNI (You
Aren't Gonna Need It) principle: don't build in features, or "hooks," that
you don't know you need yet.
Resisting the urge to be
stupid In all the years I've been in IT and using XP, I
can't think of a single case outside of a committed XP team where I could
say, "Ignore that part of the design for now," and escape unharmed. It
seems like a radical idea -- like you're ignoring a problem and hoping it
will go away. In a way, that's sort of true. But it's not as bad as you
might think, as I'll discuss shortly.
I worked on a system recently that required "integrated user help."
Look carefully at the requirement: Users need to get help within the
application. Does it matter what form that help takes? Not right now. Pop
open a window with static help text in it and you're done. We could assume
that context-sensitive help is a requirement, but that's much harder to
implement. We also don't know we need it yet. That's the key. Don't design
(and work on) features that you don't know you need yet. As it turned out,
we didn't need context-sensitive help. Users of the system were
technically savvy. They just needed a ready reference. A PDF was perfectly
acceptable, and cost much less to build in.
But saying you should design for today doesn't mean you should be
foolish. There are plenty of cases where you have to think a bit beyond
today in order to keep from shooting yourself in the foot. This is where
layering can help, and where it's wise to think ahead. For example,
imagine you're building a system that's data intensive. You'll have to hit
a particular database quite a bit. You've done this many times before in
your career, but don't assume the answer you came up with last time is
exactly the right answer this time. Apply the principles you know, but
leave room for learning. In essence, I'm suggesting that you apply
patterns (such as Gang of Four, among others) without getting stuck.
In the case of persistence, for instance, you probably should create a
"persistence layer" in your application. That's just good encapsulation
and smart layering. But don't try to create a generic, all-purpose
persistence layer, since at this point you're not sure you need it.
Instead, build the simplest persistence layer that could possibly work at
this point. Maybe have a class that hides the raw details of your
persistence mechanism, but for now uses SQL strings underneath it. That
will probably change, but it's a good start. You aren't being stupid, but
you aren't assuming too much either -- a difficult but necessary
balance.
From story cards to
code Let's use a simple example to illustrate deferring
decisions until you need to make them. As we did last month, we'll create
a basic banking system for "Roy Miller's Savings and Loan" (deposits are
now being accepted). Suppose we have the following story cards,
prioritized in this order:
- Check the account balance.
- Deposit funds.
- Withdraw funds.
- Transfer funds to another account.
You and your partner start working on the first story, "Check the
account balance." According to the story, you need an Account
class, and you need to be able to ask it for its balance. That's
all. You might feel compelled to ask what the user interface needs
to look like, but there aren't any cards for that so don't even think
about it. You can add it later. Since you should be writing tests first,
you should have a failing test that tells you what the
Account class needs to look like. Your class should look
similar to Listing 1: Listing 1. Account
class
package com.sample;
public class Account {
protected int balance;
public int balance() {
return balance;
}
}
|
You now have all you need to say that the first card -- Check the
account balance -- is done. While you know that eventually you'll
need to be able to deposit funds, you can add that capability when you
work on the "Deposit funds" card. Resist the urge to do that too soon. In
many ways, JIT design is a matter of disciplining yourself to keep from
taking steps that are too big.
According to the next card, you need to deposit funds. No problem.
Write the test, see the code fail, then get it to pass. Your code should
look something like Listing 2: Listing 2. Account
with deposit capability
package com.sample;
public class Account {
protected int balance;
public int balance() {
return balance;
}
public void deposit(int amount) {
balance += amount;
}
}
|
You're now done with the second card. We still can't withdraw anything,
but so what? That's another card, which you're now free to tackle. Write a
test that the code in Listing 3 will pass. Listing 3.
Account with withdraw capability
package com.sample;
public class Account {
protected int balance;
public int balance() {
return balance;
}
public void deposit(int amount) {
balance += amount;
}
public void withdraw(int amount) {
balance -= amount;
}
}
|
You're done again. You can now move on to what is potentially the most
dangerous card in the stack so far: the "Transfer funds to another
account" card. This is the most dangerous card for two reasons. First,
it's the last card, so we can't see what's coming up. Not knowing
increases the temptation to build in some "extras" that we don't know we
need yet. Second, the feature itself can quickly get fancier than
necessary. This is where it pays to be disciplined. We have to resist the
urge to create too much, no matter how much we want to.
The first thing to do is write a test for the simplest possible
transfer behavior, such as the code in Listing 4: Listing 4. JUnit test for transferring funds
package com.sample;
import junit.framework.TestCase;
public class TC_Account extends TestCase {
public TC_Account(String name) {
super(name);
}
public void testTransferFunds() {
Account accountOne = new Account();
accountOne.balance = 1;
Account accountTwo = new Account();
accountTwo.balance = 1;
accountOne.transferFundsTo(1, accountTwo);
assertEquals(0, accountOne.balance());
assertEquals(2, accountTwo.balance());
}
}
|
Getting the test to pass should be simple and will help you to resist
getting too fancy. You should write a transferFundsTo()
method so that Account looks similar to Listing 5: Listing 5. Account with transfer capability
package com.sample;
public class Account {
protected int balance;
public int balance() {
return balance;
}
public void deposit(int amount) {
balance += amount;
}
public void withdraw(int amount) {
balance -= amount;
}
public void transferFundsTo(int amount, Account account) {
withdraw(amount);
account.deposit(amount);
}
}
|
Notice what our code has so far: first, our Account class
isn't a bean (it essentially has a getter -- but no setter -- for its one
data member). Based on the cards we've got right now, however, it doesn't
need to be. Second, what happens when somebody tries to transfer funds
from one account to another, but the source account doesn't have a large
enough balance? Currently, the balance will just go negative. Of course
we'll need to address this issue, but not yet. Third, we don't validate
the incoming amounts for deposit() , withdraw() ,
or transferFundsTo() . It's not hard to handle that situation,
and adding the code won't take long, but we'll resist the urge to do more
than we have to for right now. Finally, we don't have a user interface,
and we can't save any data yet. We can't have a "real" system without
them, but we'll create them when we need them, not before.
The power of being
myopic When you try to design to an extremely low level of
detail before you start writing code you plan to keep, you're assuming you
can know what the end result should look like. That's a dangerous
assumption. If things change while you're in the middle of coding, your
design becomes invalid. There are three ways you can handle this likely
situation: you can deny it will happen and just rigorously adhere to your
design. The result, however, is that users get the system they
thought they wanted when the project started, not the system they
end up wanting when the project's done.
Where do we go from here? We've come
to the end of our tour of Extreme Programming; I hope you've enjoyed
learning about this methodology as much as I have enjoyed writing
about it -- and using it in my own projects. While the column will
end with this installment, I'll still be manning the associated discussion forum to
address any questions you may have on XP, and if it becomes clear
that another installment is warranted, I'll be happy to
accommodate. |
Another common approach is to do the up-front design anyway, then do a
comprehensive redesign when change happens. However, this approach
disrupts the project's rhythm and just repeats the problem at multiple
points during the project (big design up front, big redesign two months
later, and so on).
The wisest choice is to be intentionally shortsighted. Remember,
though, being intentionally short-sighted is not the same as being stupid,
irresponsible, or unprofessional.
If you approach design with a JIT mentality -- writing tests for the
current iteration's cards, then making those tests pass -- you can avoid
over-designing and having to redesign. Instead, defer design decisions you
don't have to make. When you defer design decisions, you're reserving your
option to change. This approach leads to better systems.
Resources
- Participate in the discussion forum on this
article. (You can also click Discuss at the top or bottom of the
article to access the forum.)
- Are you joining this column late in the game? See where it all
started, with the first article co-written with Chris Collins, "XP
distilled" (developerWorks, March 2001). Then, take a look at
the complete
set of articles from this column.
- Kent Beck started the XP craze with Extreme
Programming Explained: Embrace Change (Addison-Wesley, 1999).
- In this interview
(developerWorks, June 2003), Kent Beck offers insight on design
patterns, the Java platform's utility for producing quality software,
pair programming, and what developers can do to improve their software
efforts.
- Design
patterns (Addison-Wesley, 1995) by Erich Gamma, et al., is a
classic and a must for any programmer.
- Web Services and
Service-Oriented Architectures is one of the best Web sites for
information about database issues, namely OO systems. You'll find links
for OR mapping, OODBs, and so on, and also many good ideas for layering
your persistence and why that makes sense.
- Read Java
Tools for Extreme Programming (John Wiley & Sons, 2001) by
Richard Hightower and Nicholas Lesiecki for a book-length description of
various Java tools that support XP, including JUnit and Ant.
- Find hundreds more Java technology resources on the developerWorks
Java technology zone.
About the
author Roy W. Miller has been a software developer and
technology consultant for almost ten years, first with Andersen
Consulting (now Accenture) and currently with RoleModel Software,
Inc. in North Carolina. He has used heavyweight methods and agile
ones, including XP. He co-authored a book in the Addison-Wesley XP
Series (Extreme Programming Applied: Playing to Win) and is
currently writing a book about complexity, emergence, and software
development (the working title is Growing Software: Debunking the
Myth of Prediction and Control). Contact Roy at mailto:rmiller@rolemodelsoft.com
or mailto:roy@roywmiller.com. You
can also visit his personal Web site at http://www.roywmiller.com/ |
|
|