< Day Day Up > |
3.2 Distilling the ProblemVirtuosos in any profession have a common gift: they can distill a problem to its basic components. In physics, Einstein identified and captured many complex relationships in a simple equation, e=mc2. Beethoven captured and repeated a motif consisting of four notes in his fifth symphony that's endured for centuries. Programming demands the same focus. You've got to take a set of requirements, identify the essential elements, strip away everything that doesn't belong, and finally break down and solve the problem. To improve your programming, you don't have to live in a cave, reading about a design pattern that covers every possibility. You don't need to know the latest, hottest framework. You've just got to focus on the right problem, distill it to the basics, and hammer out the simplest solution that will work. In this section, I'm going to take a set of requirements, distill them, and turn them into code. 3.2.1 Collecting RequirementsLet's take a simple example. Say that you're building an ATM. Your job is to build the support for an account. In keeping with Agile programming practices, you've decided to keep a simple set of requirements in a table. You'll record the requirement number, a brief description, a size (timeline), and choose a programmer. Your team is small and your cycles are short, so that's all that you think you'll need. Table 3-1 shows the basic requirements.
These requirements are typical of the types of things you'll get from your customers. They are far from complete, but that, too, is normal. The job of the requirements document is to accumulate requirements as you understand more about your application. At the moment, you're the one assigned to this task. You'll size tasks later. At this point, you should focus on the problem at hand. Your job is to build support for an account. 3.2.2 Whittling Away the NoiseYour first job is to whittle away some of the noise. Take any of the issues that may fit neatly elsewhere and push them out to the perimeter. Immediately, you recognize that you should separate the user interface from the base account. You also see that keeping a balance means that the account will need to be persistent. You'll use your relational database. Security probably doesn't belong in the account itself; it would probably be better left to another layer of the architecture, like perhaps a façade, but security is also a special case. Too many developers treat security as an afterthought, something they can sprinkle on top of an application to make it "safe." No such pixie dust exists, though; security layers should be treated with the same respect you show to everything else in your code. Essentially, you need to build a persistent account. Rather than trying to build a design document, you'll start to code. It's a small enough problem to get your head around, and you can always refactor. In fact, you'll probably refactor several times as you think of ways to simplify and improve your design. The new set of requirements is shown in Table 3-2.
Keep in mind what you're trying to accomplish. You're not discarding the rest of the tasks. You'll still need security and a user interface. Instead, you first want to carve out a manageable size of development. When that's completed and tested, you can go ahead and layer on the other aspects of the application, like the user interface and the persistence. Also, keep in mind that as an example, these requirements may have a finer grain than they would in a production project. When you've got a rough unit, you can start to code. I'm going to omit the JUnit test cases to keep this example (and this book) brief, but I recommend that you code test cases first, as we did in Chapter 2. The next task is to rough out an interface. You don't yet need a full implementation. I recommend that you oversimplify, attaching everything to a single class until the ultimate design becomes clear. For this example, start with the implementation for requirements 1, 6, and 7, in a simple class called Account. Scoping and a simple constructor can take care of requirement 4. Stub out the rest with empty methods, so that you've got a simple interface. Start by organizing in a package, and add a simple constructor: package bank; public class Account { float balance = 0; private String accountNumber = null; Account (String acct, float bal) { accountNumber = acct; balance = bal; } Next, add the accessors for the members. Remembering your requirements, you want to keep the account number private, so you scope it accordingly, and omit the setter. public float getBalance ( ) { return balance; } public void setBalance(float bal) { balance = bal; } private String getAccountNumber ( ) { return accountNumber; } public float debit(float amount) { balance = balance - amount; return balance; } public float credit(float amount) { balance = balance + amount; return balance; } Finally, add some stubs for methods that you'll need later. You may not decide to do things in this way, but it helps the ultimate design to emerge if you can capture some placeholders. public void save( ) {} public void load( ) {} public void beginTransaction( ) {} public void endTransaction( ) {} public void isValid(String accountNumber) {} } This is a reasonable start. You've covered requirements 1, 3, 4, 5 and 6, and you've got a head start on the rest. You're probably not completely happy with the design. It's already time to refactor. Since you've been writing unit tests all along, you can do so with confidence, knowing that your tests will let you know if you break anything along the way. 3.2.3 Refining Your DesignSometimes, a metaphor can help you analyze your design. In this case, think of the job that we want the account to do. At least four things surface:
Some of my past clients would have stopped designing at this point. If you're an EJB programmer, you're thinking that you've got a match: the class is transactional, persistent, and possibly distributed. Step away from that sledge-o-matic, and pick up a plain old ordinary hammer. It's time to break this puppy down. Not many would complain if you suggested that it's a good idea to separate the business logic from the value object. Today, many modelers like to always separate value objects from the business domain. Persistence frameworks and other middleware made it easier to build systems that way. But designs are simpler and much easier to understand when you can leave them together. Now, think about the save and load methods, as well as the transactional methods. Another metaphor is useful in this situation: think of a folder that holds paper. The paper represents your data and the folder represents a value object. Think of the save and load methods as filing the folder for later access. You would not expect the folder to be able to file itself. In principle, it makes sense to break the persistence methods away from the accessor methods and the business logic. For now, let's move the transactional methods with the persistence. The result is clean, well-defined business logic, and a data access object (DAO) built explicitly to access the database. The DAO should be able to save and retrieve accounts. Here's the code to load an account using JDBC: public static Account load(String acct) throws NotFoundException, SQLException { Account valueObject; ResultSet result = null; String sql = "SELECT * FROM ACCOUNT WHERE (accountNumber = ? ) "; PreparedStatement stmt = null; stmt = conn.prepareStatement(sql); try { stmt.setString(1, acct); result = stmt.executeQuery( ); if (result.next( )) { account.setAccountNumber(result.getString("accountNumber")); account.setBalance((float)result.getDouble("balance")); return account; } else { throw new NotFoundException("Account Object Not Found!"); } } finally { if (stmt != null) { stmt.close( ); } } } The save code is similar. It's a little ugly, but that's okay. You'll only be reading this code when you're interested in the database details. You'll be able to test the business logic of the account without wiring it to the data access object. You'll also be able to add sophistication to the business logic without thinking about persistence, and you can change the persistence layer without impacting the business logic. Consider transactions for a moment. Rather than bringing in the heavyweight artillery like JTA or EJB, start with the simplest solution. You can lean on the transaction support of your database engine and access it through your JDBC connection. That means the JDBC connection should probably be attached elsewhere, because you'll want all of your different data access objects to potentially participate in the same transaction. For example, if a user opened an account, you'd probably want to update the user and the first account deposit at the same time. You know you need to refactor. Where's the correct place for the JDBC connection, and the associated transaction support? It's not in the account itself or the Account data access object. You'll need to create something, and you'll need some type of connection manager. If that strategy doesn't work out, you can always refactor again. Lean on these types of iterative refinements to improve your design as you progress. Although this is a trivial example, it demonstrates how the process works. You write tests, code a little, refactor a little, and repeat the cycle until your eventual design emerges. After wading through these details, it's time to look at issues at a higher level. |
< Day Day Up > |