Refactoring to Strategy. My example. Part 2
So… It’s time to finish refactoring the example. In part 1 I have prepared a class for extracting strategies. Let’s look at our code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | public class ConnectionManager { private static Logger logger = Logger.getLogger(ConnectionManager.class); public static final String CONTEXT_PROPERTIES_DEFAULT = "context"; public static final String COMMON_PROPERTIES_DEFAULT = "common"; private static final String DS_JNDI_NAME = "connection.jndi_name"; private DataSource dataSource = null; private Context context = null; private boolean isDataSourceUsed = false; private String url = null; private String password = null; private String login = null; private String commonProperties; private String contextProperties; private boolean autoChoose; private static final String CONNECTION_AUTO_CHOOSE = "connection.auto_choose"; private static final String CONNECTION_USE_DATASOURCE = "connection.use_datasource"; private static final String CONNECTION_DRIVER_CLASS_NAME = "connection.driver_class_name"; private static final String CONNECTION_DATABASE_URL = "connection.database_url"; private static final String CONNECTION_DATABASE_USER = "connection.database_user"; private static final String CONNECTION_DATABASE_PASSWORD = "connection.database_password"; private ResourceBundle commonRes; private ResourceBundle contextRes; public ConnectionManager() { this(COMMON_PROPERTIES_DEFAULT, CONTEXT_PROPERTIES_DEFAULT); } public ConnectionManager(String commonProperties, String contextProperties) { this.commonProperties = commonProperties; this.contextProperties = contextProperties; this.commonRes = ResourceBundle.getBundle(commonProperties); this.contextRes = ResourceBundle.getBundle(contextProperties); autoChoose = new Boolean(commonRes.getString(CONNECTION_AUTO_CHOOSE)).booleanValue(); isDataSourceUsed = new Boolean(commonRes.getString(CONNECTION_USE_DATASOURCE)).booleanValue(); initDirectConnectionParams(); if (isDataSourceUsed) { try { initDataSource(); } catch (NamingException e) { throw new RuntimeException(e); } } } private void initDataSource() throws NamingException { // get JNDI name from properies String strJNDI = commonRes.getString(DS_JNDI_NAME); if (context == null) { context = getRootContext(); } dataSource = (DataSource) context.lookup(strJNDI); } private Context getRootContext() throws NamingException { // create property for initial context Properties properties = new Properties(); Enumeration keys = contextRes.getKeys(); if (keys != null) { while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); String value = contextRes.getString(key); // add property properties.put(key, value); } } context = new InitialContext(properties); return context; } public Connection getConnection() throws Exception { Connection conn = null; try { if (isDataSourceUsed) { try { conn = getDataSourceConnection(); } catch (Exception ex) { logger.error("while getting datasource", ex); } if (autoChoose && conn == null) { logger.info("Can't find a datasource. Using direct connection"); conn = getConnectionFromURL(); } } else { conn = getConnectionFromURL(); } } catch (Exception ex) { logger.error(ex, ex); throw ex; } return conn; } private Connection getDataSourceConnection() throws SQLException, NamingException { return dataSource.getConnection(); } private void initDirectConnectionParams() { try { Class.forName(commonRes.getString(CONNECTION_DRIVER_CLASS_NAME)); url = commonRes.getString(CONNECTION_DATABASE_URL); login = commonRes.getString(CONNECTION_DATABASE_USER); password = commonRes.getString(CONNECTION_DATABASE_PASSWORD); } catch (Exception ex) { logger.error("exception while getting datasource", ex); } } private Connection getConnectionFromURL() throws SQLException { Connection conn = DriverManager.getConnection(url, login, password); return conn; } } |
We have two different ways to connect to the database. The first one is a direct connection, the second – through data source. Our logic is implemented using the conditional statements. I want to replace them with polymorphism. You could find information about this refactoring method here.
My first step will be moving code for direct connection to a separate class. Now I’m running ahead to simplify my task: usually I should create Connector interface later but I hope it’s quite clear for you:
1 2 3 | interface Connector { Connection getConnection() throws Exception; } |
I will create class DirectConnector, which implements Connector interface. I will move related methods (initDirectConnectionParams, getConnectionFromURL) from ConnectionManager to it. Thus,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | class DirectConnector implements Connector { private static Logger logger = Logger.getLogger(DirectConnector.class); private ResourceBundle commonRes; private String url; private String login; private String password; public DirectConnector(ResourceBundle commonRes) { this.commonRes = commonRes; initDirectConnectionParams(); } private void initDirectConnectionParams() { try { Class.forName(commonRes.getString(ConnectionManager.CONNECTION_DRIVER_CLASS_NAME)); url = commonRes.getString(ConnectionManager.CONNECTION_DATABASE_URL); login = commonRes.getString(ConnectionManager.CONNECTION_DATABASE_USER); password = commonRes.getString(ConnectionManager.CONNECTION_DATABASE_PASSWORD); } catch (Exception ex) { logger.error("exception while getting datasource", ex); } } private Connection getConnectionFromURL() throws SQLException { Connection conn = DriverManager.getConnection(url, login, password); return conn; } public Connection getConnection() throws Exception { return getConnectionFromURL(); } } |
I hope all is clear for you. Now I should rewrite ConnectionManager code using this class. We have the following modifications:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public class ConnectionManager { //... private DirectConnector directConnector; //.. public ConnectionManager(String commonProperties, String contextProperties) { //... directConnector = new DirectConnector(commonRes); //... } public Connection getConnection() throws Exception { Connection conn = null; try { if (isDataSourceUsed) { try { conn = getDataSourceConnection(); } catch (Exception ex) { logger.error("while getting datasource", ex); } if (autoChoose && conn == null) { logger.info("Can't find a datasource. Using direct connection"); conn = directConnector.getConnection(); } } else { conn = directConnector.getConnection(); } } catch (Exception ex) { logger.error(ex, ex); throw ex; } return conn; } //.. } |
Second step is very easy. I will use the same approach to modify datasource connection code. So,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | class DataSourceConnector implements Connector { private static Logger logger = Logger.getLogger(DataSourceConnector.class); private ResourceBundle contextRes; private Context context; private ResourceBundle commonRes; private DataSource dataSource; public DataSourceConnector(ResourceBundle commonRes, ResourceBundle contextRes) { this.contextRes = contextRes; this.commonRes = commonRes; try { initDataSource(); } catch (NamingException e) { logger.error(e, e); } } private void initDataSource() throws NamingException { String strJNDI = commonRes.getString(ConnectionManager.DS_JNDI_NAME); if (context == null) { context = getRootContext(); } dataSource = (DataSource) context.lookup(strJNDI); } private Context getRootContext() throws NamingException { Properties properties = new Properties(); Enumeration keys = contextRes.getKeys(); if (keys != null) { while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); String value = contextRes.getString(key); properties.put(key, value); } } context = new InitialContext(properties); return context; } private Connection getDataSourceConnection() throws SQLException, NamingException { return dataSource.getConnection(); } public Connection getConnection() throws Exception { return getDataSourceConnection(); } } |
And now we have following code in the ConnectionManager:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | public class ConnectionManager { private static Logger logger = Logger.getLogger(ConnectionManager.class); public static final String CONTEXT_PROPERTIES_DEFAULT = "context"; public static final String COMMON_PROPERTIES_DEFAULT = "common"; public static final String CONNECTION_AUTO_CHOOSE = "connection.auto_choose"; public static final String CONNECTION_USE_DATASOURCE = "connection.use_datasource"; public static final String CONNECTION_DRIVER_CLASS_NAME = "connection.driver_class_name"; public static final String CONNECTION_DATABASE_URL = "connection.database_url"; public static final String CONNECTION_DATABASE_USER = "connection.database_user"; public static final String CONNECTION_DATABASE_PASSWORD = "connection.database_password"; public static final String DS_JNDI_NAME = "connection.jndi_name"; private boolean isDataSourceUsed = false; private boolean autoChoose; private DirectConnector directConnector; private DataSourceConnector dataSourceConnector; public ConnectionManager() { this(COMMON_PROPERTIES_DEFAULT, CONTEXT_PROPERTIES_DEFAULT); } public ConnectionManager(String commonProperties, String contextProperties) { ResourceBundle commonRes = ResourceBundle.getBundle(commonProperties); ResourceBundle contextRes = ResourceBundle.getBundle(contextProperties); autoChoose = new Boolean(commonRes.getString(CONNECTION_AUTO_CHOOSE)).booleanValue(); isDataSourceUsed = new Boolean(commonRes.getString(CONNECTION_USE_DATASOURCE)).booleanValue(); directConnector = new DirectConnector(commonRes); if (isDataSourceUsed) { try { dataSourceConnector = new DataSourceConnector(commonRes, contextRes); } catch (Exception e) { throw new RuntimeException(e); } } } public Connection getConnection() throws Exception { Connection conn = null; try { if (isDataSourceUsed) { try { conn = dataSourceConnector.getConnection(); } catch (Exception ex) { logger.error("while getting datasource", ex); } if (autoChoose && conn == null) { logger.info("Can't find a datasource. Using direct connection"); conn = directConnector.getConnection(); } } else { conn = directConnector.getConnection(); } } catch (Exception ex) { logger.error(ex, ex); throw ex; } return conn; } } |
I want to make ConnectionManager.getConnection more simple, but a autoChoose variable prevents me doing so. I decided to create new strategy named MixedConnector. It is a composite which implements a logic of autoChoose.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class MixedConnector implements Connector { private static Logger logger = Logger.getLogger(MixedConnector.class); private DataSourceConnector dataSourceConnector; private DirectConnector directConnector; public MixedConnector(ResourceBundle commonRes, ResourceBundle contextRes) { this.dataSourceConnector = new DataSourceConnector(commonRes, contextRes); this.directConnector = new DirectConnector(commonRes); } public Connection getConnection() throws Exception { try { return dataSourceConnector.getConnection(); } catch (Exception e) { logger.warn("Can't find a datasource. Using direct connection", e); return directConnector.getConnection(); } } } |
Next step is the last. We just have one strategy for connecting to the database which initializes in the constructor and ConnectionManager.getConnection method which contains only one line. We have the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | public class ConnectionManager { public static final String CONTEXT_PROPERTIES_DEFAULT = "context"; public static final String COMMON_PROPERTIES_DEFAULT = "common"; public static final String CONNECTION_AUTO_CHOOSE = "connection.auto_choose"; public static final String CONNECTION_USE_DATASOURCE = "connection.use_datasource"; public static final String CONNECTION_DRIVER_CLASS_NAME = "connection.driver_class_name"; public static final String CONNECTION_DATABASE_URL = "connection.database_url"; public static final String CONNECTION_DATABASE_USER = "connection.database_user"; public static final String CONNECTION_DATABASE_PASSWORD = "connection.database_password"; public static final String DS_JNDI_NAME = "connection.jndi_name"; private Connector connector; public ConnectionManager() { this(COMMON_PROPERTIES_DEFAULT, CONTEXT_PROPERTIES_DEFAULT); } public ConnectionManager(String commonProperties, String contextProperties) { ResourceBundle commonRes = ResourceBundle.getBundle(commonProperties); ResourceBundle contextRes = ResourceBundle.getBundle(contextProperties); boolean autoChoose = getValue(commonRes, CONNECTION_AUTO_CHOOSE); boolean dataSourceUsed = getValue(commonRes, CONNECTION_USE_DATASOURCE); if (dataSourceUsed) { if (autoChoose) { connector = new MixedConnector(contextRes, contextRes); } else { connector = new DataSourceConnector(commonRes, contextRes); } } else { connector = new DirectConnector(commonRes); } } private boolean getValue(ResourceBundle commonRes, String key) { return Boolean.valueOf(commonRes.getString(key)).booleanValue(); } public Connection getConnection() throws Exception { return connector.getConnection(); } } |
As the result we have three different strategies (DirectConnector, DataSourceConnector and MixedConnector) for establishing connection to database. It’s easy to implement additional strategies or use mock object as connector’s stub. Compare this solution to original one: which one is more scalable? Try to add some another of a connection or write a Unit test for ConnectionManager.getConnection()… It seems that patterns are useful, aren’t they?
Responses
Why don’t you write that while refactoring you fixed a bug in the original code even not knowing it was there - it seems like another good point of using patterns and do refactoring.
You will fix many bugs when you refactor code. This refactoring is no exception.
As for me it is very strange to use some custom Connectors for obtaining database connection objects.
Since we already have standard DataSource interface. Moreover we don’t need write configuration routine in because it should placed in config files (Spring bean configs for non-container usage, and Application Server configs for container usages which are binded at startup to JNDI).
So I don’t see any problem…
Of course, you are right. I just have written about refactoring in this article. Nothing more. A title is “Refactoring to Strategy. My example”, not “Cool database connection framework”
Leave a Reply