From f5795a1a4193f3b13ed0ba4917888bd1af0a2915 Mon Sep 17 00:00:00 2001
From: mperor <mpietryga93@gmail.com>
Date: Thu, 27 Feb 2025 20:01:28 +0100
Subject: [PATCH] Add value object examples

---
 .../code/ddd/value/object/DateTimeRange.java  | 19 ++++++++
 .../code/ddd/value/object/EmailAddress.java   | 14 ++++++
 .../clean/code/ddd/value/object/Money.java    | 33 ++++++++++++++
 .../ddd/value/object/ValueObjectTest.java     | 43 +++++++++++++++++++
 4 files changed, 109 insertions(+)
 create mode 100644 CleanCode/src/main/java/pl/mperor/lab/java/clean/code/ddd/value/object/DateTimeRange.java
 create mode 100644 CleanCode/src/main/java/pl/mperor/lab/java/clean/code/ddd/value/object/EmailAddress.java
 create mode 100644 CleanCode/src/main/java/pl/mperor/lab/java/clean/code/ddd/value/object/Money.java
 create mode 100644 CleanCode/src/test/java/pl/mperor/lab/java/clean/code/ddd/value/object/ValueObjectTest.java

diff --git a/CleanCode/src/main/java/pl/mperor/lab/java/clean/code/ddd/value/object/DateTimeRange.java b/CleanCode/src/main/java/pl/mperor/lab/java/clean/code/ddd/value/object/DateTimeRange.java
new file mode 100644
index 0000000..cbc60d0
--- /dev/null
+++ b/CleanCode/src/main/java/pl/mperor/lab/java/clean/code/ddd/value/object/DateTimeRange.java
@@ -0,0 +1,19 @@
+package pl.mperor.lab.java.clean.code.ddd.value.object;
+
+import java.time.LocalDateTime;
+
+record DateTimeRange(LocalDateTime start, LocalDateTime end) {
+
+    DateTimeRange {
+        if (start == null || end == null) {
+            throw new IllegalArgumentException("Date range start & end cannot be null!");
+        }
+        if (start.isAfter(end)) {
+            throw new IllegalArgumentException("Date range start cannot be after end!");
+        }
+    }
+
+    boolean isWithinRange(LocalDateTime dateTime) {
+        return !dateTime.isBefore(start) && !dateTime.isAfter(end);
+    }
+}
diff --git a/CleanCode/src/main/java/pl/mperor/lab/java/clean/code/ddd/value/object/EmailAddress.java b/CleanCode/src/main/java/pl/mperor/lab/java/clean/code/ddd/value/object/EmailAddress.java
new file mode 100644
index 0000000..48ab527
--- /dev/null
+++ b/CleanCode/src/main/java/pl/mperor/lab/java/clean/code/ddd/value/object/EmailAddress.java
@@ -0,0 +1,14 @@
+package pl.mperor.lab.java.clean.code.ddd.value.object;
+
+import java.util.regex.Pattern;
+
+record EmailAddress(String email) {
+
+    private static final Pattern emailPattern = Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$");
+
+    EmailAddress {
+        if (email == null || emailPattern.asMatchPredicate().negate().test(email)) {
+            throw new IllegalArgumentException("Invalid email address!");
+        }
+    }
+}
diff --git a/CleanCode/src/main/java/pl/mperor/lab/java/clean/code/ddd/value/object/Money.java b/CleanCode/src/main/java/pl/mperor/lab/java/clean/code/ddd/value/object/Money.java
new file mode 100644
index 0000000..a6ff19f
--- /dev/null
+++ b/CleanCode/src/main/java/pl/mperor/lab/java/clean/code/ddd/value/object/Money.java
@@ -0,0 +1,33 @@
+package pl.mperor.lab.java.clean.code.ddd.value.object;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+record Money(BigDecimal amount) {
+
+    Money(BigDecimal amount) {
+        if (amount == null) {
+            throw new IllegalArgumentException("Amount must not be null");
+        }
+        if (amount.compareTo(BigDecimal.ZERO) < 0) {
+            throw new IllegalArgumentException("Amount cannot be negative!");
+        }
+        this.amount = setScale(amount);
+    }
+
+    private BigDecimal setScale(BigDecimal input) {
+        return input.setScale(2, RoundingMode.HALF_EVEN);
+    }
+
+    public Money add(Money other) {
+        return new Money(this.amount.add(other.amount));
+    }
+
+    public Money subtract(Money other) {
+        return new Money(this.amount.subtract(other.amount));
+    }
+
+    public static Money of(BigDecimal amount) {
+        return new Money(amount);
+    }
+}
diff --git a/CleanCode/src/test/java/pl/mperor/lab/java/clean/code/ddd/value/object/ValueObjectTest.java b/CleanCode/src/test/java/pl/mperor/lab/java/clean/code/ddd/value/object/ValueObjectTest.java
new file mode 100644
index 0000000..0548ff1
--- /dev/null
+++ b/CleanCode/src/test/java/pl/mperor/lab/java/clean/code/ddd/value/object/ValueObjectTest.java
@@ -0,0 +1,43 @@
+package pl.mperor.lab.java.clean.code.ddd.value.object;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+
+
+class ValueObjectTest {
+
+    @Test
+    void shouldAllowToUseMoneyAsValueObject() {
+        var money = Money.of(BigDecimal.ZERO);
+        Assertions.assertEquals(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_EVEN), money.amount());
+
+        Assertions.assertThrows(IllegalArgumentException.class, () -> new Money(null));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> new Money(new BigDecimal("-1")));
+
+        var three = Money.of(BigDecimal.ONE).add(Money.of(BigDecimal.TWO));
+        Assertions.assertEquals(new BigDecimal("3.00"), three.amount());
+        Assertions.assertEquals(new BigDecimal("2.00"), three.subtract(Money.of(BigDecimal.ONE)).amount());
+    }
+
+    @Test
+    void shouldAllowToUseEmailAddressAsValueObject() {
+        var email = new EmailAddress("john.doe@example.com");
+        Assertions.assertEquals("john.doe@example.com", email.email());
+        Assertions.assertThrows(IllegalArgumentException.class, () -> new EmailAddress(null));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> new EmailAddress("not an email @.wtf"));
+    }
+
+    @Test
+    void shouldAllowToUseDateTimeRangeAsValueObject() {
+        Assertions.assertTrue(new DateTimeRange(LocalDateTime.MIN, LocalDateTime.MAX)
+                .isWithinRange(LocalDateTime.now()));
+        Assertions.assertTrue(new DateTimeRange(LocalDateTime.MIN, LocalDateTime.MAX)
+                .isWithinRange(LocalDateTime.MIN));
+        Assertions.assertTrue(new DateTimeRange(LocalDateTime.MIN, LocalDateTime.MAX)
+                .isWithinRange(LocalDateTime.MAX));
+    }
+}
\ No newline at end of file