Monday, September 14, 2009

Do you speak SOLID? Part 2

"Where could I put this new functionality?"

Adding a new functionality to an existing code base is a very common situation. If we have the idea of refactoring in mind, the "where" question is declined in two separate questions: the "which" and the "how" questions. We answer the "which" question by looking for the candidate class the new functionality will be connected to.

If we have no time left or no idea about what refactoring can do for us, the anwser to the "how" question might be a modified class where the connection problem has been solved through code insertion. But we know this quick and dirty approach increases the code complexity and reduces its evolutivity.

The good approach is summarized by the Single Responsibility Principle (SRP). This design principle states that:

"There should never be more than one reason for a class to change".

So the best way to connect a new functionality to an existing code base is to put it in a new class and connect this class to the "which" question target class.

For the context of a an existing class that violates the SRP principle the designer toolbox provides us with the "Extract Class" refactoring.

Back to our Bank example, the Account class has two main responsibilities. The first one is to provide the services we can expect for an Account class. But it has also an additional responsibility which is to log operations with a specific format.

The second one that is less related to the bank domain could be extracted and defined as a new class. That is what we propose here.

Here is the modified Account class:

1:  public class Account  
2: {
3: private String name;
4: private double balance = 0.0;
5: private AccountLogger logger;
6: public Account( String name )
7: {
8: this.name = name;
9: logger = new AccountLogger( name );
10: }
11: public String getName()
12: {
13: return name;
14: }
15: public boolean open()
16: {
17: System.out.println( "Opening account " + name );
18: return true;
19: }
20: public boolean close()
21: {
22: if( balance > 0 )
23: {
24: System.out.println( "Closing account " + name );
25: return true;
26: }
27: else
28: {
29: System.out.println( "Account " + name + " not closed" );
30: return false;
31: }
32: }
33: public void deposit( double amount )
34: {
35: balance += amount;
36: writeRecord( "Deposit", amount );
37: }
38: public void withdraw( double amount )
39: {
40: balance -= amount;
41: writeRecord( "Withdraw", amount );
42: }
43: public double getBalance()
44: {
45: return balance;
46: }
47: private void writeRecord( String operation_type, double amount )
48: {
49: logger.writeRecord( operation_type, amount );
50: }
51: }

Here are the main changes. We have written an AccountLogger class to hold all the extracted code. Now an instance of the AccountLogger class is created in the Account class constructor and the writeRecord method delegates to the AccountLogger class the record logging.

The newly created AccountLogger class:
1:  import java.util.Date;  
2: import java.util.Hashtable;
3: import java.io.*;
4: import java.util.Scanner;
5: public class AccountLogger
6: {
7: PrintWriter print_writer = null;
8: enum StorageType { ASCII_STORAGE, XML_STORAGE };
9: StorageType storage;
10: String account_log_name = null;
11: public AccountLogger( String name )
12: {
13: storage = StorageType.ASCII_STORAGE;
14: account_log_name = name + ".txt";
15: }
16: public void setStorage( StorageType storage )
17: {
18: this.storage = storage;
19: }
20: public void writeRecord( String operation_type, double amount )
21: {
22: if( storage == StorageType.ASCII_STORAGE )
23: {
24: writeAsciiRecord( operation_type, amount );
25: }
26: else if ( storage == StorageType.XML_STORAGE )
27: {
28: writeXMLRecord( operation_type, amount );
29: }
30: }
31: public boolean writeAsciiRecord( String operation_type, double amount )
32: {
33: if( print_writer == null )
34: {
35: try {
36: print_writer =
37: new PrintWriter(new FileWriter( account_log_name ) );
38: } catch (IOException ioe) {
39: ioe.printStackTrace();
40: return false;
41: }
42: }
43: Date date = new Date();
44: print_writer.write( date.toString() );
45: print_writer.write( " " );
46: print_writer.write( operation_type ) ;
47: print_writer.write( " " );
48: print_writer.print( amount ) ;
49: print_writer.println() ;
50: print_writer.flush();
51: return true;
52: }
53: private void writeXMLRecord( String operation_type, double amount )
54: {
55: // To be written
56: }
57: }

All the other classes remain unmodified.

Wednesday, August 26, 2009

Do you speak SOLID? Part 1

S.O.L.I.D. is an acronym that is more and more popular today in the software developer's community. This acronym has been proposed by Robert C. Martin and summarizes five design principles in a five letters word.

Each of the letters identifies a specific design principle:

S : Single Responsability Principle (SRP)
O : Open Close Principle (OCP)
L : Liskov Substitution Principle (LSP)
I : Interface Segregation Principle (ISP)
D : Dependency Inversion Principle (DIP)

Like design patterns, design principles are an answer to the software growing complexity and provide key ideas for design quality. I am not about to justify once again why design principles are useful and should be part of every programmer's toolbox. Many object gurus did this better than I would do. I just want to mention one point.

What is interesting with design principles is it is a way to give a name to common design practices. So we can document, we can talk about them. If your software architect comes and tells you, "well, you design is pretty good, there is just a place where I have found an OCP violation that should be fixed". No need for large explaination for this problem. Everybody that is aware of design principles is able to understand what he is talking about.

Following is a simple Banking system that we are going to make evolve step by step for compliance with the 5 SOLID design principles.

Let's start with the Account class. This is the largest one, but the code is simple enough not to require explanations.

1:  import java.io.*;
2: import java.util.Date;
3: public class Account
4: {
5: private String name;
6: private double balance = 0.0;
7: PrintWriter print_writer = null;
8: enum StorageType { ASCII_STORAGE, XML_STORAGE };
9: StorageType storage;
10: public Account( String name )
11: {
12: this.name = name;
13: storage = StorageType.ASCII_STORAGE;
14: }
15: public String getName()
16: {
17: return name;
18: }
19: public boolean open()
20: {
21: System.out.println( "Opening account " + name );
22: return true;
23: }
24: public boolean close()
25: {
26: if( balance > 0 )
27: {
28: System.out.println( "Closing account " + name );
29: return true;
30: }
31: else
32: {
33: System.out.println( "Account " + name + " not closed" );
34: return false;
35: }
36: }
37: public void deposit( double amount )
38: {
39: balance += amount;
40: writeRecord( "Deposit", amount );
41: }
42: public void withdraw( double amount )
43: {
44: balance -= amount;
45: writeRecord( "Withdraw", amount );
46: }
47: public double getBalance()
48: {
49: return balance;
50: }
51: public void setStorage( StorageType storage )
52: {
53: this.storage = storage;
54: }
55: private boolean writeRecord( String operation_type, double amount )
56: {
57: if( storage == StorageType.ASCII_STORAGE )
58: {
59: return writeAsciiRecord( operation_type, amount );
60: }
61: else if ( storage == StorageType.XML_STORAGE )
62: {
63: return writeXMLRecord( operation_type, amount );
64: }
65: }
66: private boolean writeAsciiRecord( String operation_type, double amount )
67: {
68: if( print_writer == null )
69: {
70: try {
71: print_writer =
72: new PrintWriter(new FileWriter(name + ".txt"));
73: } catch (IOException ioe) {
74: ioe.printStackTrace();
75: return false;
76: }
77: }
78: Date date = new Date();
79: print_writer.write( date.toString() );
80: print_writer.write( " " );
81: print_writer.write( operation_type ) ;
82: print_writer.write( " " );
83: print_writer.print( amount ) ;
84: print_writer.println() ;
85: print_writer.flush();
86: return true;
87: }
88: private boolean writeXMLRecord( String operation_type, double amount )
89: {
90: // To be written
91: return false;
92: }
93: }

Then we have an Account subclass called SavingAccount:

1:  public class SavingAccount extends Account
2: {
3: public SavingAccount( String name )
4: {
5: super( name );
6: }
7: public void creditInterest()
8: {
9: // double interest =
10: // balance += interest;
11: }
12: }

The third class is the Bank class. This class provides basic administration operations as open and close for accounts.

1:  import java.util.*;
2: public class Bank
3: {
4: public enum AccountType { CURRENT, SAVING };
5: private Hashtable<String, Account> accounts =
6: new Hashtable<String, Account>();
7: private String name;
8: public Bank( String name )
9: {
10: this.name = name;
11: }
12: public Account openAccount( String name, AccountType type )
13: {
14: Account account = null;
15: if( type == AccountType.CURRENT )
16: account = new Account( name );
17: else if( type == AccountType.SAVING )
18: account = new SavingAccount( name );
19: accounts.put( name, account );
20: account.open();
21: return account;
22: }
23: public boolean closeAccount( Account account )
24: {
25: if( account instanceof SavingAccount )
26: {
27: SavingAccount saving_account = (SavingAccount) account;
28: saving_account.creditInterest();
29: }
30: return account.close();
31: }
32: }

Finally we have a basic Test class so that we can execute the program.

1:  public class Test
2: {
3: public static void main( String[] args )
4: {
5: Bank bank = new Bank( "bank" );
6: Account account = bank.openAccount( "my account", Bank.AccountType.CURRENT );
7: if( account == null )
8: return;
9: account.deposit( 100.0 );
10: account.withdraw( 5.0 );
11: double balance = account.getBalance();
12: bank.closeAccount( account );
13: Account saving_account = bank.openAccount( "my saving_account", Bank.AccountType.SAVING );
14: saving_account.deposit( 50.0 );
15: bank.closeAccount( saving_account );
16: }
17: }

In the next post we'll start improving the design by applying the Single Responsability Principle. But you can already have a look to the code and try to think what is going to be done for the first improvement step.