Skip to content

บทที่ 1 — OOP ลึก

← บทที่ 0 | สารบัญ | บทที่ 2 →

หลังจบบท คุณจะ:

  • เข้าใจ encapsulation (เอน-แค็พ-ซู-เล-ชั่น), inheritance (อิน-เฮอ-ริ-แทนซ์), polymorphism (พอ-ลี-มอร์-ฟิ-ซึม), abstraction (แอบ-สแตร็ก-ชั่น) ระดับลึก
  • รู้ว่าเมื่อไหร่ inheritance ทำให้แย่ — ใช้ composition แทน
  • ใช้ interface vs abstract class ถูกที่
  • เข้าใจ method overloading vs overriding
  • รู้ "Tell Don't Ask" + "Law of Demeter (ลอว์ ออฟ ดี-มี-เทอร์ — ตั้งชื่อตามเทพีดี-มี-เทอร์ในตำนานกรีก)"

🌏 language-agnostic note — บทนี้ใช้ Java เป็นภาษาตัวอย่างหลัก แต่หลักการ OOP ใช้ได้กับทุกภาษา OOP (C#, Python, TypeScript, Kotlin, ...) ถ้าคุณมาจากภาษาอื่น โฟกัสที่ concept ไม่ใช่ syntax


1. OOP คืออะไร (Recap)

"Object-Oriented Programming" = วิธีจัดระเบียบ code โดยใช้ object ที่รวม data + behavior

ก่อน OOP (procedural style — ใช้ Python เป็นตัวอย่างให้อ่านง่าย):

python
# data + function แยกกัน — data เป็นแค่ dict
user = {"name": "Alice", "age": 30}

def birthday(u):
    u["age"] = u["age"] + 1   # function ภายนอกแก้ field ของ user

birthday(user)   # ต้องส่ง user เข้าไป — user ไม่รู้จักตัวเอง

💡 ภาษา procedural เก่า ๆ (C, Pascal) ก็โครงเหมือนกัน — struct เก็บ field, function แยกอยู่ข้างนอก รับ struct* เป็น argument

OOP:

java
class User {
    private String name;
    private int age;
    
    public void birthday() {
        age = age + 1;       // object รู้จักข้อมูลของตัวเอง (เพิ่มอายุของตัวเองได้เลย)
    }
}

→ Data + behavior อยู่ด้วยกัน


2. 4 Pillars (4 เสาหลัก)

Pillarคำอ่านคืออะไร
Encapsulationเอน-แค็พ-ซู-เล-ชั่นซ่อน data + expose behavior
Inheritanceอิน-เฮอ-ริ-แทนซ์reuse + extend จาก class อื่น
Polymorphismพอ-ลี-มอร์-ฟิ-ซึมใช้ interface เดียวกัน — behavior ต่าง
Abstractionแอบ-สแตร็ก-ชั่นสนใจ "what" ไม่ใช่ "how"

3. Encapsulation — ซ่อน Internal

java
// ❌ Public field
public class Account {
    public BigDecimal balance;   // ใครก็เปลี่ยนได้
}

Account a = new Account();
a.balance = new BigDecimal("-1000");   // 😱 negative balance!
java
// ✅ Encapsulated (ใช้ BigDecimal — ห้ามใช้ double กับเงิน ดูบทที่ 0 §4D)
public class Account {
    private BigDecimal balance = BigDecimal.ZERO;   // ซ่อน

    public void deposit(BigDecimal amount) {
        if (amount.signum() <= 0) throw new IllegalArgumentException("Amount must be positive");
        balance = balance.add(amount);
    }

    public void withdraw(BigDecimal amount) {
        if (amount.signum() <= 0) throw new IllegalArgumentException("Amount must be positive");
        if (amount.compareTo(balance) > 0) throw new InsufficientFundsException();
        balance = balance.subtract(amount);
    }

    public BigDecimal getBalance() {
        return balance;   // read-only (BigDecimal เป็น immutable อยู่แล้ว)
    }
}

ข้อดี:

  • Invariantbalance >= 0 รักษาได้
  • Refactor ได้ — เปลี่ยน internal โดยไม่ break user
  • Validation ที่ entry point

⚠️ Getter/Setter อย่าใส่ทุก field

🔤 Java Bean (จาวา-บีน) = pattern เก่าใน Java ที่กำหนดว่า class ต้องมี (1) no-arg constructor (2) getter/setter ครบทุก field (3) implement Serializable — เกิดขึ้นยุค 1990s สำหรับ tooling (IDE/serializer) framework เก่า ๆ แต่ทำให้ encapsulation พังเพราะเปิด state ออกหมด ภาษาอื่นมี pattern คล้ายกัน (TypeScript/C# มี auto-properties)

java
// ❌ Java Bean style — เปิดทุกอย่าง
public class User {
    private String name;
    private int age;
    
    public void setName(String name) { this.name = name; }
    public String getName() { return name; }
    public void setAge(int age) { this.age = age; }
    public int getAge() { return age; }
}

// ใช้:
user.setAge(-5);   // 😱
user.setName(null);   // 😱

→ ไม่มี encapsulation จริง — เหมือน public field

java
// ✅ Expose action ไม่ใช่ data
public class User {
    private String name;
    private LocalDate birthday;
    
    public User(String name, LocalDate birthday) {
        Objects.requireNonNull(name);
        if (name.isBlank()) throw new IllegalArgumentException();
        this.name = name;
        this.birthday = birthday;
    }
    
    public int getAge(Clock clock) {
        // 💡 รับ Clock เป็น argument — pure function ขึ้น (ไม่พึ่ง LocalDate.now() ที่เปลี่ยนตามเวลา)
        // เทียบ: ถ้าใช้ LocalDate.now() ตรง ๆ จะ test ยาก (เพราะผลลัพธ์ขึ้นกับวันที่รัน)
        return Period.between(birthday, LocalDate.now(clock)).getYears();
    }
    
    public void rename(String newName) {
        Objects.requireNonNull(newName);
        this.name = newName;
    }
}

→ User control ว่า "ทำอะไรได้บ้าง" — ไม่ใช่ "ตั้งค่า field"


4. Inheritance — ใช้เมื่อไหร่

java
class Animal {
    protected String name;
    public void eat() { ... }
    public void sleep() { ... }
}

class Dog extends Animal {
    public void bark() { ... }
}

class Cat extends Animal {
    public void meow() { ... }
}

ดูดี — แต่ inheritance มี trap

Trap 1: Tight Coupling

java
class Bird {
    public void fly() { ... }
}

class Penguin extends Bird {    // ❌ Penguin ไม่บินได้!
    @Override
    public void fly() {
        throw new UnsupportedOperationException();
    }
}

→ ละเมิด Liskov Substitution Principle (ลิส-คอฟ ซับ-สติ-ทู-ชั่น พริน-ซิ-เพิล — ตั้งชื่อตาม Barbara Liskov (บาร์-บา-ร่า ลิส-คอฟ) นักวิทย์คอมพิวเตอร์ MIT ผู้คิดหลักนี้, Turing Award 2008)

LSP สั้น ๆ: ทุกที่ที่ใช้ Bird ได้ ต้องเอา Penguin ไปแทนได้ โดยโค้ดเดิมไม่พัง — แต่ Penguin.fly() โยน exception → ใครเรียก Bird.fly() แล้วเจอ Penguin ก็พัง → ผิด LSP

เรียนเต็มในบทที่ 2 SOLID

Trap 2: Deep Hierarchy

เปลี่ยน Mammal → break Dog + Hunting + Retriever + Golden
หา bug ก็ต้องไล่ class แม่

Trap 3: ใช้ Inheritance แทน Composition

📖 คำศัพท์ Java CollectionsArrayList<T> = list ที่ขยายขนาดได้ (เหมือน array แต่เพิ่ม/ลบได้); Stack = โครงสร้าง LIFO (last-in-first-out) ที่มีแค่ push/pop — ภาษาอื่นมีเทียบ: Python = list, JS = Array

⚠️ ตั้งชื่อ MyStack เพื่อไม่ชนกับ java.util.Stack ที่มีอยู่จริงใน Java standard library

java
class MyStack<T> extends ArrayList<T> {     // ❌
    public void push(T x) { add(x); }
    public T pop() { return remove(size() - 1); }
}

MyStack<Integer> s = new MyStack<>();
s.push(1);
s.push(2);
s.add(0, 99);    // 😱 add ที่ index 0 — Stack ไม่ควรมี!

→ extend = inherit ทุก method — รวมที่ไม่ควรเปิด

java
// ✅ Composition
class MyStack<T> {
    private final List<T> items = new ArrayList<>();
    
    public void push(T x) { items.add(x); }
    public T pop() { return items.remove(items.size() - 1); }
    public int size() { return items.size(); }
    // ไม่ expose add(index, x)
}

5. "Composition over Inheritance"

java
// ❌ Inheritance
class Bird {
    public void fly() { ... }
}
class Duck extends Bird { ... }
class Penguin extends Bird {
    @Override public void fly() { throw new UnsupportedOperationException(); }
}

// ✅ Composition with strategy
interface FlyBehavior {
    void fly();
}

class CanFly implements FlyBehavior {
    public void fly() { System.out.println("Flying!"); }
}

class CantFly implements FlyBehavior {
    public void fly() { /* no-op */ }
}

class Bird {
    private FlyBehavior flyBehavior;
    public Bird(FlyBehavior fb) { this.flyBehavior = fb; }
    public void fly() { flyBehavior.fly(); }
}

// ใช้
Bird duck = new Bird(new CanFly());
Bird penguin = new Bird(new CantFly());

// เปลี่ยน behavior ตอน runtime
duck = new Bird(new CantFly());   // duck broken wing

ข้อดี:

  • Flexible — เปลี่ยน behavior runtime
  • No artificial hierarchy
  • Easier to test (inject mock)
  • Multiple capabilities ผสมได้

"Favor object composition over class inheritance." — Gang of Four (แก๊ง ออฟ โฟร์ = แก๊งสี่คน), Design Patterns, 1994 (Favor (เฟ-เวอร์) = นิยม/เลือก — จงเลือกการประกอบ object มากกว่าการสืบทอด class)

💡 Gang of Four (GoF) = ชื่อเล่นของผู้เขียนหนังสือ Design Patterns: Elements of Reusable Object-Oriented Software (1994) 4 คน:

  1. Erich Gamma (เอ-ริค กัม-ม่า)
  2. Richard Helm (ริ-ชาร์ด เฮล์ม)
  3. Ralph Johnson (ราล์ฟ จอห์น-สัน)
  4. John Vlissides (จอห์น วลิส-สิ-เดส)

หนังสือเล่มนี้เป็นตำราคลาสสิกที่บัญญัติคำว่า "design pattern" ในวงการ — GoF เป็นคำเรียกย่อในวงการที่ใช้กันมาก กฎ "favor composition over inheritance" ไม่ใช่ "quote" ของพวกเขาตรง ๆ แต่เป็น guideline ในบทที่ 1 ของเล่ม

กฎ — เมื่อไหร่ใช้ Inheritance

✅ ใช้ Inheritance เมื่อ:

  • "is-a" relationship จริง — Dog is-a Animal
  • Behavior 100% shared — ไม่มี method ที่ override กลายเป็น no-op
  • Stable hierarchy — ไม่เปลี่ยนบ่อย
  • Liskov compliant — subclass ใช้แทน parent ได้

❌ อย่าใช้ Inheritance เมื่อ:

  • เพื่อ reuse code (ใช้ composition)
  • "has-a" relationship (User has-a Address — ไม่ใช่ extends)
  • จะ override method เป็น no-op
  • Hierarchy ลึก > 3 level

Real Example: Service ที่ดี

📖 คำศัพท์ Java/Spring (ถ้าไม่คุ้น Spring/JPA ก็เข้าใจตาม comment ได้):

  • EntityManager (เอน-ทิ-ตี้ แมน-เน-เจอร์) = วัตถุของ JPA ที่ใช้คุยกับ database (em.persist(x) = save object x ลง DB)
  • Logger (ล็อก-เกอร์) = วัตถุที่ใช้บันทึก log
java
// ❌ Inherit BaseService — เหตุผล "เพื่อ reuse"
abstract class BaseService {
    protected EntityManager em;   // tool คุยกับ DB
    protected Logger log;          // tool บันทึก log
    public void save(Object o) { em.persist(o); }
    public void log(String msg) { log.info(msg); }
}

class UserService extends BaseService { ... }
class OrderService extends BaseService { ... }

// ✅ Composition
class UserService {
    private final EntityManager em;
    private final Logger log;
    
    public UserService(EntityManager em, Logger log) {
        this.em = em;
        this.log = log;
    }
    
    public void create(User u) {
        log.info("Creating user: " + u);
        em.persist(u);
    }
}

6. Polymorphism

"Many forms" — ใช้ interface เดียวกัน — implementation ต่าง

java
interface PaymentProcessor {
    PaymentResult charge(BigDecimal amount, Card card);
}

class StripeProcessor implements PaymentProcessor { ... }
class PayPalProcessor implements PaymentProcessor { ... }
class CryptoProcessor implements PaymentProcessor { ... }

// ใช้
PaymentProcessor processor = chooseProcessor(user);
PaymentResult result = processor.charge(amount, card);

→ Code ที่ใช้ PaymentProcessor ไม่รู้ ว่าเป็น Stripe/PayPal/Crypto
→ เพิ่ม BankProcessor ใหม่ → ไม่ต้องแก้ code เก่า

2 Types

A. Subtype polymorphism (most common)

java
Animal a = new Dog();
a.makeSound();   // bark

B. Parametric polymorphism (generics)

java
List<Integer> ints = ...;
List<String> strings = ...;
// Same List<T> code

7. Abstraction — Interface vs Abstract Class

Interface — "what"

java
interface Vehicle {
    void start();
    void stop();
    int getSpeed();
}
  • No state (no field — ส่วนใหญ่)
  • Pure contract
  • Multiple implement ได้

Abstract Class — "what + partial how"

📖 Java Generics (เจ-เน-ริกส์) เร็ว ๆ — <T> คือ "type parameter" = ตัวแทนของ type ที่ยังไม่ระบุ ใช้บอกว่า class นี้ทำงานกับ type อะไรก็ได้

java
class Box<T> {           // T = type ที่จะระบุภายหลัง
    T value;
}
Box<String> b1 = new Box<>();   // T = String
Box<Integer> b2 = new Box<>();  // T = Integer

เทียบภาษาอื่น: TypeScript = Box<T>, C# = Box<T>, Python = Box[T] (3.12+)

ตัวอย่างข้างล่างใช้ <T, ID> 2 ตัว = type ของ entity + type ของ id

java
abstract class AbstractRepository<T, ID> {
    protected final EntityManager em;   // tool คุยกับ DB (JPA)
    
    public T findById(ID id) {
        return em.find(getEntityClass(), id);   // shared
    }
    
    public void save(T entity) {
        em.persist(entity);   // shared
    }
    
    protected abstract Class<T> getEntityClass();   // subclass implement
}

class UserRepository extends AbstractRepository<User, Long> {
    @Override
    protected Class<User> getEntityClass() { return User.class; }
}
  • มี state + method body ได้
  • Subclass extend ได้ ตัวเดียว
  • ใช้เมื่อต้องการ share code ระหว่าง subclass

🚀 2026 modern Spring tip — ในโปรเจ็กต์ Spring สมัยใหม่มักไม่ต้องเขียน AbstractRepository เอง ใช้ Spring Data JpaRepository<T, ID> interface แทน — Spring สร้าง implementation ให้อัตโนมัติ (ดูบทที่ 6 + Java book)

เลือกยังไง?

Java 8+ — Interface Default Method

java
interface Greeter {
    String greet(String name);
    
    default String greetLoudly(String name) {
        return greet(name).toUpperCase() + "!!!";
    }
}

class EnglishGreeter implements Greeter {
    public String greet(String name) { return "Hello, " + name; }
    // greetLoudly inherited
}

→ Interface ใกล้ abstract class — แต่ยัง multiple-implement ได้


8. Method Overloading vs Overriding

Overloading — same name, different signature

java
class Calculator {
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
    int add(int a, int b, int c) { return a + b + c; }
}
  • Compile-time decision (static dispatch — สแต-ติก ดิส-แพทช์ = "เลือกว่าจะเรียก method ตัวไหน" ตั้งแต่ตอน compile โดยดูจากชนิดของ argument)
  • ภายใน 1 class

Overriding — subclass แทน parent

java
class Animal {
    public String sound() { return "..."; }
}

class Dog extends Animal {
    @Override
    public String sound() { return "Woof"; }
}

Animal a = new Dog();
a.sound();   // "Woof" — runtime decision (dynamic dispatch — ได-นา-มิก ดิส-แพทช์)

🔤 dispatch (ดิส-แพทช์) = การส่งงาน/เลือกเรียก dynamic dispatch (ได-นา-มิก ดิส-แพทช์) = "เลือกว่าจะเรียก method ตัวไหน" ตอนโปรแกรมทำงานจริง (runtime) โดยดูจากชนิดจริงของ object — ต่างจาก static dispatch ที่ตัดสินตั้งแต่ตอน compile

📊 เห็นภาพ:

static dispatch:                   dynamic dispatch:
  compile time → ตัดสินแล้ว           compile time → ยังไม่ตัดสิน
  ดูชนิด argument                    runtime → ดูชนิดจริงของ object
  เร็วกว่า (no lookup)               ช้ากว่านิดหน่อย (vtable lookup)
  เปลี่ยน behavior ไม่ได้             เปลี่ยน behavior ผ่าน subclass ได้ → polymorphism
  • Runtime decision — based on actual object type
  • เป็น polymorphism ในใช้งาน

⚠️ Use @Override

java
class Dog extends Animal {
    @Override
    public String sound() { return "Woof"; }   // ✅ compiler check spelling
}

class Cat extends Animal {
    public String soundd() { return "Meow"; }   // ❌ typo — silent bug
}

@Override = compiler verify ว่ามี method นี้ใน parent — ลด typo bug


9. "Tell, Don't Ask"

java
// ❌ Ask + decide ภายนอก
if (order.getStatus() == Status.PENDING && order.getCreatedAt().isBefore(thirtyDaysAgo)) {
    order.setStatus(Status.EXPIRED);
}

// ✅ Tell
order.expireIfStale(thirtyDaysAgo);

// ภายใน Order
public void expireIfStale(LocalDateTime threshold) {
    if (status == Status.PENDING && createdAt.isBefore(threshold)) {
        status = Status.EXPIRED;
    }
}

→ Object รู้พฤติกรรมตัวเอง — caller ไม่ต้องรู้ internal

ข้อดี:

  • Logic อยู่ใน object ที่เกี่ยวข้อง
  • Encapsulation จริง
  • Test ง่ายขึ้น (test Order ตรง ๆ)

10. Law of Demeter — "Don't Talk to Strangers" (อย่าคุยกับคนแปลกหน้า)

🔤 Law of Demeter (ลอว์ ออฟ ดี-มี-เทอร์) — ตั้งชื่อตาม Demeter เทพีแห่งการเกษตรในตำนานกรีก (เป็นชื่อ research project ของ Northeastern University ปี 1987) — บางที่เรียก LoD (แอล-โอ-ดี)

"Don't Talk to Strangers" (โดนท์ ทอล์ค ทู สเตรน-เจอร์ส = "อย่าคุยกับคนแปลกหน้า") — สำนวนนี้พ่อแม่ฝรั่งใช้สอนเด็กเรื่องความปลอดภัย — ในที่นี้หมายถึง อย่าเรียก method ของ object ที่ไม่ใช่เพื่อนตรง ๆ ของเรา

java
// ❌ Chain ลึก — รู้ internal มากเกิน
// (สมมุติ Order ของระบบ e-commerce ที่เราออกแบบ — มี Customer, Customer มี Address, ฯลฯ)
order.getCustomer().getAddress().getCity().getCountry().getCode()

// "Don't talk to strangers"
// order รู้จัก customer (เพื่อน) แต่ไม่ควรรู้ city > country > code (เพื่อนของเพื่อนของเพื่อน)
java
// ✅ Method ที่จะใช้
order.getShippingCountryCode()

// ภายใน Order
public String getShippingCountryCode() {
    return customer.getCountryCode();
}

// ภายใน Customer
public String getCountryCode() {
    return address.getCountryCode();
}

หรือ:

java
// ✅ Pass argument directly
class Order {
    void ship(ShippingService service) {
        service.ship(this, customer.getShippingAddress());
    }
}

กฎ

Method ของ class A คุยกับ:

  1. Field ของ A เอง
  2. Parameter ที่ส่งมา
  3. Object ที่ A สร้างใน method
  4. Direct components ของ A

ไม่คุยกับ "friends of friends" (เฟรนด์ส ออฟ เฟรนด์ส = เพื่อนของเพื่อน — object ที่เราเข้าถึงผ่าน object อื่นต่อ ๆ ไป)

⚠️ อย่ายึดเป็น dogma (ด็อก-ม่า = หลักศาสนา/ความเชื่อตายตัว) — มี exception ที่สำคัญ:

  • DTOs / Data holders / API responses (เช่น JSON parse จาก API) — chain .getX().getY() ถือเป็นเรื่องปกติเพราะวัตถุพวกนี้เป็น "data structure" ไม่ใช่ "object ที่มี behavior"
  • Fluent builders (เช่น StringBuilder, Stream API) — ออกแบบมาให้ chain ได้โดยเจตนา

11. Static vs Instance

java
class MathUtil {
    public static int square(int x) {     // ไม่มี state
        return x * x;
    }
}

MathUtil.square(5);   // 25 — เรียกผ่าน class

ใช้ static เมื่อ:

  • Pure function (no state)
  • Utility / helper
  • Factory method

⚠️ Static ทำ unit test ยาก:

  • Mock ลำบาก — ต้องใช้ static mocking (Mockito 3.4+ ปี 2020 มี mockStatic() built-in แล้ว — เครื่องมือเก่า PowerMock ถูกแทนที่ไปแล้ว) → ดูบท testing
  • Hidden dependency = ทุกที่ที่ static เรียก dependency ลับ มองจาก signature ไม่เห็น

📝 Mock = วัตถุปลอมที่สวมรอย object จริงในการทดสอบ — รายละเอียดดูบท testing

java
// ❌ Hard to test
class OrderService {
    public void process(Order o) {
        EmailUtil.send(o.getEmail(), ...);   // static — hard to mock
    }
}

// ✅ Inject
class OrderService {
    private final EmailService emailService;
    
    public OrderService(EmailService emailService) {
        this.emailService = emailService;
    }
}

12. Immutable Objects

java
public final class Money {
    private final BigDecimal amount;
    private final Currency currency;
    
    public Money(BigDecimal amount, Currency currency) {
        this.amount = Objects.requireNonNull(amount);
        this.currency = Objects.requireNonNull(currency);
    }
    
    public Money add(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch");
        }
        return new Money(amount.add(other.amount), currency);   // return ใหม่
    }
    
    public BigDecimal getAmount() { return amount; }
    public Currency getCurrency() { return currency; }
    // no setters
}

ข้อดี

  • Thread-safe — shared ระหว่าง thread ได้ฟรี
  • Predictable — ไม่เปลี่ยนตอนเรา hold reference
  • Hash-able — ใช้เป็น HashMap key
  • Easy to test

Java 14+ — Record

📖 Java record = syntax สำหรับสร้าง immutable data class (มาตั้งแต่ Java 14, stable ใน 16) — Java สร้าง constructor, getter, equals, hashCode, toString ให้อัตโนมัติ

Compact constructor (คอม-แพ็ก-ต์ คอน-สตรัก-เตอร์) = public Money { ... } (ไม่มี (...)) ใช้ใส่ validation ก่อน assign field — Java จะ assign ให้เองตอนจบ

เทียบภาษาอื่น: Kotlin data class, C# record, TypeScript readonly class

java
public record Money(BigDecimal amount, Currency currency) {
    public Money {                  // compact constructor — validation ก่อน assign
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);
    }
    
    public Money add(Money other) {
        return new Money(amount.add(other.amount), currency);
    }
}

→ Record = immutable data class — ใส่ amount + currency จบ


13. Object Equality

java
String s1 = new String("hello");
String s2 = new String("hello");

s1 == s2          // false — different objects
s1.equals(s2)     // true — same content

== = same object reference
equals() = logical equality (override ได้)

Override equals + hashCode

java
class Point {
    final int x, y;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false;
        return x == p.x && y == p.y;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}

กฎ: ถ้า override equalsต้อง override hashCode (สำหรับ HashMap/HashSet)

record ทำให้ฟรี


14. Real-world Example — Order System

มาดู OOP ที่ดีกับไม่ดีในเคสจริง — anemic domain model (anemic อ่านว่า "อะ-นี-มิก" = โลหิตจาง — โมเดลโดเมนแบบโลหิตจาง: class เก็บแค่ field ไม่มี behavior, logic ไปกองอยู่ที่ service หมด) เป็น anti-pattern (อ่านว่า "แอน-ติ-แพ็ท-เทิร์น" = รูปแบบลวง/รูปแบบที่ดูเหมือนดีแต่จริง ๆ แย่) ที่เจอบ่อย ส่วน rich domain model (ริช โด-เมน โม-เดล = โมเดลโดเมนแบบสมบูรณ์: ใส่ behavior ไว้ใน object เอง) คือ OOP ที่แท้จริง ตัวอย่างนี้เทียบสองแบบให้เห็นชัด:

java
// ❌ Anemic Domain Model — class แค่เก็บ field
class Order {
    private Long id;
    private List<OrderItem> items;
    private OrderStatus status;
    // getter + setter ทุก field
}

// Business logic in Service
class OrderService {
    public void confirm(Order order) {
        if (order.getItems().isEmpty()) throw new ...;
        if (order.getStatus() != PENDING) throw new ...;
        order.setStatus(CONFIRMED);
    }
    
    public BigDecimal calculateTotal(Order order) {
        return order.getItems().stream()
            .map(i -> i.getPrice().multiply(BigDecimal.valueOf(i.getQuantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}
java
// ✅ Rich Domain Model
class Order {
    private final Long id;
    private final List<OrderItem> items;
    private OrderStatus status;
    
    public Order(Long id, List<OrderItem> items) {
        if (items.isEmpty()) throw new IllegalArgumentException();
        this.id = id;
        this.items = List.copyOf(items);
        this.status = OrderStatus.PENDING;
    }
    
    public void confirm() {
        if (status != OrderStatus.PENDING) {
            throw new IllegalStateException("Already confirmed or cancelled");
        }
        this.status = OrderStatus.CONFIRMED;
    }
    
    public BigDecimal total() {
        return items.stream()
            .map(OrderItem::subtotal)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    public List<OrderItem> items() {
        return Collections.unmodifiableList(items);
    }
    
    public OrderStatus status() { return status; }
}

class OrderItem {
    private final Product product;
    private final int quantity;
    
    public BigDecimal subtotal() {
        return product.price().multiply(BigDecimal.valueOf(quantity));
    }
}

→ Order "knows" how to confirm itself + calculate total
→ State transition valid เสมอ — encapsulation จริง


15. ⚠️ Common Pitfalls

ปิดท้ายบท OOP ด้วยกับดักที่เจอบ่อย — public field, setter ทุก field, anemic model, inheritance ลึก, static เต็มไปหมด ตารางจับคู่ "ผิด" กับ "ถูก" ใช้เป็น checklist ตรวจว่าโค้ดเรา OOP ดีจริงไหม:

Public fieldPrivate + method
Setter ทุก fieldConstructor + behavior methods
Anemic model (data class + service)Rich domain model
Deep inheritanceComposition
Inherit just for code reuseComposition + delegation
Static everywhereInject dependencies
Mutable object passed aroundImmutable or defensive copy (สำเนาป้องกัน — คัดลอกข้อมูลก่อนรับเข้า/ส่งออก เพื่อกันคนนอกแก้ของภายใน)
equals without hashCodeBoth — or use record
Chain .getX().getY().getZ()Law of Demeter — wrapper method
throw new UnsupportedOperationException ใน overrideWrong inheritance — composition

16. Checkpoint

🛠️ Checkpoint 1.1 — Refactor Bag of Setters

java
class User {
    String name; int age; String email;
    // all getters + setters
}
  • Force valid construction
  • Add behavior methods (changeEmail, birthday)
  • ไม่มี setter

🛠️ Checkpoint 1.2 — Composition vs Inheritance
Design MediaPlayer:

  • Plays audio, video, both
  • Has volume, brightness (video only)
  • ทำเป็น inheritance — แล้ว refactor เป็น composition + strategy

🛠️ Checkpoint 1.3 — Rich Domain Model
สร้าง BankAccount:

  • deposit(amount)
  • withdraw(amount) — throw if insufficient
  • transfer(to, amount)
  • Track transaction history (read-only list)
  • Immutable transaction object (record)

🛠️ Checkpoint 1.4 — Tell Don't Ask
ดูตัวอย่าง:

java
if (cart.getItems().size() > 10) {
    cart.setDiscount(0.10);
}

Refactor เป็น "tell"


17. สรุปบท

OOP = data + behavior ใน object
Encapsulation = private field + behavior methods (ไม่ใช่ getter/setter ทุก field)
Inheritance ใช้เมื่อ "is-a" จริง + Liskov compliant — อย่าใช้เพื่อ reuse
Composition > Inheritance — flexible, testable, no artificial hierarchy
Polymorphism — same interface, different behavior
Interface (contract) vs Abstract class (shared impl)
✅ Java 8+ default method — interface ใกล้ abstract class
@Override annotation — compiler check
Tell, Don't Ask — object รู้พฤติกรรมตัวเอง
Law of Demeter — ไม่คุยกับ "friends of friends"
Immutable (record) > mutable — thread-safe + predictable
Rich Domain Model > Anemic — business logic อยู่ใน entity


← บทที่ 0 | บทที่ 2 → SOLID Principles