Immutable Object และการ Design Class ที่ปลอดภัย

บทนำ: ปัญหาของ Mutable Objects

Mutable = สามารถเปลี่ยนแปลงได้

java// ❌ ปัญหา: Object ที่ mutable
public class Point {
    public double x;
    public double y;
}

// สร้าง point
Point p1 = new Point();
p1.x = 10;
p1.y = 20;

// ส่งให้ function อื่น
printPoint(p1);

// ❌ ใครก็เปลี่ยนได้!
p1.x = 999;
p1.y = -999;

// ❌ ปัญหา: Function คาดหวังค่าเดิม แต่เปลี่ยนไปแล้ว

ปัญหา:

  • ❌ Object ถูกเปลี่ยนโดยไม่ตั้งใจ
  • ❌ ไม่ปลอดภัยใน multi-threading
  • ❌ Bug ยากต่อการ debug
  • ❌ ไม่สามารถ cache ได้

Immutable Object คืออะไร?

Immutable Object = Object ที่ไม่สามารถเปลี่ยนแปลงได้ หลังจากสร้าง

textCharacteristics:
1. ไม่มี setter
2. Attributes เป็น private final
3. Constructor initialize เมื่อสร้าง
4. ไม่สามารถเปลี่ยนค่าได้ หลังจากนั้น

Immutable Object Design Pattern

เกณฑ์ที่ต้องปฏิบัติ

text1. ✓ ทุก attributes เป็น private final
2. ✓ ไม่มี setter
3. ✓ Constructor initialize ทั้งหมด
4. ✓ Mutable attributes ต้อง deep copy
5. ✓ Getter ต้อง safe (ไม่ return reference เลย)

ตัวอย่างที่ 1: String เป็น Immutable

java// String ใน Java เป็น immutable
String name = "John";

// ✓ ดูเหมือน change ค่า
String newName = name.toUpperCase();

// ❓ แต่จริงๆ สร้าง object ใหม่
System.out.println(name);      // "John" (ไม่เปลี่ยน)
System.out.println(newName);   // "JOHN" (object ใหม่)

// ✓ ไม่สามารถ change ได้
// name[0] = 'j';  // ERROR! ไม่มี method

// ✓ String reference เดียวกัน → value เดียวกัน
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2);  // true (same object)

// ✓ ปลอดภัยใน multi-threading

ตัวอย่างที่ 2: Immutable Point Class

java// ✓ Immutable Point
public class ImmutablePoint {
    // ✓ private final
    private final double x;
    private final double y;
    
    // Constructor (เดียวทาง initialize)
    public ImmutablePoint(double x, double y) {
        this.x = x;
        this.y = y;
    }
    
    // ✓ Getter (ไม่มี setter)
    public double getX() {
        return this.x;
    }
    
    public double getY() {
        return this.y;
    }
    
    // ✓ ไม่สามารถ change ได้
    // ถ้าต้องการค่าใหม่ → สร้าง object ใหม่
    public ImmutablePoint move(double dx, double dy) {
        return new ImmutablePoint(this.x + dx, this.y + dy);
    }
    
    public void displayInfo() {
        System.out.printf("Point: (%.2f, %.2f)\n", x, y);
    }
}

// ใช้งาน
public class Main {
    public static void main(String[] args) {
        ImmutablePoint p1 = new ImmutablePoint(10, 20);
        p1.displayInfo();  // Point: (10.00, 20.00)
        
        // ✓ ต้องการจุดใหม่ → สร้าง object ใหม่
        ImmutablePoint p2 = p1.move(5, 10);
        p2.displayInfo();  // Point: (15.00, 30.00)
        
        // ✓ p1 ไม่เปลี่ยน
        p1.displayInfo();  // Point: (10.00, 20.00)
        
        // ❌ ไม่มี setter
        // p1.setX(100);  // ERROR!
    }
}

ตัวอย่างที่ 3: Immutable Date Class

javaimport java.time.LocalDate;

// ✓ Immutable Date
public class ImmutableDate {
    private final int day;
    private final int month;
    private final int year;
    
    public ImmutableDate(int day, int month, int year) {
        if (day < 1 || day > 31) {
            throw new IllegalArgumentException("Invalid day");
        }
        if (month < 1 || month > 12) {
            throw new IllegalArgumentException("Invalid month");
        }
        
        this.day = day;
        this.month = month;
        this.year = year;
    }
    
    public int getDay() {
        return this.day;
    }
    
    public int getMonth() {
        return this.month;
    }
    
    public int getYear() {
        return this.year;
    }
    
    // ✓ Methods ที่ return new object
    public ImmutableDate addDays(int days) {
        // Simplified (ไม่คำนวณแบบ real date)
        int newDay = this.day + days;
        return new ImmutableDate(newDay, this.month, this.year);
    }
    
    public void displayInfo() {
        System.out.printf("%d/%d/%d\n", day, month, year);
    }
}

// ใช้งาน
public class Main {
    public static void main(String[] args) {
        ImmutableDate d1 = new ImmutableDate(25, 10, 2024);
        d1.displayInfo();  // 25/10/2024
        
        // ✓ Add days → new object
        ImmutableDate d2 = d1.addDays(10);
        d2.displayInfo();  // 35/10/2024 (simplified)
        
        // ✓ d1 ไม่เปลี่ยน
        d1.displayInfo();  // 25/10/2024
    }
}

ตัวอย่างที่ 4: Immutable Person ด้วย Collections

javaimport java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// ✓ Immutable Person
public class ImmutablePerson {
    private final String name;
    private final int age;
    private final List<String> hobbies;  // reference type
    
    public ImmutablePerson(String name, int age, List<String> hobbies) {
        this.name = name;
        this.age = age;
        // ✓ Deep copy + unmodifiable
        this.hobbies = Collections.unmodifiableList(
            new ArrayList<>(hobbies)
        );
    }
    
    public String getName() {
        return this.name;
    }
    
    public int getAge() {
        return this.age;
    }
    
    // ✓ Return unmodifiable list
    public List<String> getHobbies() {
        return this.hobbies;
    }
    
    public void displayInfo() {
        System.out.printf("Name: %s | Age: %d | Hobbies: %s\n",
                         name, age, hobbies);
    }
}

// ใช้งาน
public class Main {
    public static void main(String[] args) {
        List<String> hobbyList = new ArrayList<>();
        hobbyList.add("Reading");
        hobbyList.add("Gaming");
        
        ImmutablePerson person = new ImmutablePerson("John", 25, hobbyList);
        person.displayInfo();
        
        // ✓ ปรับเปลี่ยน original list
        hobbyList.add("Swimming");
        
        // ✓ Person ไม่เปลี่ยน (deep copy)
        person.displayInfo();
        
        // ❌ ไม่สามารถ add hobby
        try {
            person.getHobbies().add("Cooking");  // ERROR!
        } catch (Exception e) {
            System.out.println("Error: Cannot modify hobbies");
        }
    }
}

Output:

textName: John | Age: 25 | Hobbies: [Reading, Gaming]
Name: John | Age: 25 | Hobbies: [Reading, Gaming]
Error: Cannot modify hobbies

Mutable vs Immutable

ตารางเปรียบเทียบ

text┌──────────────┬──────────────────────┬──────────────────────┐
│              │ Mutable              │ Immutable            │
├──────────────┼──────────────────────┼──────────────────────┤
│ Setter       │ มี                   │ ไม่มี                │
│ Attributes   │ public / private     │ private final        │
│ Change       │ setX(), setY()       │ return new object    │
│ Safety       │ ต่ำ                  │ สูง                  │
│ Threading    │ ต้อง synchronize     │ thread-safe          │
│ Cache        │ ไม่ได้               │ ได้                  │
│ Example      │ StringBuilder        │ String, Integer      │
└──────────────┴──────────────────────┴──────────────────────┘

Safe Class Design Principles

1. Principle of Least Privilege

java// ✓ ดี: เปิดเผยน้อยที่สุด
public class Account {
    private double balance;           // private
    
    public double getBalance() {      // public getter
        return this.balance;
    }
    
    public void withdraw(double amt) { // business method
        if (amt > this.balance) {
            throw new Exception("Insufficient");
        }
        this.balance -= amt;
    }
    // ไม่มี setBalance()
}

// ❌ ไม่ดี: เปิดเผยมากเกินไป
public class Account {
    public double balance;  // public - ใครก็เปลี่ยนได้
    
    public void setBalance(double amt) {  // direct setter
        this.balance = amt;  // ไม่มี validation
    }
}

2. Encapsulation

java// ✓ ดี: Encapsulation
public class Rectangle {
    private double width;
    private double height;
    
    public void setWidth(double w) {
        if (w <= 0) {
            throw new IllegalArgumentException("Width must be positive");
        }
        this.width = w;  // ✓ validation ทำงาน
    }
}

// ❌ ไม่ดี: Direct access
public class Rectangle {
    public double width;
    
    // width = -5;  // ❌ Invalid state!
}

3. Defense Against Mutation

java// ✓ ดี: Prevent mutation
public class Config {
    private final Map<String, String> settings;
    
    public Config(Map<String, String> data) {
        // ✓ Deep copy
        this.settings = Collections.unmodifiableMap(
            new HashMap<>(data)
        );
    }
    
    public String getSetting(String key) {
        return this.settings.get(key);
    }
    
    // ไม่มี setSetting()
}

// ❌ ไม่ดี: Vulnerable
public class Config {
    private Map<String, String> settings;
    
    public Map<String, String> getSettings() {
        return this.settings;  // ❌ ใครก็เปลี่ยนได้
    }
}

ตัวอย่างที่ 5: Design Safe Money Class

java// ✓ Immutable Money
public class Money {
    private final double amount;
    private final String currency;
    
    public Money(double amount, String currency) {
        if (amount < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }
        if (currency == null || currency.isEmpty()) {
            throw new IllegalArgumentException("Currency required");
        }
        
        this.amount = amount;
        this.currency = currency;
    }
    
    public double getAmount() {
        return this.amount;
    }
    
    public String getCurrency() {
        return this.currency;
    }
    
    // ✓ ไม่สามารถ change ได้
    // ถ้าต้องการรวมเงิน → return new Money
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Different currencies");
        }
        return new Money(this.amount + other.amount, this.currency);
    }
    
    // ✓ ไม่สามารถ change ได้
    // ถ้าต้องการหักเงิน → return new Money
    public Money subtract(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Different currencies");
        }
        double result = this.amount - other.amount;
        if (result < 0) {
            throw new IllegalArgumentException("Result would be negative");
        }
        return new Money(result, this.currency);
    }
    
    @Override
    public String toString() {
        return String.format("%.2f %s", amount, currency);
    }
}

// ใช้งาน
public class Main {
    public static void main(String[] args) {
        Money m1 = new Money(1000, "THB");
        Money m2 = new Money(500, "THB");
        
        System.out.println("m1 = " + m1);  // 1000.00 THB
        System.out.println("m2 = " + m2);  // 500.00 THB
        
        // ✓ Add
        Money m3 = m1.add(m2);
        System.out.println("m1 + m2 = " + m3);  // 1500.00 THB
        
        // ✓ m1, m2 ไม่เปลี่ยน
        System.out.println("m1 = " + m1);  // 1000.00 THB
        System.out.println("m2 = " + m2);  // 500.00 THB
        
        // ❌ ไม่มี setAmount()
        // m1.setAmount(9999);  // ERROR!
    }
}

Output:

textm1 = 1000.00 THB
m2 = 500.00 THB
m1 + m2 = 1500.00 THB
m1 = 1000.00 THB
m2 = 500.00 THB

ตัวอย่างที่ 6: Immutable Address ด้วย Complex Logic

java// ✓ Immutable Address
public class ImmutableAddress {
    private final String street;
    private final String city;
    private final String country;
    private final String zipCode;
    
    public ImmutableAddress(String street, String city, String country, String zip) {
        if (street == null || street.isEmpty()) {
            throw new IllegalArgumentException("Street required");
        }
        if (city == null || city.isEmpty()) {
            throw new IllegalArgumentException("City required");
        }
        if (country == null || country.isEmpty()) {
            throw new IllegalArgumentException("Country required");
        }
        if (!zip.matches("\\d{5}")) {
            throw new IllegalArgumentException("Invalid zip code");
        }
        
        this.street = street;
        this.city = city;
        this.country = country;
        this.zipCode = zip;
    }
    
    public String getStreet() {
        return this.street;
    }
    
    public String getCity() {
        return this.city;
    }
    
    public String getCountry() {
        return this.country;
    }
    
    public String getZipCode() {
        return this.zipCode;
    }
    
    // ✓ Factory method → return new object
    public ImmutableAddress updateCity(String newCity) {
        return new ImmutableAddress(
            this.street,
            newCity,
            this.country,
            this.zipCode
        );
    }
    
    @Override
    public String toString() {
        return String.format("%s, %s, %s %s",
                           street, city, country, zipCode);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ImmutableAddress)) {
            return false;
        }
        ImmutableAddress other = (ImmutableAddress) obj;
        return this.street.equals(other.street) &&
               this.city.equals(other.city) &&
               this.country.equals(other.country) &&
               this.zipCode.equals(other.zipCode);
    }
}

// ใช้งาน
public class Main {
    public static void main(String[] args) {
        ImmutableAddress addr1 = 
            new ImmutableAddress("123 Main St", "Bangkok", "Thailand", "10110");
        
        System.out.println("Address 1: " + addr1);
        
        // ✓ Change city → new object
        ImmutableAddress addr2 = addr1.updateCity("Chiang Mai");
        System.out.println("Address 2: " + addr2);
        
        // ✓ Original unchanged
        System.out.println("Address 1: " + addr1);
        
        // ✓ Can be used as HashMap key (because immutable)
        System.out.println("Same? " + addr1.equals(addr2));  // false
    }
}

Output:

textAddress 1: 123 Main St, Bangkok, Thailand 10110
Address 2: 123 Main St, Chiang Mai, Thailand 10110
Address 1: 123 Main St, Bangkok, Thailand 10110
Same? false

Benefits ของ Immutable Objects

text┌────────────────────────────────────────────┐
│  Benefits ของ Immutable Objects           │
├────────────────────────────────────────────┤
│                                            │
│  1. Thread Safety                          │
│     └─ ไม่ต้อง synchronize                │
│                                            │
│  2. Simpler Code                           │
│     └─ ไม่ต้อง worry mutation             │
│                                            │
│  3. Can Use As HashMap Key                 │
│     └─ hashCode ไม่เปลี่ยน                │
│                                            │
│  4. Caching                                │
│     └─ Value ไม่เปลี่ยน → cache ได้       │
│                                            │
│  5. Easier Testing                         │
│     └─ Predictable behavior               │
│                                            │
│  6. String Interning                       │
│     └─ Memory optimization                │
│                                            │
└────────────────────────────────────────────┘

Immutable ใน Java

Java Built-in Immutable Classes

text- String
- Integer, Long, Double, Boolean (wrapper classes)
- BigInteger, BigDecimal
- LocalDate, LocalTime, LocalDateTime
- Collections.unmodifiableList/Map/Set

ตัวอย่างการใช้

java// String immutable
String s = "Hello";
s = s.toUpperCase();  // ✓ ไม่ change ต้องเดิม, return new String

// Integer immutable
Integer i = 10;
Integer j = i + 5;  // ✓ return new Integer

// LocalDate immutable
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);  // ✓ return new LocalDate

Best Practices: Design Safe Classes

Checklist

text┌─────────────────────────────────────────┐
│  Safe Class Design Checklist            │
├─────────────────────────────────────────┤
│                                         │
│ ✓ All fields private final              │
│ ✓ No setters                            │
│ ✓ Constructor validates input           │
│ ✓ Defensive copy for mutable fields     │
│ ✓ Getters return safe copies            │
│ ✓ Methods return new objects (not this) │
│ ✓ No public/static mutable collections  │
│ ✓ Override equals() and hashCode()      │
│ ✓ Prevent extension (final class)       │
│ ✓ Document immutability                 │
│                                         │
└─────────────────────────────────────────┘

ตัวอย่างสุดท้าย: Complete Immutable Person Class

javaimport java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// ✓ Fully Immutable Person
public final class ImmutablePerson {  // final = ไม่ extend
    private final String name;
    private final int age;
    private final LocalDate birthDate;
    private final List<String> certifications;
    
    public ImmutablePerson(String name, int age, LocalDate birthDate, 
                          List<String> certifications) {
        // Validation
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("Name required");
        }
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Invalid age");
        }
        if (birthDate == null) {
            throw new IllegalArgumentException("Birth date required");
        }
        
        this.name = name;
        this.age = age;
        this.birthDate = birthDate;
        // ✓ Deep copy + unmodifiable
        this.certifications = Collections.unmodifiableList(
            new ArrayList<>(certifications)
        );
    }
    
    // Getters only
    public String getName() {
        return this.name;
    }
    
    public int getAge() {
        return this.age;
    }
    
    public LocalDate getBirthDate() {
        return this.birthDate;
    }
    
    public List<String> getCertifications() {
        return this.certifications;
    }
    
    // ✓ Factory methods (return new objects)
    public ImmutablePerson withName(String newName) {
        return new ImmutablePerson(newName, this.age, 
                                  this.birthDate, this.certifications);
    }
    
    public ImmutablePerson withCertification(String cert) {
        List<String> newCerts = new ArrayList<>(this.certifications);
        newCerts.add(cert);
        return new ImmutablePerson(this.name, this.age, 
                                  this.birthDate, newCerts);
    }
    
    @Override
    public String toString() {
        return String.format("Person{name='%s', age=%d, birth=%s, certs=%s}",
                           name, age, birthDate, certifications);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ImmutablePerson)) {
            return false;
        }
        ImmutablePerson other = (ImmutablePerson) obj;
        return this.name.equals(other.name) &&
               this.age == other.age &&
               this.birthDate.equals(other.birthDate) &&
               this.certifications.equals(other.certifications);
    }
    
    @Override
    public int hashCode() {
        return name.hashCode() * 31 + age;
    }
}

// ใช้งาน
public class Main {
    public static void main(String[] args) {
        LocalDate dob = LocalDate.of(1990, 5, 15);
        List<String> certs = new ArrayList<>();
        certs.add("Java");
        certs.add("OOP");
        
        ImmutablePerson person1 = 
            new ImmutablePerson("John", 34, dob, certs);
        
        System.out.println("Person 1: " + person1);
        
        // ✓ Add certification → new object
        ImmutablePerson person2 = person1.withCertification("Design Pattern");
        System.out.println("Person 2: " + person2);
        
        // ✓ Original unchanged
        System.out.println("Person 1: " + person1);
        
        // ✓ Can use as HashMap key
        System.out.println("Can cache: yes (immutable)");
    }
}

Output:

textPerson 1: Person{name='John', age=34, birth=1990-05-15, certs=[Java, OOP]}
Person 2: Person{name='John', age=34, birth=1990-05-15, certs=[Java, OOP, Design Pattern]}
Person 1: Person{name='John', age=34, birth=1990-05-15, certs=[Java, OOP]}
Can cache: yes (immutable)

สรุป: Immutable Object & Safe Design

text┌──────────────────────────────────────────────┐
│  Immutable Object & Safe Class Design       │
├──────────────────────────────────────────────┤
│                                              │
│  IMMUTABLE OBJECT                           │
│  ├─ ไม่สามารถเปลี่ยนแปลงได้                 │
│  ├─ ทุก attributes private final            │
│  ├─ ไม่มี setter                            │
│  └─ Methods return new objects              │
│                                              │
│  DESIGN PRINCIPLES                          │
│  ├─ Principle of Least Privilege           │
│  ├─ Encapsulation                           │
│  ├─ Defense Against Mutation                │
│  ├─ Fail-Fast (throw exception early)      │
│  └─ Stateless Design                        │
│                                              │
│  SAFETY TECHNIQUES                          │
│  ├─ Input validation ใน constructor        │
│  ├─ Deep copy สำหรับ reference types       │
│  ├─ Return unmodifiable collections         │
│  ├─ Override equals() + hashCode()         │
│  └─ Final class (prevent extension)        │
│                                              │
│  BENEFITS                                    │
│  ├─ Thread-safe (no synchronization)       │
│  ├─ Simpler code                            │
│  ├─ Can use as HashMap key                  │
│  ├─ Easier testing                          │
│  └─ Memory optimization (caching)          │
│                                              │
│  EXAMPLES                                    │
│  ├─ String                                  │
│  ├─ Integer, Long, Double                  │
│  ├─ LocalDate, LocalTime                   │
│  └─ Custom: Money, Address, etc            │
│                                              │
└──────────────────────────────────────────────┘