In fast development cycles, applying continuous integration, we develop mock implementations of interfaces that we need to get our system up and running as quickly as possible.
The mock implementation provides the functionality required by its interface in a minimalistic manner. The goal is to make it available to components that depend on that interface early on in the development process.
This way, the mock implementation can be replaced with a more final one at the same time as the rest of the application is developed. This development strategy speeds up time-to-market quite a bit.
Here, we will store the books in a Map, indexed by ISBN, for look-up on modify operations.
public class BookInventoryMockImpl implements BookInventory { public static final String DEFAULT_CATEGORY = "default"; private Map<String, MutableBook> booksByISBN = new HashMap<String, MutableBook>();
A factory method provides the caller with a new instance of a MutableBook
. This re-enforces the decoupling between the components the component that uses the Book
and MutableBook
does not need to know the class or classes that implement the interfaces.
public MutableBook createBook(String isbn) { return new MutableBookImpl(isbn); }
In order to have a listing of the categories (without having to go through the whole map every time), we'll keep a count of the books that are in a category and update that count as we store and remove books.
private Map<String, Integer> categories = new HashMap<String, Integer>(); public Set<String> getCategories() { return this.categories.keySet(); }
Before storing a book, we first check if its attributes are valid. In our case, the only requirement is that it has an ISBN set.
We also need to keep track of the category to which it belongs to, to update the categories
cache. This implementation will place books that don't have a set category into the default
category.
public String storeBook(MutableBook book) throws InvalidBookException { String isbn = book.getIsbn(); if (isbn == null) { throw new InvalidBookException("ISBN is not set"); } this.booksByISBN.put(isbn, book); String category = book.getCategory(); if (category == null) { category = DEFAULT_CATEGORY; } if (this.categories.containsKey(category)) { int count = this.categories.get(category); this.categories.put(category, count + 1); } else { BookInventory interface, implementingBookInventory interface, implementingbook, storingthis.categories.put(category, 1); } return isbn; }
Removing a stored book is the opposite operation. We remove it from the mapping and update the categories cache accordingly.
public void removeBook(String isbn) throws BookNotFoundException { Book book = this.booksByISBN.remove(isbn); if (book == null) { throw new BookNotFoundException(isbn); } String category = book.getCategory(); int count = this.categories.get(category); if (count == 1) { this.categories.remove(category); } else { this.categories.put(category, count - 1); } }
Since we've carefully designed our BookInventory
interface, separating operations that use a Book
from those that use a MutableBook
, we are safe to use the same implementation (MutableBookImpl) for either of them.
In our case, we don't pay any attention to concurrency concerns as they are beyond the scope of this book and therefore no data locking is implemented when an item is loaded for edit. When a file- or database-based implementation of this interface is written, consider adding a lock mechanism to prevent multiple parties from editing a book simultaneously.
public Book loadBook(String isbn) throws BookNotFoundException { return loadBookForEdit(isbn); } public MutableBook loadBookForEdit(String isbn) throws BookNotFoundException { MutableBook book = this.booksByISBN.get(isbn); if (book == null) { throw new BookNotFoundException(isbn); } return book; }
We're expecting the search functionality to be slow. Remember, this is a mock implementation. In a database-based implementation, the criteria can be used in turn to specify a filter and provide a significantly better performance.
public Set<String> searchBooks( Map<SearchCriteria, String> criteria) { LinkedList<Book> books = new LinkedList<Book>(); books.addAll(this.booksByISBN.values()); for (Map.Entry<SearchCriteria, String> criterion : criteria.entrySet()) { Iterator<Book> it = books.iterator(); while (it.hasNext()) { Book book = it.next(); switch (criterion.getKey()) { case AUTHOR_LIKE: if ( !checkStringMatch(book.getAuthor(), criterion.getValue())) { it.remove(); continue; } break;
The checkStringMatch
method will attempt to match the given string attribute to the given criterion value.
The same matching mechanism is applied for the ISBN, group, and title. The rating matching uses another set of methods listed further down.
case ISBN_LIKE: if (!checkStringMatch( book.getISBN(), criterion.getValue())) { it.remove(); continue; } break; case CATEGORY_LIKE: if (!checkStringMatch( book.getCategory(), criterion.getValue())) { it.remove(); continue; } break; case TITLE_LIKE: if (!checkStringMatch( book.getTitle(), criterion.getValue())) { it.remove(); continue; } break; case RATING_GT: if (!checkIntegerGreater( book.getRating(), criterion.getValue())) { it.remove(); continue; } break; case RATING_LT: if (!checkIntegerSmaller( book.getRating(), criterion.getValue())) { it.remove(); continue; } break; } } }
Next, gather the books that match, extract their ISBNs, and return the result to the caller.
// copy ISBNs HashSet<String> isbns = new HashSet<String>(); for (Book book : books) { isbns.add(book.getISBN()); } return isbns; }
In a typical implementation, returning the references to items as a result of a search improves performance, especially when the results returned are many. It saves the time and resources required to load and transmit the results.
The method checkIntegerGreater
for checking the rating criterion match is as follows:
private boolean checkIntegerGreater(int attr, String critVal) { int critValInt; try { critValInt = Integer.parseInt(critVal); } catch (NumberFormatException e) { return false; } if (attr >= critValInt) { return true; } return false; }
The method checkIntegerSmaller
is similar to the previous one, the difference being the presence of the compare operator (not listed).
The last missing method is the checkStringMatch
method for comparing strings having a wildcard:
private boolean checkStringMatch(String attr, String critVal) { if (attr == null) { return false; } attr = attr.toLowerCase(); critVal = critVal.toLowerCase(); boolean startsWith = critVal.startsWith("%"); boolean endsWith = critVal.endsWith("%"); if (startsWith && endsWith) { if (critVal.length()==1) { return true; } else { return attr.contains( critVal.substring(1, critVal.length() - 1)); } BookInventory interface, implementingBookInventory interface, implementingbook search, implementing} else if (startsWith) { return attr.endsWith(critVal.substring(1)); } else if (endsWith) { return attr.startsWith( critVal.substring(0, critVal.length() - 1)); } else { return attr.equals(critVal); } } }
Even though we're done with the implementation of the service, we still need to hook it into the Felix framework and make it available as a service.