Love life, love challenge, make difference, change the world-by Ricky Li

Archive for 4月 28, 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.

At first this looks simple and we can start to implement it as follows:

01.public class UserSearchAction extends Action
02.{
03.public ActionForward execute(...)
04.{
05.SearchForm sf = (SearchForm)form;
06.String searchName = sf.getSearchName();
07.UserService userService = new UserService();
08.List<User> searchResults = userService.search(searchName);
09.//put search results in request and dsplay in JSP
10.}
11.
12.}
01.public class UserService
02.{
03.public List<User> search(String username)
04.{
05.// query the DB and get the results by applying filter on USERNAME column
06.List<User> users = UserDAO.search(username);
07.
08.}
09.}

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 the code need to be changed for the change request.

01.public class UserSearchAction extends Action
02.{
03.public ActionForward execute(...)
04.{
05.SearchForm sf = (SearchForm)form;
06.String searchName = sf.getSearchName();
07.UserService userService = new UserService();
08.Map<String, Object> searchResultsMap = userService.search(searchName, start, pageSize);
09.List<User> users = (List<User>)searchResultsMap.get("DATA");
10.Integer count = (Integer)searchResultsMap.get("COUNT");
11.//put search results in request and dsplay in JSP
12.}
13.
14.}
01.public class UserService
02.{
03.public Map<String, Object> search(String username, int start, int pageSize)
04.{
05.//Get the total number of results for this criteria       int count = UserDAO.searchResultsCount(username);
06.List<User> users = UserDAO.search(username, start, pageSize);
07.// query the DB and get the start to start+pageSize results by applying filter on USERNAME column
08.Map<String, Object> RESULTS_MAP = new HashMap<String, Object>();
09.RESULTS_MAP.put("DATA",users);
10.RESULTS_MAP.put("COUNT",count);
11.return RESULTS_MAP;
12.}
13.}

Later the client 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 again the code needs to be changed for the change request.

01.public class UserSearchAction extends Action
02.{
03.public ActionForward execute(...)
04.{
05.SearchForm sf = (SearchForm)form;
06.String searchName = sf.getSearchName();
07.String searchId = sf.getSearchId();
08.UserService userService = new UserService();
09.Map<String, Object> searchCriteriaMap = new HashMap<String, Object>();
10.//searchCriteriaMap.put("SEARCH_BY","NAME");
11.searchCriteriaMap.put("SEARCH_BY","ID");
12.searchCriteriaMap.put("ID",searchId);
13.searchCriteriaMap.put("START",start);
14.searchCriteriaMap.put("PAGESIZE",pageSize);
15.
16.Map<String, Object> searchResultsMap = userService.search(searchCriteriaMap);
17.List<User> users = (List<User>)searchResultsMap.get("DATA");
18.Integer count = (Integer)searchResultsMap.get("COUNT");
19.//put search results in request and dsplay in JSP
20.}
21.
22.}
1.public class UserService
2.{
3.public Map<String, Object> search(Map<String, Object> searchCriteriaMap)
4.{
5.return UserDAO.search(searchCriteriaMap);
6.}
7.}
01.public class UserDAO
02.{
03.public Map<String, Object> search(Map<String, Object> searchCriteriaMap)
04.{
05.String SEARCH_BY = (String)searchCriteriaMap.get("SEARCH_BY");
06.int start = (Integer)searchCriteriaMap.get("START");
07.int pageSize = (Integer)searchCriteriaMap.get("PAGESIZE");
08.if("ID".equals(SEARCH_BY))
09.{
10.int id = (Integer)searchCriteriaMap.get("ID");
11.//Get the total number of results for this criteria
12.int count = UserDAO.searchResultsCount(id);
13.// query the DB and get the start to start+pageSize results by applying filter on USER_ID column
14.List<User> users = search(id, start, pageSize);
15.
16.}
17.else
18.{
19.String username = (String)searchCriteriaMap.get("USERNAME");
20.//Get the total number of results for this criteria
21.int count = UserDAO.searchResultsCount(username);
22.// query the DB and get the start to start+pageSize results by applying filter on USERNAME column
23.List<User> users = search(username, start, pageSize);
24.
25.}
26.Map<String, Object> RESULTS_MAP = new HashMap<String, Object>();
27.RESULTS_MAP.put("DATA",users);
28.RESULTS_MAP.put("COUNT",count);
29.return RESULTS_MAP;
30.}
31.
32.}

Finally the code becomes a big mess and completely violates 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 such as adding more search criteria

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

A generic SearchCriteria which holds common search criteria like pagination, sorting details.

01.package com.sivalabs.javabp;
02.public abstract class SearchCriteria
03.{
04.private boolean pagination = false;
05.private int pageSize = 25;
06.private String sortOrder = "ASC";
07.
08.public boolean isPagination()
09.{
10.return pagination;
11.}
12.public void setPagination(boolean pagination)
13.{
14.this.pagination = pagination;
15.}
16.public String getSortOrder()
17.{
18.return sortOrder;
19.}
20.public void setSortOrder(String sortOrder)
21.{
22.this.sortOrder = sortOrder;
23.}
24.public int getPageSize()
25.{
26.return pageSize;
27.}
28.public void setPageSize(int pageSize)
29.{
30.this.pageSize = pageSize;
31.}
32.
33.}

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

01.package com.sivalabs.javabp;
02.
03.import java.util.ArrayList;
04.import java.util.List;
05.
06.public abstract class SearchResults<T>
07.{
08.private int totalResults = 0;
09.private int pageSize = 25;
10.private List<T> results = null;
11.
12.public int getPageSize()
13.{
14.return pageSize;
15.}
16.public void setPageSize(int pageSize)
17.{
18.this.pageSize = pageSize;
19.}   
20.public int getTotalResults()
21.{
22.return totalResults;
23.}
24.private void setTotalResults(int totalResults)
25.{
26.this.totalResults = totalResults;
27.}
28.
29.public List<T> getResults()
30.{
31.return results;
32.}
33.public List<T> getResults(int page)
34.{
35.if(page <= 0 || page > this.getNumberOfPages())
36.{
37.throw new RuntimeException("Page number is zero or there are no that many page results.");
38.}
39.List<T> subList = new ArrayList<T>();
40.int start = (page -1)*this.getPageSize();
41.int end = start + this.getPageSize();
42.if(end > this.results.size())
43.{
44.end = this.results.size();
45.}
46.for (int i = start; i < end; i++)
47.{
48.subList.add(this.results.get(i));
49.}
50.return subList;
51.}
52.
53.public int getNumberOfPages()
54.{
55.if(this.results == null || this.results.size() == 0)
56.{
57.return 0;
58.}
59.return (this.totalResults/this.pageSize)+(this.totalResults%this.pageSize > 0 ?10);
60.}
61.public void setResults(List<T> aRresults)
62.{
63.if(aRresults == null)
64.{
65.aRresults = new ArrayList<T>();
66.}
67.this.results = aRresults;
68.this.setTotalResults(this.results.size());
69.}
70.
71.}

A SearchCriteria class specific to User Search.

01.package com.sivalabs.javabp;
02.
03.public class UserSearchCriteria extends SearchCriteria
04.{
05.public enum UserSearchType
06.{
07.BY_ID, BY_NAME
08.};
09.
10.private UserSearchType searchType = UserSearchType.BY_NAME;
11.private int id;
12.private String username;
13.
14.public UserSearchType getSearchType()
15.{
16.return searchType;
17.}
18.public void setSearchType(UserSearchType searchType)
19.{
20.this.searchType = searchType;
21.}
22.
23.public int getId()
24.{
25.return id;
26.}
27.public void setId(int id)
28.{
29.this.id = id;
30.}
31.public String getUsername()
32.{
33.return username;
34.}
35.public void setUsername(String username)
36.{
37.this.username = username;
38.}
39.}

A SearchResults class specific to User Search.

01.package com.sivalabs.javabp;
02.import java.text.MessageFormat;
03.
04.public class UserSearchResults<T> extends SearchResults<User>
05.{
06.public static String getDataGridMessage(int start, int end, int total)
07.{
08.return MessageFormat.format("Displaying {0} to {1} Users of {2}", start, end, total);
09.}
10.
11.}

UserService takes the SearchCriteria, invokes the DAO and get the results, prepares the UserSearchResults and return it back.

01.package com.sivalabs.javabp;
02.
03.import java.util.ArrayList;
04.import java.util.List;
05.
06.import com.sivalabs.javabp.UserSearchCriteria.UserSearchType;
07.public class UserService
08.{
09.public SearchResults<User> search(UserSearchCriteria searchCriteria)
10.{
11.UserSearchType searchType = searchCriteria.getSearchType();
12.String sortOrder = searchCriteria.getSortOrder();
13.System.out.println(searchType+":"+sortOrder);
14.List<User> results = null;
15.if(searchType == UserSearchType.BY_NAME)
16.{
17.//Use hibernate Criteria API to get and sort results based on USERNAME field in sortOrder
18.results = userDAO.searchUsers(...);   
19.}
20.else if(searchType == UserSearchType.BY_ID)
21.{
22.//Use hibernate Criteria API to get and sort results based on USER_ID field in sortOrder
23.results = userDAO.searchUsers(...);
24.}
25.
26.UserSearchResults<User> searchResults = new UserSearchResults<User>();
27.searchResults.setPageSize(searchCriteria.getPageSize());
28.searchResults.setResults(results);
29.return searchResults;
30.}
31.
32.}
01.package com.sivalabs.javabp;
02.import com.sivalabs.javabp.UserSearchCriteria.UserSearchType;
03.
04.public class TestClient
05.{
06.public static void main(String[] args)
07.{
08.UserSearchCriteria criteria = new UserSearchCriteria();
09.criteria.setPageSize(3);
10.//criteria.setSearchType(UserSearchType.BY_ID);
11.//criteria.setId(2);
12.
13.criteria.setSearchType(UserSearchType.BY_NAME);
14.criteria.setUsername("s");       
15.
16.UserService userService = new UserService();
17.SearchResults<User> searchResults = userService.search(criteria);
18.
19.System.out.println(searchResults.getTotalResults());
20.System.out.println(searchResults.getResults().size()+":"+searchResults.getResults());
21.System.out.println(searchResults.getResults(1).size()+":"+searchResults.getResults(1));
22.}
23.}

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 🙂

From : http://sivalabs.blogspot.com/2011/02/java-coding-best-practices-better.ht