SPOILER ALERT: this is a long post with code examples.
This is my first take on the famous "Design a Library Management System". Feel free to provide feedback on what's wrong / how this can be improved.
Books have the following information:
There can be multiple copies of the same book (book items). Each book item has a unique barcode.
There can be 2 types of users:
Each user has a unique barcode and a name.
Also, we have the following limitations:
We have the following classes/interfaces:
A BookDetails class representing the details for a given book (id, title, author, etc). This is NOT a book item, just a class describing book details. Notice that it is immutable (once created, cannot be modified) so it can be used as a key for Maps:
public class BookDetails {
private String id;
private String authorName;
private LocalDateTime publicationDate;
public BookDetails(String id, String authorName, LocalDateTime publicationDate) {
this.id = id;
this.authorName = authorName;
this.publicationDate = publicationDate;
}
public String getId() {
return id;
}
public String getAuthorName() {
return authorName;
}
public LocalDateTime getPublicationDate() {
return publicationDate;
}
}A BookItem class representing a single instance of a given book. We use aggregation here to get the book details. In addition to the book details, we are also adding a barcode property that is unique to each BookItem:
public class BookItem {
public BookItem(String bookItemId, BookDetails bookDetails) {
this.bookItemId = bookItemId;
this.bookDetails = bookDetails;
}
private String bookItemId;
private BookDetails bookDetails;
public String getBookItemId() {
return bookItemId;
}
public BookDetails getBookDetails() {
return bookDetails;
}
}A UserType enum representing user types.
public enum UserType {
LIBRARIAN, MEMBER
}A User class representing the library users. Some solutions suggest creating multiple classes for different types of users (not sure what we are achieving by doing that):
public class User {
public User(String barcode, String name, UserType userType) {
this.barcode = barcode;
this.name = name;
this.userType = userType;
}
private String barcode;
private String name;
private UserType userType;
public String getBarcode() {
return barcode;
}
public UserType getUserType() {
return userType;
}
public String getName() {
return name;
}
}Now, let's look at the main class - the Library class that exposes all the necessary actions:
public class Library {
private User currentUser;
private UserRepository userRepository;
private BookCatalog bookCatalog;
private LendingService lendingService;
private void ensureLoggedInUser() {
Objects.requireNonNull(this.currentUser, "There should be a logged in user");
}
private void ensureLibrarianAccess() throws RestrictedAccessException {
ensureLoggedInUser();
if (currentUser.getUserType() != UserType.LIBRARIAN) {
throw new RestrictedAccessException();
}
}
public void loginUser(String barcode) {
User user = userRepository.getUserByBarCode(barcode);
Objects.requireNonNull(user, "Wrong barcode");
this.currentUser = user;
}
public void createUser(User newUser) throws RestrictedAccessException {
ensureLibrarianAccess();
userRepository.addUser(newUser);
}
public void removeUser(String barcode) throws RestrictedAccessException {
ensureLibrarianAccess();
User user = userRepository.getUserByBarCode(barcode);
Objects.requireNonNull(user, "No user found");
userRepository.removeUser(barcode);
}
public void addBookItem(BookItem bookItem) throws RestrictedAccessException {
ensureLibrarianAccess();
bookCatalog.addBookItem(bookItem);
}
public void removeBookItem(String bookItemID) throws RestrictedAccessException {
ensureLibrarianAccess();
bookCatalog.removeBookItem(bookItemID);
}
public List<BookDetails> searchByTitle(String title) {
ensureLoggedInUser();
return bookCatalog.searchByTitle(title);
}
public List<BookDetails> searchByAuthor(String name) {
ensureLoggedInUser();
return bookCatalog.searchByAuthor(name);
}
public List<BookDetails> searchByPublicationDate(LocalDateTime publicationDate) {
ensureLoggedInUser();
return bookCatalog.searchByPublicationDate(publicationDate);
}
public List<BookItem> getCheckoutBooks() {
ensureLoggedInUser();
return lendingService.getCheckedOutBooks(currentUser);
}
public List<User> getLendingUsers(String bookID) throws RestrictedAccessException {
ensureLibrarianAccess();
return lendingService.getLendingUsers(bookID);
}
public int getOverdueFines() {
ensureLoggedInUser();
return lendingService.getOverdueFineAmount(currentUser);
}
public BookItem checkout(String bookID) {
ensureLoggedInUser();
return lendingService.checkout(currentUser, bookID);
}
public boolean renew(BookItem item) {
ensureLoggedInUser();
return lendingService.renew(currentUser, item);
}
public boolean returnBookItem(BookItem item) {
ensureLoggedInUser();
return lendingService.returnBook(currentUser, item);
}
public Library(
UserRepository userRepository,
BookCatalog bookCatalog,
LendingService lendingService
) {
this.userRepository = userRepository;
this.bookCatalog = bookCatalog;
this.lendingService = lendingService;
}
}A couple of important notes about this class:
UserRepository (a repository handling user persistence), a BookCatalog (allowing us to search/add/remove books and book items) and a LendingService (responsible for actions like checkout, renew, return and find calculation). UserRepository , BookCatalog and LendingService are all interfaces exposing appropriate actions (they will be discussed later). This makes unit testing easier and results in a loosely coupled code. The Library class uses these services to perform all the necessary actions. This seems to be a neat way to organize the responsibilities (if you feel that this can be further improved, please, let me know).currentUser field). Also, we have a couple of access check related methods (ensureLoggedInUser, ensureLibrarianAccess) that are throwing exceptions if certain invariants are violated. As a potential improvement, we could create an AccessCheckService that does this.Now, let's look at the service interfaces:
Fairly simple user persistence abstraction. I haven't provided a concrete implementation, can be done by having a Map from barcode to user instance:
public interface UserRepository {
User getUserByBarCode(String barCode);
void addUser(User user);
void removeUser(String barCode);
}The BookCatalog interface - represents a book catalog. For brevity, I have not have provided an implementation for this either but its easy to code a Map based implementation:
public interface BookCatalog {
void addBookItem(BookItem bookItem);
void removeBookItem(String bookItemID);
List<BookDetails> searchByTitle(String title);
List<BookDetails> searchByAuthor(String name);
List<BookDetails> searchByPublicationDate(LocalDateTime publicationDate);
}A LendingService interface - performs the lending related tasks:
public interface LendingService {
List<BookItem> getCheckedOutBooks(User user);
List<User> getLendingUsers(String bookID);
int getOverdueFineAmount(User user);
BookItem checkout(User user, String bookID);
boolean renew(User user, BookItem bookItem);
boolean returnBook(User user, BookItem bookItem);
}This is my first time solving this kind of problems. Any feedback is much appreciated.