โหมดมืด
บทที่ 1 — OOP ลึก
หลังจบบท คุณจะ:
- เข้าใจ 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 อยู่แล้ว)
}
}ข้อดี:
- Invariant —
balance >= 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 Collections —
ArrayList<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 คน:
- Erich Gamma (เอ-ริค กัม-ม่า)
- Richard Helm (ริ-ชาร์ด เฮล์ม)
- Ralph Johnson (ราล์ฟ จอห์น-สัน)
- 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(); // barkB. Parametric polymorphism (generics)
java
List<Integer> ints = ...;
List<String> strings = ...;
// Same List<T> code7. 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 อะไรก็ได้javaclass 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 คุยกับ:
- Field ของ A เอง
- Parameter ที่ส่งมา
- Object ที่ A สร้างใน method
- 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 referenceequals() = 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 field | Private + method |
| Setter ทุก field | Constructor + behavior methods |
| Anemic model (data class + service) | Rich domain model |
| Deep inheritance | Composition |
| Inherit just for code reuse | Composition + delegation |
| Static everywhere | Inject dependencies |
| Mutable object passed around | Immutable or defensive copy (สำเนาป้องกัน — คัดลอกข้อมูลก่อนรับเข้า/ส่งออก เพื่อกันคนนอกแก้ของภายใน) |
equals without hashCode | Both — or use record |
Chain .getX().getY().getZ() | Law of Demeter — wrapper method |
throw new UnsupportedOperationException ใน override | Wrong 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 insufficienttransfer(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