Wednesday, February 9, 2011

Java Coding Best Practices: Better Search Implementation

In web applications searching for information based on the selected criteria and displaying the results is a very common requirement.
Suppose we need to search users based on their name. The end user will enter the username in the textbox and hit the search button and the user results will be fetched from database and display in a grid.
In the first look it looks simple and we start to implement it as follows:

public class UserSearchAction extends Action
{
 public ActionForward execute(...)
 {
  SearchForm sf = (SearchForm)form;
  String searchName = sf.getSearchName();
  UserService userService = new UserService();
  List<User> searchResults = userService.search(searchName);
  //put search results in request and dsplay in JSP
 }

}
public class UserService
{
 public List<User> search(String username)
 {
  // query the DB and get the results by applying filter on USERNAME column
  List<User> users = UserDAO.search(username);
 }
}
The above implementation works fine for the current requirement.

Later client wants to display only 10 rows per page and display a message like "Displaying 1-10 of 35 Users".

Now we need to change the above code for the change request.
public class UserSearchAction extends Action
{
 public ActionForward execute(...)
 {
  SearchForm sf = (SearchForm)form;
  String searchName = sf.getSearchName();
  UserService userService = new UserService();
  Map<String, Object> searchResultsMap = userService.search(searchName, start, pageSize);
  List<User> users = (List<User>)searchResultsMap.get("DATA");
  Integer count = (Integer)searchResultsMap.get("COUNT");
  //put search results in request and dsplay in JSP
 }

}
public class UserService
{
 public Map<String, Object> search(String username, int start, int pageSize)
 {
  //Get the total number of results for this criteria
  int count = UserDAO.searchResultsCount(username);
  // query the DB and get the start to start+pageSize results by applying filter on USERNAME column
  List<User> users = UserDAO.search(username, start, pageSize);
  Map<String, Object> RESULTS_MAP = new HashMap<String, Object>();
  RESULTS_MAP.put("DATA",users);
  RESULTS_MAP.put("COUNT",count);
  return RESULTS_MAP;
 }
}
Later Client again wants to give an option to the end user to choose the search type either by UserID or by Username and show the paginated results.
Now we need to change the above code for the change request.
public class UserSearchAction extends Action
{
 public ActionForward execute(...)
 {
  SearchForm sf = (SearchForm)form;
  String searchName = sf.getSearchName();
  String searchId = sf.getSearchId();
  UserService userService = new UserService();
  Map<String, Object> searchCriteriaMap = new HashMap<String, Object>();
  //searchCriteriaMap.put("SEARCH_BY","NAME");
  searchCriteriaMap.put("SEARCH_BY","ID");
  searchCriteriaMap.put("ID",searchId);
  searchCriteriaMap.put("START",start);
  searchCriteriaMap.put("PAGESIZE",pageSize);
    
  Map<String, Object> searchResultsMap = userService.search(searchCriteriaMap);
  List<User> users = (List<User>)searchResultsMap.get("DATA");
  Integer count = (Integer)searchResultsMap.get("COUNT");
  //put search results in request and dsplay in JSP
 }

}
public class UserService
{
 public Map<String, Object> search(Map<String, Object> searchCriteriaMap)
 {
  return UserDAO.search(searchCriteriaMap);
 }
}
public class UserDAO
{
 public Map<String, Object> search(Map<String, Object> searchCriteriaMap)
 {
  String SEARCH_BY = (String)searchCriteriaMap.get("SEARCH_BY"); 
  int start = (Integer)searchCriteriaMap.get("START"); 
  int pageSize = (Integer)searchCriteriaMap.get("PAGESIZE");
  if("ID".equals(SEARCH_BY))
  {
   int id = (Integer)searchCriteriaMap.get("ID"); 
   //Get the total number of results for this criteria
   int count = UserDAO.searchResultsCount(id);
   // query the DB and get the start to start+pageSize results 
   //by applying filter on USER_ID column
   List<User> users = search(id, start, pageSize);
  
  }
  else
  {
   String username = (String)searchCriteriaMap.get("USERNAME"); 
   //Get the total number of results for this criteria
   int count = UserDAO.searchResultsCount(username);
   // query the DB and get the start to start+pageSize results 
   //by applying filter on USERNAME column
   List<User> users = search(username, start, pageSize);
  
  }
  Map<String, Object> RESULTS_MAP = new HashMap<String, Object>();
  RESULTS_MAP.put("DATA",users);
  RESULTS_MAP.put("COUNT",count);
  return RESULTS_MAP;
 }

}
Finally the code became a big mess and completely violating the object oriented principles. There are lot of problems with the above code.
1. For each change request the method signatures are changing
2. Code needs to be changed for each enhancement like adding more search criteria

We can design a better object model for this kind of search functionality which is Object Oriented and enhancable as follws.

A generic SearchCriteria which holds common search criteria like pagination, sorting details.
package com.sivalabs.javabp;
public abstract class SearchCriteria
{
 private boolean pagination = false;
 private int pageSize = 25;
 private String sortOrder = "ASC";
 
 public boolean isPagination()
 {
  return pagination;
 }
 public void setPagination(boolean pagination)
 {
  this.pagination = pagination;
 }
 public String getSortOrder()
 {
  return sortOrder;
 }
 public void setSortOrder(String sortOrder)
 {
  this.sortOrder = sortOrder;
 }
 public int getPageSize()
 {
  return pageSize;
 }
 public void setPageSize(int pageSize)
 {
  this.pageSize = pageSize;
 }
 
}

A generic SearchResults object which holds the actual results and other detials like total available results count, page wise results provider etc.

package com.sivalabs.javabp;

import java.util.ArrayList;
import java.util.List;

public abstract class SearchResults<T>
{
 private int totalResults = 0;
 private int pageSize = 25;
 private List<T> results = null;
 
 public int getPageSize()
 {
  return pageSize;
 }
 public void setPageSize(int pageSize)
 {
  this.pageSize = pageSize;
 } 
 public int getTotalResults()
 {
  return totalResults;
 }
 private void setTotalResults(int totalResults)
 {
  this.totalResults = totalResults;
 }
 
 public List<T> getResults()
 {
  return results;
 }
 public List<T> getResults(int page)
 {
  if(page <= 0 || page > this.getNumberOfPages())
  {
   throw new RuntimeException
   ("Page number is zero or there are no that many page results.");
  }
  List<T> subList = new ArrayList<T>();
  int start = (page -1)*this.getPageSize();
  int end = start + this.getPageSize();
  if(end > this.results.size())
  {
   end = this.results.size();
  }
  for (int i = start; i < end; i++)
  {
   subList.add(this.results.get(i));
  }
  return subList;
 }
 
 public int getNumberOfPages()
 {
  if(this.results == null || this.results.size() == 0)
  {
   return 0;
  }
  return (this.totalResults/this.pageSize)+(this.totalResults%this.pageSize > 0 ? 1: 0);
 }
 public void setResults(List<T> aRresults)
 {
  if(aRresults == null)
  {
   aRresults = new ArrayList<T>();
  }
  this.results = aRresults;
  this.setTotalResults(this.results.size());
 }
 
}
A SearchCriteria class specific to User Search.
package com.sivalabs.javabp;

public class UserSearchCriteria extends SearchCriteria
{
 public enum UserSearchType 
 {
  BY_ID, BY_NAME
 };
 
 private UserSearchType searchType = UserSearchType.BY_NAME;
 private int id;
 private String username;
  
 public UserSearchType getSearchType()
 {
  return searchType;
 }
 public void setSearchType(UserSearchType searchType)
 {
  this.searchType = searchType;
 }
 
 public int getId()
 {
  return id;
 }
 public void setId(int id)
 {
  this.id = id;
 }
 public String getUsername()
 {
  return username;
 }
 public void setUsername(String username)
 {
  this.username = username;
 }
}
A SearchResults class specific to User Search.
package com.sivalabs.javabp;
import java.text.MessageFormat;

public class UserSearchResults<T> extends SearchResults
{
 public static String getDataGridMessage(int start, int end, int total)
 {
  return MessageFormat.format("Displaying {0} to {1} Users of {2}", start, end, total);
 }
 
}
UserService takes the SearchCriteria, invokes the DAO and get the results, prepares the UserSearchResults and return it back.
package com.sivalabs.javabp;

import java.util.ArrayList;
import java.util.List;

import com.sivalabs.javabp.UserSearchCriteria.UserSearchType;
public class UserService
{
 public SearchResults search(UserSearchCriteria searchCriteria)
 {
  UserSearchType searchType = searchCriteria.getSearchType();
  String sortOrder = searchCriteria.getSortOrder();
  System.out.println(searchType+":"+sortOrder);
  List<User> results = null;
  if(searchType == UserSearchType.BY_NAME)
  {
  //Use hibernate Criteria API to get and sort results 
  //based on USERNAME field in sortOrder
   results = userDAO.searchUsers(...); 
  }
  else if(searchType == UserSearchType.BY_ID)
  {
  //Use hibernate Criteria API to get and sort results 
  //based on USER_ID field in sortOrder
   results = userDAO.searchUsers(...);
  }
  
  UserSearchResults searchResults = new UserSearchResults();
  searchResults.setPageSize(searchCriteria.getPageSize());
  searchResults.setResults(results);
  return searchResults;
 }
 
}
package com.sivalabs.javabp;
import com.sivalabs.javabp.UserSearchCriteria.UserSearchType;

public class TestClient
{
 public static void main(String[] args)
 {
  UserSearchCriteria criteria = new UserSearchCriteria();
  criteria.setPageSize(3);
  //criteria.setSearchType(UserSearchType.BY_ID);
  //criteria.setId(2);
  
  criteria.setSearchType(UserSearchType.BY_NAME);
  criteria.setUsername("s");  
  
  UserService userService = new UserService();
  SearchResults searchResults = userService.search(criteria);
  
  System.out.println(searchResults.getTotalResults());
  System.out.println(searchResults.getResults().size()+":"+searchResults.getResults());
  System.out.println(searchResults.getResults(1).size()+":"+searchResults.getResults(1));
 }
}

With this approach if we want to add a new criteria like search by EMAIL we can do it as follows:
1. Add BY_EMAIL criteria type to UserSearchType enum
2. Add new property "email" to UserSearchCriteria
3. criteria.setSearchType(UserSearchType.BY_EMAIL);
criteria.setEmail("gmail");
4. In UserService prepare the HibernateCriteria with email filter.

Thats it :-)

1 comment:

  1. Nice article. Code review is single best development practice I recommend since code spent 90% time in maintenance and only 10% in initial development , a code which has comments and readable is easy to maintain and understand and saves a lot of time to understand and less error prone while making any changes but at the same time there should be guidelines for maintainers also because I have seen code quality getting degraded with every version.

    Thanks
    How to use ArrayList in Java 1.5 with generics

    ReplyDelete