feat: Implemented the Table Inheritance pattern (#3105)

* sample classes and tests

* sample classes and tests

* read me addition

* fix violations

* fix quality (coverage)

* fix quality (coverage) #2

* resolved comments
This commit is contained in:
HabibaMekay
2025-01-06 19:03:27 +02:00
committed by GitHub
parent eb7a0dff55
commit c5a6862887
10 changed files with 745 additions and 0 deletions
+1
View File
@@ -218,6 +218,7 @@
<module>function-composition</module>
<module>microservices-distributed-tracing</module>
<module>microservices-idempotent-consumer</module>
<module>table-inheritance</module>
</modules>
<repositories>
<repository>
+201
View File
@@ -0,0 +1,201 @@
---
title: "Table Inheritance Pattern in Java: Modeling Hierarchical Data in Relational Databases"
shortTitle: Table Inheritance
description: "Explore the Table Inheritance pattern in Java with real-world examples, database schema, and tutorials. Learn how to model class hierarchies elegantly in relational databases."
category: Data Access Pattern, Structural Pattern
language: en
tag:
- Decoupling
- Inheritance
- Polymorphism
- Object Mapping
- Persistence
- Data Transformation
---
## Also Known As
- Class Table Inheritance
---
## Intent of Table Inheritance Pattern
The Table Inheritance pattern models a class hierarchy in a relational database by creating
separate tables for each class in the hierarchy. These tables share a common primary key, which in
subclass tables also serves as a foreign key referencing the primary key of the base class table.
This linkage maintains relationships and effectively represents the inheritance structure. This pattern
enables the organization of complex data models, particularly when subclasses have unique properties
that must be stored in distinct tables.
---
## Detailed Explanation of Table Inheritance Pattern with Real-World Examples
### Real-World Example
Consider a **Vehicle Management System** with a `Vehicle` superclass and subclasses like `Car` and `Truck`.
- The **Vehicle Table** stores attributes common to all vehicles, such as `make`, `model`, and `year`. Its primary key (`id`) uniquely identifies each vehicle.
- The **Car Table** and **Truck Table** store attributes specific to their respective types, such as `numberOfDoors` for cars and `payloadCapacity` for trucks.
- The `id` column in the **Car Table** and **Truck Table** serves as both the primary key for those tables and a foreign key referencing the `id` in the **Vehicle Table**.
This setup ensures each subclass entry corresponds to a base class entry, maintaining the inheritance relationship while keeping subclass-specific data in their own tables.
### In Plain Words
In table inheritance, each class in the hierarchy is represented by a separate table, which
allows for a clear distinction between shared attributes (stored in the base class table) and
specific attributes (stored in subclass tables).
### Martin Fowler Says
Relational databases don't support inheritance, which creates a mismatch when mapping objects.
To fix this, Table Inheritance uses a separate table for each class in the hierarchy while maintaining
relationships through foreign keys, making it easier to link the classes together in the database.
For more detailed information, refer to Martin Fowler's article on [Class Table Inheritance](https://martinfowler.com/eaaCatalog/classTableInheritance.html).
## Programmatic Example of Table Inheritance Pattern in Java
The `Vehicle` class will be the superclass, and we will have `Car` and `Truck` as subclasses that extend
`Vehicle`. The `Vehicle` class will store common attributes, while `Car` and `Truck` will store
attributes specific to those subclasses.
### Key Aspects of the Pattern:
1. **Superclass (`Vehicle`)**:
The `Vehicle` class stores attributes shared by all vehicle types, such as:
- `make`: The manufacturer of the vehicle.
- `model`: The model of the vehicle.
- `year`: The year the vehicle was manufactured.
- `id`: A unique identifier for the vehicle.
These attributes are stored in the **`Vehicle` table** in the database.
2. **Subclass (`Car` and `Truck`)**:
Each subclass (`Car` and `Truck`) stores attributes specific to that vehicle type:
- `Car`: Has an additional attribute `numberOfDoors` representing the number of doors the car has.
- `Truck`: Has an additional attribute `payloadCapacity` representing the payload capacity of the truck.
These subclass-specific attributes are stored in the **`Car` and `Truck` tables**.
3. **Foreign Key Relationship**:
Each subclass (`Car` and `Truck`) contains the `id` field which acts as a **foreign key** that
references the primary key (`id`) of the superclass (`Vehicle`). This foreign key ensures the
relationship between the common attributes in the `Vehicle` table and the specific attributes in the
subclass tables (`Car` and `Truck`).
```java
/**
* Superclass
* Represents a generic vehicle with basic attributes like make, model, year, and ID.
*/
public class Vehicle {
private String make;
private String model;
private int year;
private int id;
// Constructor, getters, and setters...
}
/**
* Represents a car, which is a subclass of Vehicle.
*/
public class Car extends Vehicle {
private int numberOfDoors;
// Constructor, getters, and setters...
}
/**
* Represents a truck, which is a subclass of Vehicle.
*/
public class Truck extends Vehicle {
private int payloadCapacity;
// Constructor, getters, and setters...
}
```
## Table Inheritance Pattern Class Diagram
<img src="etc/class-diagram.png" width="400" height="500" />
## Table Inheritance Pattern Database Schema
### Vehicle Table
| Column | Description |
|--------|-------------------------------------|
| id | Primary key |
| make | The make of the vehicle |
| model | The model of the vehicle |
| year | The manufacturing year of the vehicle |
### Car Table
| Column | Description |
|------------------|-------------------------------------|
| id | Foreign key referencing `Vehicle(id)` |
| numberOfDoors | Number of doors in the car |
### Truck Table
| Column | Description |
|-------------------|-------------------------------------|
| id | Foreign key referencing `Vehicle(id)` |
| payloadCapacity | Payload capacity of the truck |
---
## When to Use the Table Inheritance Pattern in Java
- When your application requires a clear mapping of an object-oriented class hierarchy to relational tables.
- When subclasses have unique attributes that do not fit into a single base table.
- When scalability and normalization of data are important considerations.
- When you need to separate concerns and organize data in a way that each subclass has its own
table but maintains relationships with the superclass.
## Table Inheritance Pattern Java Tutorials
- [Software Patterns Lexicon: Class Table Inheritance](https://softwarepatternslexicon.com/patterns-sql/4/4/2/)
- [Martin Fowler: Class Table Inheritance](http://thierryroussel.free.fr/java/books/martinfowler/www.martinfowler.com/isa/classTableInheritance.html)
---
## Real-World Applications of Table Inheritance Pattern in Java
- **Vehicle Management System**: Used to store different types of vehicles like Car and Truck in separate tables but maintain a relationship through a common superclass `Vehicle`.
- **E-Commerce Platforms**: Where different product types, such as Clothing, Electronics, and Furniture, are stored in separate tables with shared attributes in a superclass `Product`.
## Benefits and Trade-offs of Table Inheritance Pattern
### Benefits
- **Clear Structure**: Each class has its own table, making the data model easier to maintain and understand.
- **Scalability**: Each subclass can be extended independently without affecting the other tables, making the system more scalable.
- **Data Normalization**: Helps avoid data redundancy and keeps the schema normalized.
### Trade-offs
- **Multiple Joins**: Retrieving data that spans multiple subclasses may require joining multiple tables, which could lead to performance issues.
- **Increased Complexity**: Managing relationships between tables and maintaining integrity can become more complex.
- **Potential for Sparse Tables**: Subclasses with fewer attributes may end up with tables that have many null fields.
## Related Java Design Patterns
- **Single Table Inheritance** A strategy where a single table is used to store all classes in an
inheritance hierarchy. It stores all attributes of the class and its subclasses in one table.
- **Singleton Pattern** Used when a class needs to have only one instance.
## References and Credits
- **Martin Fowler** - [*Patterns of Enterprise Application Architecture*](https://www.amazon.com/Patterns-Enterprise-Application-Architecture-Martin/dp/0321127420)
- **Java Persistence with Hibernate** - [Link to book](https://www.amazon.com/Java-Persistence-Hibernate-Christian-Bauer/dp/193239469X)
- **Object-Relational Mapping on Wikipedia** - [Link to article](https://en.wikipedia.org/wiki/Object-relational_mapping)
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.iluwatar</groupId>
<artifactId>java-design-patterns</artifactId>
<version>1.26.0-SNAPSHOT</version>
</parent>
<artifactId>table-inheritance</artifactId>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,65 @@
package com.iluwatar.table.inheritance;
import java.util.logging.Logger;
/**
* The main entry point of the application demonstrating the use of vehicles.
*
* <p>The Table Inheritance pattern models a class hierarchy in a relational database by creating
* separate tables for each class in the hierarchy. These tables share a common primary key, which in
* subclass tables also serves as a foreign key referencing the primary key of the base class table.
* This linkage maintains relationships and effectively represents the inheritance structure. This
* pattern enables the organization of complex data models, particularly when subclasses have unique
* properties that must be stored in distinct tables.
*/
public class App {
/**
* Manages the storage and retrieval of Vehicle objects, including Cars and Trucks.
*
* <p>This example demonstrates the **Table Inheritance** pattern, where each vehicle type
* (Car and Truck) is stored in its own separate table. The `VehicleDatabase` simulates
* a simple database that manages these entities, with each subclass (Car and Truck)
* being stored in its respective table.
*
* <p>The `VehicleDatabase` contains the following tables:
* - `vehicleTable`: Stores all vehicle objects, including both `Car` and `Truck` objects.
* - `carTable`: Stores only `Car` objects, with fields specific to cars.
* - `truckTable`: Stores only `Truck` objects, with fields specific to trucks.
*
* <p>The example demonstrates:
* 1. Saving instances of `Car` and `Truck` to their respective tables in the database.
* 2. Retrieving vehicles (both cars and trucks) from the appropriate table based on their ID.
* 3. Printing all vehicles stored in the database.
* 4. Showing how to retrieve specific types of vehicles (`Car` or `Truck`) by their IDs.
*
* <p>In the **Table Inheritance** pattern, each subclass has its own table, making it easier
* to manage specific attributes of each subclass.
*
* @param args command-line arguments
*/
public static void main(String[] args) {
final Logger logger = Logger.getLogger(App.class.getName());
VehicleDatabase database = new VehicleDatabase();
Car car = new Car(2020, "Toyota", "Corolla", 4, 1);
Truck truck = new Truck(2018, "Ford", "F-150", 60, 2);
database.saveVehicle(car);
database.saveVehicle(truck);
database.printAllVehicles();
Vehicle vehicle = database.getVehicle(car.getId());
Car retrievedCar = database.getCar(car.getId());
Truck retrievedTruck = database.getTruck(truck.getId());
logger.info(String.format("Retrieved Vehicle: %s", vehicle));
logger.info(String.format("Retrieved Car: %s", retrievedCar));
logger.info(String.format("Retrieved Truck: %s", retrievedTruck));
}
}
@@ -0,0 +1,50 @@
package com.iluwatar.table.inheritance;
import lombok.Getter;
/**
* Represents a car with a specific number of doors.
*/
@Getter
public class Car extends Vehicle {
private int numDoors;
/**
* Constructs a Car object.
*
* @param year the manufacturing year
* @param make the make of the car
* @param model the model of the car
* @param numDoors the number of doors
* @param id the unique identifier for the car
*/
public Car(int year, String make, String model, int numDoors, int id) {
super(year, make, model, id);
if (numDoors <= 0) {
throw new IllegalArgumentException("Number of doors must be positive.");
}
this.numDoors = numDoors;
}
/**
* Sets the number of doors for the car.
*
* @param doors the number of doors
*/
public void setNumDoors(int doors) {
if (doors <= 0) {
throw new IllegalArgumentException("Number of doors must be positive.");
}
this.numDoors = doors;
}
@Override
public String toString() {
return "Car{"
+ "id=" + getId()
+ ", make='" + getMake() + '\''
+ ", model='" + getModel() + '\''
+ ", year=" + getYear()
+ ", numberOfDoors=" + getNumDoors()
+ '}';
}
}
@@ -0,0 +1,57 @@
package com.iluwatar.table.inheritance;
import lombok.Getter;
/**
* Represents a truck, a type of vehicle with a specific load capacity.
*/
@Getter
public class Truck extends Vehicle {
private double loadCapacity;
/**
* Constructs a Truck object with the given parameters.
*
* @param year the year of manufacture
* @param make the make of the truck
* @param model the model of the truck
* @param loadCapacity the load capacity of the truck
* @param id the unique ID of the truck
*/
public Truck(int year, String make, String model, double loadCapacity, int id) {
super(year, make, model, id);
if (loadCapacity <= 0) {
throw new IllegalArgumentException("Load capacity must be positive.");
}
this.loadCapacity = loadCapacity;
}
/**
* Sets the load capacity of the truck.
*
* @param capacity the new load capacity
*/
public void setLoadCapacity(double capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("Load capacity must be positive.");
}
this.loadCapacity = capacity;
}
/**
* Returns a string representation of the truck.
*
* @return a string with the truck's details
*/
@Override
public String toString() {
return "Truck{"
+ "id=" + getId()
+ ", make='" + getMake() + '\''
+ ", model='" + getModel() + '\''
+ ", year=" + getYear()
+ ", payloadCapacity=" + getLoadCapacity()
+ '}';
}
}
@@ -0,0 +1,48 @@
package com.iluwatar.table.inheritance;
import lombok.Getter;
import lombok.Setter;
/**
* Represents a generic vehicle with basic attributes like make, model, year, and ID.
*/
@Setter
@Getter
public class Vehicle {
private String make;
private String model;
private int year;
private int id;
/**
* Constructs a Vehicle object with the given parameters.
*
* @param year the year of manufacture
* @param make the make of the vehicle
* @param model the model of the vehicle
* @param id the unique ID of the vehicle
*/
public Vehicle(int year, String make, String model, int id) {
this.make = make;
this.model = model;
this.year = year;
this.id = id;
}
/**
* Returns a string representation of the vehicle.
*
* @return a string with the vehicle's details
*/
@Override
public String toString() {
return "Vehicle{"
+ "id=" + id
+ ", make='" + make + '\''
+ ", model='" + model + '\''
+ ", year=" + year
+ '}';
}
}
@@ -0,0 +1,73 @@
package com.iluwatar.table.inheritance;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
/**
* Manages the storage and retrieval of Vehicle objects, including Cars and Trucks.
*/
public class VehicleDatabase {
final Logger logger = Logger.getLogger(VehicleDatabase.class.getName());
private Map<Integer, Vehicle> vehicleTable = new HashMap<>();
private Map<Integer, Car> carTable = new HashMap<>();
private Map<Integer, Truck> truckTable = new HashMap<>();
/**
* Saves a vehicle to the database. If the vehicle is a Car or Truck, it is added to the respective table.
*
* @param vehicle the vehicle to save
*/
public void saveVehicle(Vehicle vehicle) {
vehicleTable.put(vehicle.getId(), vehicle);
if (vehicle instanceof Car) {
carTable.put(vehicle.getId(), (Car) vehicle);
} else if (vehicle instanceof Truck) {
truckTable.put(vehicle.getId(), (Truck) vehicle);
}
}
/**
* Retrieves a vehicle by its ID.
*
* @param id the ID of the vehicle
* @return the vehicle with the given ID, or null if not found
*/
public Vehicle getVehicle(int id) {
return vehicleTable.get(id);
}
/**
* Retrieves a car by its ID.
*
* @param id the ID of the car
* @return the car with the given ID, or null if not found
*/
public Car getCar(int id) {
return carTable.get(id);
}
/**
* Retrieves a truck by its ID.
*
* @param id the ID of the truck
* @return the truck with the given ID, or null if not found
*/
public Truck getTruck(int id) {
return truckTable.get(id);
}
/**
* Prints all vehicles in the database.
*/
public void printAllVehicles() {
for (Vehicle vehicle : vehicleTable.values()) {
logger.info(vehicle.toString());
}
}
}
@@ -0,0 +1,50 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.iluwatar.table.inheritance.App;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Logger;
import org.junit.jupiter.api.Test;
/**
* Tests if the main method runs without throwing exceptions and prints expected output.
*/
class AppTest {
@Test
void testAppMainMethod() {
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
PrintStream printStream = new PrintStream(outContent);
System.setOut(printStream);
Logger logger = Logger.getLogger(App.class.getName());
Handler handler = new ConsoleHandler() {
@Override
public void publish(java.util.logging.LogRecord recordObj) {
printStream.println(getFormatter().format(recordObj));
}
};
handler.setLevel(java.util.logging.Level.ALL);
logger.addHandler(handler);
App.main(new String[]{});
String output = outContent.toString();
assertTrue(output.contains("Retrieved Vehicle:"));
assertTrue(output.contains("Toyota")); // Car make
assertTrue(output.contains("Ford")); // Truck make
assertTrue(output.contains("Retrieved Car:"));
assertTrue(output.contains("Retrieved Truck:"));
}
}
@@ -0,0 +1,169 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.iluwatar.table.inheritance.Car;
import com.iluwatar.table.inheritance.Truck;
import com.iluwatar.table.inheritance.Vehicle;
import com.iluwatar.table.inheritance.VehicleDatabase;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Unit tests for the {@link VehicleDatabase} class.
* Tests saving, retrieving, and printing vehicles of different types.
*/
class VehicleDatabaseTest {
private VehicleDatabase vehicleDatabase;
/**
* Sets up a new instance of {@link VehicleDatabase} before each test.
*/
@BeforeEach
public void setUp() {
vehicleDatabase = new VehicleDatabase();
}
/**
* Tests saving a {@link Car} to the database and retrieving it.
*/
@Test
void testSaveAndRetrieveCar() {
Car car = new Car(2020, "Toyota", "Corolla", 4, 1);
vehicleDatabase.saveVehicle(car);
Vehicle retrievedVehicle = vehicleDatabase.getVehicle(car.getId());
assertNotNull(retrievedVehicle);
assertEquals(car.getId(), retrievedVehicle.getId());
assertEquals(car.getMake(), retrievedVehicle.getMake());
assertEquals(car.getModel(), retrievedVehicle.getModel());
assertEquals(car.getYear(), retrievedVehicle.getYear());
Car retrievedCar = vehicleDatabase.getCar(car.getId());
assertNotNull(retrievedCar);
assertEquals(car.getNumDoors(), retrievedCar.getNumDoors());
}
/**
* Tests saving a {@link Truck} to the database and retrieving it.
*/
@Test
void testSaveAndRetrieveTruck() {
Truck truck = new Truck(2018, "Ford", "F-150", 60, 2);
vehicleDatabase.saveVehicle(truck);
Vehicle retrievedVehicle = vehicleDatabase.getVehicle(truck.getId());
assertNotNull(retrievedVehicle);
assertEquals(truck.getId(), retrievedVehicle.getId());
assertEquals(truck.getMake(), retrievedVehicle.getMake());
assertEquals(truck.getModel(), retrievedVehicle.getModel());
assertEquals(truck.getYear(), retrievedVehicle.getYear());
Truck retrievedTruck = vehicleDatabase.getTruck(truck.getId());
assertNotNull(retrievedTruck);
assertEquals(truck.getLoadCapacity(), retrievedTruck.getLoadCapacity());
}
/**
* Tests saving multiple vehicles to the database and printing them.
*/
@Test
void testPrintAllVehicles() {
Car car = new Car(2020, "Toyota", "Corolla", 4, 1);
Truck truck = new Truck(2018, "Ford", "F-150", 60, 2);
vehicleDatabase.saveVehicle(car);
vehicleDatabase.saveVehicle(truck);
vehicleDatabase.printAllVehicles();
Vehicle retrievedCar = vehicleDatabase.getVehicle(car.getId());
Vehicle retrievedTruck = vehicleDatabase.getVehicle(truck.getId());
assertNotNull(retrievedCar);
assertNotNull(retrievedTruck);
}
/**
* Tests the constructor of {@link Car} with valid values.
*/
@Test
void testCarConstructor() {
Car car = new Car(2020, "Toyota", "Corolla", 4, 1);
assertEquals(2020, car.getYear());
assertEquals("Toyota", car.getMake());
assertEquals("Corolla", car.getModel());
assertEquals(4, car.getNumDoors());
assertEquals(1, car.getId()); // Assuming the ID is auto-generated in the constructor
}
/**
* Tests the constructor of {@link Car} with invalid number of doors (negative value).
*/
@Test
void testCarConstructorWithInvalidNumDoors() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
new Car(2020, "Toyota", "Corolla", -4, 1);
});
assertEquals("Number of doors must be positive.", exception.getMessage());
}
/**
* Tests the constructor of {@link Car} with zero doors.
*/
@Test
void testCarConstructorWithZeroDoors() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
new Car(2020, "Toyota", "Corolla", 0, 1);
});
assertEquals("Number of doors must be positive.", exception.getMessage());
}
/**
* Tests the constructor of {@link Truck} with invalid load capacity (negative value).
*/
@Test
void testTruckConstructorWithInvalidLoadCapacity() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
new Truck(2018, "Ford", "F-150", -60, 2);
});
assertEquals("Load capacity must be positive.", exception.getMessage());
}
/**
* Tests the constructor of {@link Truck} with zero load capacity.
*/
@Test
void testTruckConstructorWithZeroLoadCapacity() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
new Truck(2018, "Ford", "F-150", 0, 2);
});
assertEquals("Load capacity must be positive.", exception.getMessage());
}
/**
* Tests setting invalid number of doors in {@link Car} using setter (negative value).
*/
@Test
void testSetInvalidNumDoors() {
Car car = new Car(2020, "Toyota", "Corolla", 4, 1);
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
car.setNumDoors(-2);
});
assertEquals("Number of doors must be positive.", exception.getMessage());
}
/**
* Tests setting invalid load capacity in {@link Truck} using setter (negative value).
*/
@Test
void testSetInvalidLoadCapacity() {
Truck truck = new Truck(2018, "Ford", "F-150", 60, 2);
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
truck.setLoadCapacity(-10);
});
assertEquals("Load capacity must be positive.", exception.getMessage());
}
}