Last tutorial, we discussed one type of relationship between entities: one to many. We added the product management flow and discovered that some code was repetitive. In this tutorial, we will focus on improving our code to make scaling easier and to get rid of redundant code. We will use some important Java concepts: Generics, Interfaces and Inheritance.
Usually, when creating a real-world application, we need to handle many entities. The process of adding an entity to an existing layered-application should be straightforward. Our application should scale efficiently without requiring any boiler-plate code (redundant code).
At this point, we can analyze our existing application and identify ways to improve our classes in order to remove the repeated parts. We will apply abstraction principles of OOP using inheritance, interfaces and generic classes.
Inheritance is a mechanism where one class (child or subclass) inherits the properties and behaviors (fields and methods) of another class (parent or superclass). This promotes code reusability and establishes an “is-a” relationship. We will apply this concept to the classes in our Domain.
An interface is a blueprint of a class that contains abstract methods (methods without a body) and constants. It defines a contract that a class must follow, specifying the methods a class must implement without dictating how. A class implements an interface to provide the specific behaviors for those methods. We will create an interface for the CRUD operations in the repository layer.
Generic classes allow you to create classes that work with any data type while maintaining type safety. Instead of specifying a specific data type, generics use type parameters (e.g. T, E, K, etc.) that are defined when the class is instantiated. This helps avoid casting and ensures compile-time type checking. We will create Generic Repositories in our application.
We will include examples of all these concepts, so don’t worry if some concepts are unclear at this stage.
In our existing implementation, both classes in our domain share a common field: id. We can use inheritance to eliminate the repeated code in each class. To achieve this, we will create a class called Entity, which contains only the id field. The updated class diagram is shown below:
We can now add a new class to the domain package:
public abstract class Entity {
protected int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
With inheritance, we can use the “super” constructor to pass the id attribute. Below is an example of how our Supplier and Product classes should look:
public class Supplier extends Entity {
private String name;
private SupplierType type;
private String contactEmail;
public Supplier() {
}
public Supplier(int id, String name, SupplierType type, String contactEmail) {
super.id = id;
this.name = name;
this.type = type;
this.contactEmail = contactEmail;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public SupplierType getType() {
return type;
}
public void setType(SupplierType type) {
this.type = type;
}
public String getContactEmail() {
return contactEmail;
}
public void setContactEmail(String contactEmail) {
this.contactEmail = contactEmail;
}
@Override
public String toString() {
return "Supplier{" +
"id=" + id +
", name='" + name + '\'' +
", type=" + type +
", contactEmail='" + contactEmail + '\'' +
'}';
}
}
public class Product extends Entity {
private String name;
private String description;
private int price;
private int quantity;
private Supplier supplier;
public Product(){
}
public Product(int id, String name, String description, int price, int quantity, Supplier supplier) {
super.id = id;
this.name = name;
this.description = description;
this.price = price;
this.quantity = quantity;
this.supplier = supplier;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
", description='" + description + '\'' +
", price=" + price +
", quantity=" + quantity +
'}';
}
public Supplier getSupplier() {
return supplier;
}
public void setSupplier(Supplier supplier) {
this.supplier = supplier;
}
}
In the future, if we add new classes, the Entity class will also be extended.
Now that the Domain looks better, we can improve the Repository layer.
We observe some common methods in our repository classes: save, delete, update, findAll, findById.
For these methods we can create an interface and all our classes will implement this behavior. As mentioned earlier, instead of specifying a specific data type, generics use type parameters (e.g. T, E, K).
Since all our Domain classes inherit Entity, we can use the parent class in the interface.
The implementation of this interface would look like this in the repository package:
public interface RepositoryInterface {
T save(T entity) throws IDNotUniqueException, ValidationException;
T update(T entity);
void delete(int entityID);
Iterable findAll();
T findById(int entityId);
}
We also notice that our repositories have common code. Both use Hashtables for storage and share similar implementations.
This observation allows us to create a GenericRepository that uses Entity as the main entity.
This GenericRepository will implement our newly created Interface:
public class GenericRepository implements RepositoryInterface {
private Map entities;
public GenericRepository() {
entities = new HashMap<>();
}
@Override
public T save(T entity) throws IDNotUniqueException {
if (entities.containsKey(entity.getId())) {
throw new IDNotUniqueException("The id is not unique ");
}
entities.put(entity.getId(), entity);
return entity;
}
public T update(T entity) {
if (entities.containsKey(entity.getId())) {
entities.put(entity.getId(), entity);
}
return entity;
}
public void delete(int productId) {
entities.remove(productId);
}
public Iterable findAll() {
return entities.values();
}
public T findById(int productId) {
return entities.get(productId);
}
}
Finally, the FileRepositories can inherit methods from the GenericRepository:
public class ProductFileRepository extends GenericRepository {
private String filename;
public ProductFileRepository(String filename) throws IDNotUniqueException {
super();
this.filename = filename;
loadProductsFromFile();
}
...
}
The last adjustment is to use the interface in the Service constructor. This change will allow us to switch to a different storage type for an entity in the future.
public class SupplierService {
private RepositoryInterface supplierRepository;
private SupplierValidator supplierValidator;
public SupplierService(RepositoryInterface supplierRepository, SupplierValidator supplierValidator) {
this.supplierRepository = supplierRepository;
this.supplierValidator = supplierValidator;
}
...
}
The main class remains unchanged:
public class Main {
public static void main(String[] args) throws ValidationException, IDNotUniqueException {
SupplierFileRepository supplierRepository = new SupplierFileRepository("suppliers.csv");
SupplierValidator supplierValidator = new SupplierValidator();
SupplierService supplierService = new SupplierService(supplierRepository, supplierValidator);
ProductFileRepository productFileRepository = new ProductFileRepository("products.csv");
ProductValidator productValidator = new ProductValidator();
ProductService productService = new ProductService(productFileRepository, productValidator, supplierService);
UserInterface userInterface = new UserInterface(productService, supplierService);
MainTest mainTest = new MainTest();
mainTest.runAllTest();
userInterface.runMenu();
}
}
To verify that the changes did not affect existing functionalities, we need to run tests.
When we added the second entity, we observed opportunities to improve our application by abstracting things. As a result, the code is now cleaner and the application is more expandable if we want to add more entities. In the next tutorial, we will explore how to manage relationships between multiple entities.
Learn advanced concepts, work on real-world projects, and fast-track your journey to becoming a proficient Java developer. Start now and unlock your full potential in the world of Java programming!
Start now and unlock your full potential in the world of Java programming!
The place where you can start your Java journey.
© All Rights Reserved.