mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-05-14 12:58:37 +00:00
feat: Money pattern (#3109)
* Money pattern the implementation of the money patter and the tests along with the read me file * Update money/README.md Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com> * added App file anf modified README file and pom.xml file * modified README and pom.xml * added comments * Added a test for App.java --------- Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
This commit is contained in:
+168
@@ -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)
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
|
||||
The MIT License
|
||||
Copyright © 2014-2022 Ilkka Seppälä
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
-->
|
||||
<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>money</artifactId>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -218,7 +218,8 @@
|
||||
<module>function-composition</module>
|
||||
<module>microservices-distributed-tracing</module>
|
||||
<module>microservices-idempotent-consumer</module>
|
||||
<module>table-inheritance</module>
|
||||
<module>money</module>
|
||||
<module>table-inheritance</module>
|
||||
</modules>
|
||||
<repositories>
|
||||
<repository>
|
||||
|
||||
Reference in New Issue
Block a user