diff --git a/.all-contributorsrc b/.all-contributorsrc index 4f780072c499..449bb9b3ccdd 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3312,6 +3312,24 @@ "contributions": [ "translation" ] + }, + { + "login": "HabibaMekay", + "name": "HabibaMekay", + "avatar_url": "https://avatars.githubusercontent.com/u/133516736?v=4", + "profile": "https://github.com/HabibaMekay", + "contributions": [ + "code" + ] + }, + { + "login": "Ahmed-Taha-981", + "name": "Ahmed-Taha-981", + "avatar_url": "https://avatars.githubusercontent.com/u/122402269?v=4", + "profile": "https://github.com/Ahmed-Taha-981", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 6, diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 992d8cb25e94..000000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,61 +0,0 @@ -# Configuration for probot-stale - https://github.com/probot/stale - -# Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 60 - -# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. -# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: false - -# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) -onlyLabels: [] - -# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable -exemptLabels: - - "info: help wanted" - -# Set to true to ignore issues in a project (defaults to false) -exemptProjects: false - -# Set to true to ignore issues in a milestone (defaults to false) -exemptMilestones: false - -# Set to true to ignore issues with an assignee (defaults to false) -exemptAssignees: false - -# Label to use when marking as stale -staleLabel: "status: stale" - -# Comment to post when marking as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. The issue will be unassigned if no further activity occurs. Thank you - for your contributions. - -# Comment to post when removing the stale label. -# unmarkComment: > -# Your comment here. - -# Comment to post when closing a stale Issue or Pull Request. -# closeComment: > -# Your comment here. - -# Limit the number of actions per hour, from 1-30. Default is 30 -limitPerRun: 30 - -# Limit to only `issues` or `pulls` -# only: issues - -# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': -pulls: - daysUntilStale: 30 - daysUntilClose: 45 - markComment: > - This pull request has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. - closeComment: > - Closed due to inactivity. Thank you for your contributions. - -# issues: -# exemptLabels: -# - confirmed diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000000..cbfaa87d9b71 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,19 @@ +name: 'Comment on stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: 'This issue is stale because it has been open 60 days with no activity.' + stale-pr-message: 'This PR is stale because it has been open 60 days with no activity.' + close-issue-message: 'This issue was closed because it has been stalled for too long with no activity.' + close-pr-message: 'This PR was closed because it has been stalled for too long with no activity.' + days-before-issue-stale: 60 + days-before-pr-stale: 60 + days-before-issue-close: -1 + days-before-pr-close: -1 diff --git a/README.md b/README.md index 009984d7c43c..a1eb6d660b6b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=iluwatar_java-design-patterns&metric=coverage)](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns) [![Join the chat at https://gitter.im/iluwatar/java-design-patterns](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/iluwatar/java-design-patterns?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-362-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-365-orange.svg?style=flat-square)](#contributors-)
@@ -544,6 +544,8 @@ This project is licensed under the terms of the MIT license. Salma
Salma

💻 Arpit Sarang
Arpit Sarang

💻 Maya
Maya

🌍 + HabibaMekay
HabibaMekay

💻 + Ahmed-Taha-981
Ahmed-Taha-981

💻 diff --git a/hexagonal-architecture/pom.xml b/hexagonal-architecture/pom.xml index 633e39f0004b..d79a7c124ada 100644 --- a/hexagonal-architecture/pom.xml +++ b/hexagonal-architecture/pom.xml @@ -46,7 +46,7 @@ de.flapdoodle.embed de.flapdoodle.embed.mongo - 4.18.0 + 4.18.1 test diff --git a/money/README.md b/money/README.md new file mode 100644 index 000000000000..ca64d68fe882 --- /dev/null +++ b/money/README.md @@ -0,0 +1,168 @@ +--- +title: "Money Pattern in Java: Encapsulating Monetary Values with Currency Consistency" +shortTitle: Money +description: "Learn how the Money design pattern in Java ensures currency safety, precision handling, and maintainable financial operations. Explore examples, applicability, and benefits of the pattern." +category: Behavioral +language: en +tag: + - Encapsulation + - Precision handling + - Currency safety + - Value Object + - Financial operations + - Currency + - Financial + - Immutable + - Value Object +--- + +## Also known as + +* Monetary Value Object + +## Intent of Money Design Pattern + +The Money design pattern provides a robust way to encapsulate monetary values and their associated currencies. It ensures precise calculations, currency consistency, and maintainability of financial logic in Java applications. + +## Detailed Explanation of Money Pattern with Real-World Examples + +### Real-world example + +> Imagine an e-commerce platform where customers shop in their local currencies. The platform needs to calculate order totals, taxes, and discounts accurately while handling multiple currencies seamlessly. + +In this example: +- Each monetary value (like a product price or tax amount) is encapsulated in a `Money` object. +- The `Money` class ensures that only values in the same currency are combined and supports safe currency conversion for global operations. + +### In plain words + +> The Money pattern encapsulates both an amount and its currency, ensuring financial operations are precise, consistent, and maintainable. + +### Wikipedia says + +> "The Money design pattern encapsulates a monetary value and its currency, allowing for safe arithmetic operations and conversions while preserving accuracy and consistency in financial calculations." + +## Programmatic Example of Money Pattern in Java + +### Money Class + +```java + +/** + * Represents a monetary value with an associated currency. + * Provides operations for basic arithmetic (addition, subtraction, multiplication), + * as well as currency conversion while ensuring proper rounding. + */ +@Getter +public class Money { + private @Getter double amount; + private @Getter String currency; + + public Money(double amnt, String curr) { + this.amount = amnt; + this.currency = curr; + } + + private double roundToTwoDecimals(double value) { + return Math.round(value * 100.0) / 100.0; + } + + public void addMoney(Money moneyToBeAdded) throws CannotAddTwoCurrienciesException { + if (!moneyToBeAdded.getCurrency().equals(this.currency)) { + throw new CannotAddTwoCurrienciesException("You are trying to add two different currencies"); + } + this.amount = roundToTwoDecimals(this.amount + moneyToBeAdded.getAmount()); + } + + public void subtractMoney(Money moneyToBeSubtracted) throws CannotSubtractException { + if (!moneyToBeSubtracted.getCurrency().equals(this.currency)) { + throw new CannotSubtractException("You are trying to subtract two different currencies"); + } else if (moneyToBeSubtracted.getAmount() > this.amount) { + throw new CannotSubtractException("The amount you are trying to subtract is larger than the amount you have"); + } + this.amount = roundToTwoDecimals(this.amount - moneyToBeSubtracted.getAmount()); + } + + public void multiply(int factor) { + if (factor < 0) { + throw new IllegalArgumentException("Factor must be non-negative"); + } + this.amount = roundToTwoDecimals(this.amount * factor); + } + + public void exchangeCurrency(String currencyToChangeTo, double exchangeRate) { + if (exchangeRate < 0) { + throw new IllegalArgumentException("Exchange rate must be non-negative"); + } + this.amount = roundToTwoDecimals(this.amount * exchangeRate); + this.currency = currencyToChangeTo; + } +} + +## When to Use the Money Pattern + +The Money pattern should be used in scenarios where: + +1. **Currency-safe arithmetic operations** + To ensure that arithmetic operations like addition, subtraction, and multiplication are performed only between amounts in the same currency, preventing inconsistencies or errors in calculations. + +2. **Accurate rounding for financial calculations** + Precise rounding to two decimal places is critical to maintain accuracy and consistency in financial systems. + +3. **Consistent currency conversion** + When handling international transactions or displaying monetary values in different currencies, the Money pattern facilitates easy and reliable conversion using exchange rates. + +4. **Encapsulation of monetary logic** + By encapsulating all monetary operations within a dedicated class, the Money pattern improves maintainability and reduces the likelihood of errors. + +5. **Preventing errors in financial operations** + Strict validation ensures that operations like subtraction or multiplication are only performed when conditions are met, safeguarding against misuse or logical errors. + +6. **Handling diverse scenarios in financial systems** + Useful in complex systems like e-commerce, banking, and payroll applications where precise and consistent monetary value handling is crucial. + +--- +## Benefits and Trade-offs of Money Pattern + +### Benefits +1. **Precision and Accuracy** + The Money pattern ensures precise handling of monetary values, reducing the risk of rounding errors. + +2. **Encapsulation of Business Logic** + By encapsulating monetary operations, the pattern enhances maintainability and reduces redundancy in financial systems. + +3. **Currency Safety** + It ensures operations are performed only between amounts of the same currency, avoiding logical errors. + +4. **Improved Readability** + By abstracting monetary logic into a dedicated class, the code becomes easier to read and maintain. + +5. **Ease of Extension** + Adding new operations, handling different currencies, or incorporating additional business rules is straightforward. + +### Trade-offs +1. **Increased Complexity** + Introducing a dedicated `Money` class can add some overhead, especially for small or simple projects. + +2. **Potential for Misuse** + Without proper validation and handling, incorrect usage of the Money pattern may introduce subtle bugs. + +3. **Performance Overhead** + Precision and encapsulation might slightly affect performance in systems with extremely high transaction volumes. + +--- + +## Related Design Patterns + +1. **Value Object** + Money is a classic example of the Value Object pattern, where objects are immutable and define equality based on their value. + Link:https://martinfowler.com/bliki/ValueObject.html +2. **Factory Method** + Factories can be employed to handle creation logic, such as applying default exchange rates or rounding rules. + Link:https://www.geeksforgeeks.org/factory-method-for-designing-pattern/ +--- + +## References and Credits + +- [Patterns of Enterprise Application Architecture](https://martinfowler.com/eaaCatalog/money.html) by Martin Fowler +- [Design Patterns: Elements of Reusable Object-Oriented Software](https://amzn.to/3w0pvKI) diff --git a/money/pom.xml b/money/pom.xml new file mode 100644 index 000000000000..0129bab9501c --- /dev/null +++ b/money/pom.xml @@ -0,0 +1,49 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + money + + + + + org.junit.jupiter + junit-jupiter-api + test + + + + \ No newline at end of file diff --git a/money/src/main/java/com/iluwatar/App.java b/money/src/main/java/com/iluwatar/App.java new file mode 100644 index 000000000000..4030aa776159 --- /dev/null +++ b/money/src/main/java/com/iluwatar/App.java @@ -0,0 +1,65 @@ +package com.iluwatar; + +import java.util.logging.Level; +import java.util.logging.Logger; +/** + * The `App` class demonstrates the functionality of the {@link Money} class, which encapsulates + * monetary values and their associated currencies. It showcases operations like addition, + * subtraction, multiplication, and currency conversion, while ensuring validation and immutability. + * + *

Through this example, the handling of invalid operations (e.g., mismatched currencies or + * invalid inputs) is demonstrated using custom exceptions. Logging is used for transparency. + * + *

This highlights the practical application of object-oriented principles such as encapsulation + * and validation in a financial context. + */ +public class App { + + // Initialize the logger + private static final Logger logger = Logger.getLogger(App.class.getName()); + /** + * Program entry point. + * + * @param args command line args + */ + public static void main(String[] args) { + // Create instances of Money + Money usdAmount1 = new Money(50.00, "USD"); + Money usdAmount2 = new Money(20.00, "USD"); + + // Demonstrate addition + try { + usdAmount1.addMoney(usdAmount2); + logger.log(Level.INFO, "Sum in USD: {0}", usdAmount1.getAmount()); + } catch (CannotAddTwoCurrienciesException e) { + logger.log(Level.SEVERE, "Error adding money: {0}", e.getMessage()); + } + + // Demonstrate subtraction + try { + usdAmount1.subtractMoney(usdAmount2); + logger.log(Level.INFO, "Difference in USD: {0}", usdAmount1.getAmount()); + } catch (CannotSubtractException e) { + logger.log(Level.SEVERE, "Error subtracting money: {0}", e.getMessage()); + } + + // Demonstrate multiplication + try { + usdAmount1.multiply(2); + logger.log(Level.INFO, "Multiplied Amount in USD: {0}", usdAmount1.getAmount()); + } catch (IllegalArgumentException e) { + logger.log(Level.SEVERE, "Error multiplying money: {0}", e.getMessage()); + } + + // Demonstrate currency conversion + try { + double exchangeRateUsdToEur = 0.85; // Example exchange rate + usdAmount1.exchangeCurrency("EUR", exchangeRateUsdToEur); + logger.log(Level.INFO, "USD converted to EUR: {0} {1}", new Object[]{usdAmount1.getAmount(), usdAmount1.getCurrency()}); + } catch (IllegalArgumentException e) { + logger.log(Level.SEVERE, "Error converting currency: {0}", e.getMessage()); + } + + } +} + diff --git a/money/src/main/java/com/iluwatar/CannotAddTwoCurrienciesException.java b/money/src/main/java/com/iluwatar/CannotAddTwoCurrienciesException.java new file mode 100644 index 000000000000..587c8917ecef --- /dev/null +++ b/money/src/main/java/com/iluwatar/CannotAddTwoCurrienciesException.java @@ -0,0 +1,14 @@ +package com.iluwatar; +/** + * An exception for when the user tries to add two diffrent currencies. + */ +public class CannotAddTwoCurrienciesException extends Exception { + /** + * Constructs an exception with the specified message. + * + * @param message the message shown in the terminal (as a String). + */ + public CannotAddTwoCurrienciesException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/money/src/main/java/com/iluwatar/CannotSubtractException.java b/money/src/main/java/com/iluwatar/CannotSubtractException.java new file mode 100644 index 000000000000..881b458c0481 --- /dev/null +++ b/money/src/main/java/com/iluwatar/CannotSubtractException.java @@ -0,0 +1,15 @@ +package com.iluwatar; +/** + * An exception for when the user tries to subtract two diffrent currencies or subtract an amount he doesn't have. + */ +public class CannotSubtractException extends Exception { + /** + * Constructs an exception with the specified message. + * + * @param message the message shown in the terminal (as a String). + */ + public CannotSubtractException(String message) { + super(message); + } + +} \ No newline at end of file diff --git a/money/src/main/java/com/iluwatar/Money.java b/money/src/main/java/com/iluwatar/Money.java new file mode 100644 index 000000000000..e0afe4c7cbb9 --- /dev/null +++ b/money/src/main/java/com/iluwatar/Money.java @@ -0,0 +1,91 @@ +package com.iluwatar; + +import lombok.Getter; + +/** + * Represents a monetary value with an associated currency. + * Provides operations for basic arithmetic (addition, subtraction, multiplication), + * as well as currency conversion while ensuring proper rounding. + */ +@Getter +public class Money { + private @Getter double amount; + private @Getter String currency; + + /** + * Constructs a Money object with the specified amount and currency. + * + * @param amnt the amount of money (as a double). + * @param curr the currency code (e.g., "USD", "EUR"). + */ + public Money(double amnt, String curr) { + this.amount = amnt; + this.currency = curr; + } + + /** + * Rounds the given value to two decimal places. + * + * @param value the value to round. + * @return the rounded value, up to two decimal places. + */ + private double roundToTwoDecimals(double value) { + return Math.round(value * 100.0) / 100.0; + } + + /** + * Adds another Money object to the current instance. + * + * @param moneyToBeAdded the Money object to add. + * @throws CannotAddTwoCurrienciesException if the currencies do not match. + */ + public void addMoney(Money moneyToBeAdded) throws CannotAddTwoCurrienciesException { + if (!moneyToBeAdded.getCurrency().equals(this.currency)) { + throw new CannotAddTwoCurrienciesException("You are trying to add two different currencies"); + } + this.amount = roundToTwoDecimals(this.amount + moneyToBeAdded.getAmount()); + } + + /** + * Subtracts another Money object from the current instance. + * + * @param moneyToBeSubtracted the Money object to subtract. + * @throws CannotSubtractException if the currencies do not match or if the amount to subtract is larger than the current amount. + */ + public void subtractMoney(Money moneyToBeSubtracted) throws CannotSubtractException { + if (!moneyToBeSubtracted.getCurrency().equals(this.currency)) { + throw new CannotSubtractException("You are trying to subtract two different currencies"); + } else if (moneyToBeSubtracted.getAmount() > this.amount) { + throw new CannotSubtractException("The amount you are trying to subtract is larger than the amount you have"); + } + this.amount = roundToTwoDecimals(this.amount - moneyToBeSubtracted.getAmount()); + } + + /** + * Multiplies the current amount of money by a factor. + * + * @param factor the factor to multiply by. + * @throws IllegalArgumentException if the factor is negative. + */ + public void multiply(int factor) { + if (factor < 0) { + throw new IllegalArgumentException("Factor must be non-negative"); + } + this.amount = roundToTwoDecimals(this.amount * factor); + } + + /** + * Converts the current amount of money to another currency using the provided exchange rate. + * + * @param currencyToChangeTo the new currency to convert to. + * @param exchangeRate the exchange rate to convert from the current currency to the new currency. + * @throws IllegalArgumentException if the exchange rate is negative. + */ + public void exchangeCurrency(String currencyToChangeTo, double exchangeRate) { + if (exchangeRate < 0) { + throw new IllegalArgumentException("Exchange rate must be non-negative"); + } + this.amount = roundToTwoDecimals(this.amount * exchangeRate); + this.currency = currencyToChangeTo; + } +} diff --git a/money/src/test/java/com/iluwater/money/MoneyTest.java b/money/src/test/java/com/iluwater/money/MoneyTest.java new file mode 100644 index 000000000000..94d93359b0ad --- /dev/null +++ b/money/src/test/java/com/iluwater/money/MoneyTest.java @@ -0,0 +1,125 @@ +package com.iluwater.money; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import com.iluwatar.CannotAddTwoCurrienciesException; +import com.iluwatar.CannotSubtractException; +import com.iluwatar.Money; +import com.iluwatar.App; + + + class MoneyTest { + + @Test + void testConstructor() { + // Test the constructor + Money money = new Money(100.00, "USD"); + assertEquals(100.00, money.getAmount()); + assertEquals("USD", money.getCurrency()); + } + + @Test + void testAddMoney_SameCurrency() throws CannotAddTwoCurrienciesException { + // Test adding two Money objects with the same currency + Money money1 = new Money(100.00, "USD"); + Money money2 = new Money(50.25, "USD"); + + money1.addMoney(money2); + + assertEquals(150.25, money1.getAmount(), "Amount after addition should be 150.25"); + } + + @Test + void testAddMoney_DifferentCurrency() { + // Test adding two Money objects with different currencies + Money money1 = new Money(100.00, "USD"); + Money money2 = new Money(50.25, "EUR"); + + assertThrows(CannotAddTwoCurrienciesException.class, () -> { + money1.addMoney(money2); + }); + } + + @Test + void testSubtractMoney_SameCurrency() throws CannotSubtractException { + // Test subtracting two Money objects with the same currency + Money money1 = new Money(100.00, "USD"); + Money money2 = new Money(50.25, "USD"); + + money1.subtractMoney(money2); + + assertEquals(49.75, money1.getAmount(), "Amount after subtraction should be 49.75"); + } + + @Test + void testSubtractMoney_DifferentCurrency() { + // Test subtracting two Money objects with different currencies + Money money1 = new Money(100.00, "USD"); + Money money2 = new Money(50.25, "EUR"); + + assertThrows(CannotSubtractException.class, () -> { + money1.subtractMoney(money2); + }); + } + + @Test + void testSubtractMoney_AmountTooLarge() { + // Test subtracting an amount larger than the current amount + Money money1 = new Money(50.00, "USD"); + Money money2 = new Money(60.00, "USD"); + + assertThrows(CannotSubtractException.class, () -> { + money1.subtractMoney(money2); + }); + } + + @Test + void testMultiply() { + // Test multiplying the money amount by a factor + Money money = new Money(100.00, "USD"); + + money.multiply(3); + + assertEquals(300.00, money.getAmount(), "Amount after multiplication should be 300.00"); + } + + @Test + void testMultiply_NegativeFactor() { + // Test multiplying by a negative factor + Money money = new Money(100.00, "USD"); + + assertThrows(IllegalArgumentException.class, () -> { + money.multiply(-2); + }); + } + + @Test + void testExchangeCurrency() { + // Test converting currency using an exchange rate + Money money = new Money(100.00, "USD"); + + money.exchangeCurrency("EUR", 0.85); + + assertEquals("EUR", money.getCurrency(), "Currency after conversion should be EUR"); + assertEquals(85.00, money.getAmount(), "Amount after conversion should be 85.00"); + } + + @Test + void testExchangeCurrency_NegativeExchangeRate() { + // Test converting currency with a negative exchange rate + Money money = new Money(100.00, "USD"); + + assertThrows(IllegalArgumentException.class, () -> { + money.exchangeCurrency("EUR", -0.85); + }); + } + + + @Test + void testAppExecution() { + assertDoesNotThrow(() -> { + App.main(new String[]{}); + }, "App execution should not throw any exceptions"); + } + +} diff --git a/pom.xml b/pom.xml index 97128f9f3cc2..fc1e6e80f9fd 100644 --- a/pom.xml +++ b/pom.xml @@ -218,7 +218,9 @@ function-composition microservices-distributed-tracing microservices-idempotent-consumer - templateview + templateview + money + table-inheritance diff --git a/table-inheritance/README.md b/table-inheritance/README.md new file mode 100644 index 000000000000..3e3ad4f53bf9 --- /dev/null +++ b/table-inheritance/README.md @@ -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 + + + + + + + + + +## 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) diff --git a/table-inheritance/pom.xml b/table-inheritance/pom.xml new file mode 100644 index 000000000000..9a886307ffe7 --- /dev/null +++ b/table-inheritance/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + table-inheritance + + + + org.junit.jupiter + junit-jupiter-engine + 5.7.0 + test + + + org.projectlombok + lombok + 1.18.24 + provided + + + + + + \ No newline at end of file diff --git a/table-inheritance/src/main/java/com/iluwatar/table/inheritance/App.java b/table-inheritance/src/main/java/com/iluwatar/table/inheritance/App.java new file mode 100644 index 000000000000..4f848fb83e61 --- /dev/null +++ b/table-inheritance/src/main/java/com/iluwatar/table/inheritance/App.java @@ -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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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)); + + } +} diff --git a/table-inheritance/src/main/java/com/iluwatar/table/inheritance/Car.java b/table-inheritance/src/main/java/com/iluwatar/table/inheritance/Car.java new file mode 100644 index 000000000000..b7332bf2a6b0 --- /dev/null +++ b/table-inheritance/src/main/java/com/iluwatar/table/inheritance/Car.java @@ -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() + + '}'; + } +} diff --git a/table-inheritance/src/main/java/com/iluwatar/table/inheritance/Truck.java b/table-inheritance/src/main/java/com/iluwatar/table/inheritance/Truck.java new file mode 100644 index 000000000000..5d093c688c9e --- /dev/null +++ b/table-inheritance/src/main/java/com/iluwatar/table/inheritance/Truck.java @@ -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() + + '}'; + } +} + diff --git a/table-inheritance/src/main/java/com/iluwatar/table/inheritance/Vehicle.java b/table-inheritance/src/main/java/com/iluwatar/table/inheritance/Vehicle.java new file mode 100644 index 000000000000..1333f6a166ee --- /dev/null +++ b/table-inheritance/src/main/java/com/iluwatar/table/inheritance/Vehicle.java @@ -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 + + '}'; + } +} diff --git a/table-inheritance/src/main/java/com/iluwatar/table/inheritance/VehicleDatabase.java b/table-inheritance/src/main/java/com/iluwatar/table/inheritance/VehicleDatabase.java new file mode 100644 index 000000000000..403112c32aa8 --- /dev/null +++ b/table-inheritance/src/main/java/com/iluwatar/table/inheritance/VehicleDatabase.java @@ -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 vehicleTable = new HashMap<>(); + private Map carTable = new HashMap<>(); + private Map 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()); + } + } +} + diff --git a/table-inheritance/src/test/java/AppTest.java b/table-inheritance/src/test/java/AppTest.java new file mode 100644 index 000000000000..9d4e74ebdd5b --- /dev/null +++ b/table-inheritance/src/test/java/AppTest.java @@ -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:")); + } +} + + + + diff --git a/table-inheritance/src/test/java/VehicleDatabaseTest.java b/table-inheritance/src/test/java/VehicleDatabaseTest.java new file mode 100644 index 000000000000..33f7335372f5 --- /dev/null +++ b/table-inheritance/src/test/java/VehicleDatabaseTest.java @@ -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()); + } +} + +