โหมดมืด
บทที่ 2 — SOLID Principles
SOLID = 5 หลัก OOP design ที่ทำให้ code maintainable + extensible
🔤 คำอ่านชื่อหลัก (สำหรับคนไทย):
- Single Responsibility (ซิง-เกิล รี-สปอน-ซิ-บิ-ลิ-ตี้) = ความรับผิดชอบเดียว
- Open/Closed (โอ-เพ่น/โคลส-ด) = เปิด/ปิด
- Liskov Substitution (ลิส-คอฟ ซับ-สติ-ทู-ชั่น) = การแทนที่แบบลิสคอฟ (ตั้งตามชื่อ Barbara Liskov ผู้เสนอแนวคิด ปี 1987)
- Interface Segregation (อิน-เทอร์-เฟส เซก-รี-เกชั่น) = การแยกอินเทอร์เฟส
- Dependency Inversion (ดี-เพน-เดน-ซี อิน-เวอร์-ชั่น) = การกลับทิศ dependency
| Letter | Principle | คำอ่าน |
|---|---|---|
| S | Single Responsibility | ซิง-เกิล รี-สปอน-ซิ-บิ-ลิ-ตี้ |
| O | Open/Closed | โอ-เพ่น/โคลส-ด |
| L | Liskov Substitution | ลิส-คอฟ ซับ-สติ-ทู-ชั่น |
| I | Interface Segregation | อิน-เทอร์-เฟส เซก-รี-เกชั่น |
| D | Dependency Inversion | ดี-เพน-เดน-ซี อิน-เวอร์-ชั่น |
หลังจบบท คุณจะ:
- รู้ทุกหลัก SOLID + ใช้ตัดสินใจออกแบบได้
- เห็นว่า SOLID ไม่ใช่ dogma — มี trade-off
- รู้ว่าเมื่อไหร่ ไม่ต้อง follow SOLID
1. SRP — Single Responsibility Principle
"A class should have one reason to change." (1 class ควรมี "เหตุผลที่ต้องแก้" เพียงเหตุผลเดียว)
📌 คำชี้แจงสำคัญจาก Robert Martin (โร-เบิร์ต มาร์-ติน หรือ "Uncle Bob" อันเคิล บ็อบ) ปี 2017: "reason to change" ไม่ได้แปลว่า "1 class ทำได้แค่ 1 อย่าง" แบบตรงตัว — แต่หมายถึง "1 stakeholder/actor" (ผู้มีส่วนได้ส่วนเสีย 1 กลุ่ม)
ตัวอย่าง: ถ้า
Employeeclass มี methodcalculatePay()(HR เป็นเจ้าของ logic) +reportHours()(Accounting เป็นเจ้าของ) +save()(DBA เป็นเจ้าของ) — มี 3 actors ที่อาจมาขอแก้คนละทิศคนละทาง → ละเมิด SRP เพราะ "เหตุผลที่ต้องแก้" มาจาก 3 กลุ่ม(อ้างอิง: Robert Martin, Module Architecture talk 2017 — ในหนังสือ Clean Architecture บทที่ 7)
java
// ❌ ทำหลายอย่าง — มีเหตุผลหลายอย่างที่ต้องเปลี่ยน
class User {
private String name;
private String email;
public void save() {
// เปิดการเชื่อมต่อ DB แล้ว save (ตัวอย่าง JDBC แบบดิบ ๆ — ในงานจริงใช้ repository pattern)
// Connection conn = DriverManager.getConnection(...);
// save to DB
}
public void sendWelcomeEmail() {
// send email
}
public String toJson() {
// serialize
}
public boolean validate() {
// validate
}
}เหตุผลที่ต้องเปลี่ยน User:
- เปลี่ยน DB schema → save()
- เปลี่ยน email format → sendWelcomeEmail()
- เปลี่ยน JSON format → toJson()
- เพิ่ม validation rule → validate()
→ 4 reasons = ละเมิด SRP
java
// ✅ Split responsibility
class User {
private String name;
private String email;
// domain behavior + state
public boolean isValid() { ... }
}
class UserRepository {
public void save(User user) { ... }
}
class WelcomeEmailService {
public void send(User user) { ... }
}
class UserJsonSerializer {
public String toJson(User user) { ... }
}→ แต่ละ class มี 1 reason to change
⚠️ Trap — Over-split
UserCreator + UserUpdater + UserDeleter + UserFinder + ...ทุก method = class → boilerplate ระเบิด
กฎ: SRP = group by reason to change (= stakeholder) ไม่ใช่ "1 class = 1 method"
ตัวอย่าง — เหตุผลที่อาจเปลี่ยน
| Class | Reason |
|---|---|
UserRepository | DB schema เปลี่ยน |
UserApiController | API contract เปลี่ยน |
UserService | Business rule เปลี่ยน |
User | Domain model เปลี่ยน |
→ 4 stakeholder ต่างกัน → 4 class
2. OCP — Open/Closed Principle
"Software entities should be open for extension (เอ็กซ์-เทน-ชั่น = ขยาย), but closed for modification (โม-ดิ-ฟิ-เค-ชั่น = แก้ไข)." (โค้ดควร "เปิด" ให้เพิ่มความสามารถใหม่ได้ แต่ "ปิด" ไม่ต้องไปแก้ของเดิม)
📚 ที่มา (สำหรับคนสนใจประวัติ):
- Bertrand Meyer (เบอร์-ทรานด์ ไม-เออร์) ปี 1988 เป็นคนเสนอครั้งแรกในหนังสือ Object-Oriented Software Construction — เน้นใช้ inheritance เป็นเครื่องมือหลัก
- Robert Martin (Uncle Bob) ปี 2014 ปรับนิยามใหม่ให้ใช้ polymorphism (โพ-ลี-มอร์-ฟิ-ซึ่ม) ผ่าน interface เป็นหลัก (ไม่ผูกกับ inheritance) — ที่บทนี้สอนคือแบบของ Uncle Bob
java
// ❌ ต้อง modify เมื่อเพิ่ม payment method
class PaymentService {
public void process(String type, BigDecimal amount) {
if (type.equals("STRIPE")) {
// Stripe logic
} else if (type.equals("PAYPAL")) {
// PayPal logic
} else if (type.equals("CRYPTO")) { // ← เพิ่มต้อง modify class
// Crypto logic
}
}
}เพิ่ม "Bank Transfer" → ต้องแก้ process() → risk break Stripe/PayPal
java
// ✅ OCP — เพิ่มโดยไม่ต้องแก้ของเดิม
interface PaymentProcessor {
void process(BigDecimal amount);
}
class StripeProcessor implements PaymentProcessor { ... }
class PayPalProcessor implements PaymentProcessor { ... }
class CryptoProcessor implements PaymentProcessor { ... }
class BankTransferProcessor implements PaymentProcessor { ... } // ใหม่ — ไม่แตะของเก่า
class PaymentService {
private final Map<String, PaymentProcessor> processors;
public PaymentService(Map<String, PaymentProcessor> processors) {
this.processors = processors;
}
public void process(String type, BigDecimal amount) {
PaymentProcessor processor = processors.get(type);
if (processor == null) throw new IllegalArgumentException();
processor.process(amount);
}
}→ Spring inject ทุก PaymentProcessor → เพิ่ม class ใหม่ = ทำงาน
🛡️ เกร็ด safety (qa): ใน production ควรใช้
Map.copyOf(processors)ใน constructor เพื่อให้ map เป็น immutable — ป้องกัน caller (หรือ Spring proxy บางแบบ) มาแก้ map ทีหลังจนเกิด race condition
และควร log ถึงtypeที่ไม่รู้จักก่อน throw + ไม่ต้องเช็คnullซ้ำหลังเช็คแล้ว (processorที่ผ่านif (processor == null) throwมาแล้วยังไงprocessor.process(...)ก็ปลอดภัย)
📌 กล่อง: Spring จัดการ
Map<String, PaymentProcessor>ยังไง?เมื่อ inject
Map<String, T>Spring จะใช้ bean name เป็น key:java@Component("stripe") // ← key = "stripe" class StripeProcessor implements PaymentProcessor { ... } @Component("paypal") class PayPalProcessor implements PaymentProcessor { ... }เรียกใช้:
processors.get("stripe")— ถ้า key ไม่ตรง bean name จะได้nullทางเลือกที่ปลอดภัยกว่า — enum key (compile-time check):
javaenum PaymentType { STRIPE, PAYPAL, CRYPTO } class PaymentService { private final Map<PaymentType, PaymentProcessor> processors; // สร้าง map จาก List<PaymentProcessor> ที่ inject + ดู getType() ของแต่ละตัว }ใช้ enum ดีกว่าเพราะ typo ไม่หลุดถึง runtime
🆕 ถ้ายังไม่เคยใช้ Spring DI: Spring จะดูชนิดของ parameter ใน constructor (เช่น
Map<String, PaymentProcessor>) แล้ว "หา bean ที่ตรง" มาส่งให้อัตโนมัติ — เราไม่ต้องnew StripeProcessor()เอง Spring เก็บ bean ทุกตัวที่ implementPaymentProcessorไว้ใน container แล้วใส่ใน map ให้ครบ ดูรายละเอียดบทที่ 16 (Spring Boot Fundamentals)
Strategy Pattern = วิธี implement OCP
ดู Strategy ใน design patterns บทที่ 3
⚠️ Trap — Premature OCP
java
// ❌ Over-engineer ตั้งแต่แรก "เผื่ออนาคต"
interface UserNameFormatter { ... }
class StandardUserNameFormatter { ... }
class UppercaseUserNameFormatter { ... }
class ReversedUserNameFormatter { ... }→ YAGNI — ยังไม่ต้องการ — รอเห็น pattern ซ้ำ 2-3 ครั้ง ค่อย OCP
3. LSP — Liskov Substitution Principle
"Subtypes must be substitutable for their base types." (class ลูกต้องเอาไปใช้ "แทนที่" class แม่ได้ โดยโปรแกรมยังทำงานถูกต้องเหมือนเดิม)
📚 ที่มา: Barbara Liskov (บาร์-บา-รา ลิส-คอฟ) นักวิจัย MIT (ผู้ได้รับ Turing Award ปี 2008) เสนอหลักนี้ใน keynote ของงาน OOPSLA ปี 1987 ชื่อตอน "Data Abstraction and Hierarchy" —
Liskovออกเสียง "ลิส-คอฟ" (เน้นพยางค์แรก ไม่ใช่ "ลิส-คอฟฟ์")
ตัวอย่างง่าย ๆ ของ "substitutable": ถ้ามี method ที่รับ List<Bird> แล้วเรียก .eat() — เราเอา Eagle, Sparrow, หรือ Penguin ใส่ไปได้หมดโดยไม่ต้องเช็คชนิด เพราะทุกตัว "เป็นนก" ที่ eat() ได้ → substitutable
แต่ถ้าเอา Penguin แล้ว method ดันเรียก .fly() แล้ว Penguin throw exception → ไม่ substitutable = ละเมิด LSP
Subclass ใช้แทน parent ได้โดย behavior ไม่เปลี่ยน (caller ไม่รู้)
❌ ละเมิด (classic example — Rectangle/Square)
💡 หมายเหตุ: ตัวอย่างนี้เป็น "classroom example" ที่ใช้สอน LSP มาตั้งแต่ยุค 90 — ปัญหารากจริง ๆ คือ mutability (มี setter เปลี่ยน state ได้) ตัวอย่างที่เจอใน production จริงดูในหัวข้อ "LSP violations enterprise" ด้านล่าง
java
class Rectangle {
protected int width;
protected int height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int area() { return width * height; }
}
class Square extends Rectangle {
@Override
public void setWidth(int w) {
width = w;
height = w; // ⭐ Square — width = height
}
@Override
public void setHeight(int h) {
width = h;
height = h;
}
}
// Test
void test(Rectangle r) {
r.setWidth(5);
r.setHeight(10);
// ใช้ if/throw แทน assert เพราะ Java ปิด assert ตอนรัน production by default
if (r.area() != 50) {
throw new AssertionError("area expected 50 got " + r.area());
}
// ✅ Rectangle → area = 50
// ❌ Square → area = 100! (เพราะ setHeight ไป overwrite width)
}→ Square ไม่ substitutable สำหรับ Rectangle → ละเมิด LSP
Math: Square is-a Rectangle
Code: Square ≠ substitutable
แก้:
- ไม่ใช้ inheritance — Square + Rectangle = 2 class แยก หรือ
- ทำให้ immutable ทั้งคู่ (ไม่มี setter) — สร้าง shape ใหม่แทนที่จะแก้ — ปัญหา LSP จะหายไปทันที
❌ Penguin extends Bird
java
class Bird { void fly() { ... } }
class Penguin extends Bird {
@Override
void fly() { throw new UnsupportedOperationException(); }
}
// Code ที่ใช้ Bird
List<Bird> birds = ...;
birds.forEach(Bird::fly); // 💥 Penguin throw!แก้:
java
interface Bird { void eat(); }
interface FlyingBird extends Bird { void fly(); }
class Eagle implements FlyingBird { ... }
class Penguin implements Bird { ... } // ไม่ implement FlyingBirdกฎ LSP
🔤 กางศัพท์ (พร้อมคำอ่าน):
- precondition (พรี-คอน-ดิ-ชั่น) = เงื่อนไขก่อนเรียก — สิ่งที่ caller ต้องเตรียมให้ครบก่อนเรียก method
- ตัวอย่าง:
withdraw(amount)มี precondition ว่าamount > 0(caller ต้องส่งค่าบวกมา)- postcondition (โพสต์-คอน-ดิ-ชั่น) = เงื่อนไขหลังเรียก — สิ่งที่ method รับประกันว่าจะเป็นจริงหลังทำงานเสร็จ
- ตัวอย่าง:
withdraw()รับประกันว่าหลังเรียกbalanceจะลดลงเท่ากับamount- invariant (อิน-แวร์-เรียนต์) = เงื่อนไขคงที่ — สิ่งที่ต้องเป็นจริงตลอดเวลา ไม่ว่า method ไหนจะเรียก
- ตัวอย่าง:
Accountมี invariant ว่าbalance >= 0เสมอ (ไม่มีทางติดลบ)
Subclass ห้าม:
- Strengthen precondition — ขอ (require) มากกว่า parent (เช่น parent รับ amount > 0 แต่ลูกบังคับ amount > 100)
- Weaken postcondition — รับประกัน (guarantee) น้อยกว่า parent
- Throw exception ใหม่ ที่ parent ไม่ throw
- Change invariant — เปลี่ยนเงื่อนไขคงที่ของ parent
java
class Account {
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException();
// ...
}
}
class SavingsAccount extends Account {
@Override
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException();
if (amount > 5000) throw new IllegalArgumentException(); // ❌ stricter
// ...
}
}Caller ของ Account ไม่รู้ว่ามี limit 5000 → bug
LSP violations ที่เจอจริงในงาน enterprise
ตัวอย่าง Rectangle/Square เป็นคลาสสิกแต่ห่างจาก code ใน production จริง — นี่คือ LSP violation ที่เจอบ่อยกว่า:
1. CachedRepository ที่ return stale data
java
class UserRepository {
public User findById(Long id) { /* always fresh */ }
}
class CachedUserRepository extends UserRepository {
@Override
public User findById(Long id) {
// computeIfAbsent: ถ้า key id "ยังไม่มี" ใน cache → เรียก super::findById มา compute แล้ว put;
// ถ้ามีอยู่แล้ว → return ของเดิมใน cache (อาจเก่า)
// super::findById คือ method reference — ย่อมาจาก (Long id) -> super.findById(id)
return cache.computeIfAbsent(id, super::findById); // ❌ อาจ return ข้อมูลเก่า
}
}Caller ที่คาดว่า "ข้อมูลสด" (เช่นหน้า admin verify) จะ break เมื่อรับ stale cache — invariant ของ parent ("returns current state") ถูกทำลาย
2. Subclass throw exception ใหม่
java
class FileExporter {
public void export(Data d) throws IOException { ... }
}
class S3FileExporter extends FileExporter {
@Override
public void export(Data d) throws IOException {
throw new S3ThrottlingException(...); // ❌ subclass-specific exception ที่ caller ไม่รู้จัก
}
}3. BigDecimal subclass ที่เปลี่ยน rounding mode ถ้า subclass override การคำนวณแล้วเปลี่ยน rounding default — caller ที่ใช้ผ่าน reference BigDecimal จะได้ผลลัพธ์ผิดเงียบ ๆ
กฎจำง่าย: ถ้า test ที่เขียนสำหรับ parent fail เมื่อสลับเป็น subclass → LSP violation
4. ISP — Interface Segregation Principle
🔤 คำอ่าน:
Interface Segregation(อิน-เทอร์-เฟส เซก-รี-เก-ชั่น = "การแยกออก") — Segregation เป็นคำที่ออกเสียงยากที่สุดใน SOLID สำหรับคนไทย เน้นพยางค์ "เซก"
"Clients should not be forced to depend on methods they do not use." (ผู้ใช้งาน interface ไม่ควรถูกบังคับให้พึ่งพา method ที่ตัวเองไม่ได้ใช้)
java
// ❌ Fat interface
interface Worker {
void work();
void eat();
void sleep();
}
class Human implements Worker { ... }
class Robot implements Worker {
public void work() { ... }
public void eat() { throw new UnsupportedOperationException(); } // ❌
public void sleep() { throw new UnsupportedOperationException(); }
}java
// ✅ Split
interface Workable { void work(); }
interface Eatable { void eat(); }
interface Sleepable { void sleep(); }
class Human implements Workable, Eatable, Sleepable { ... }
class Robot implements Workable { ... }Real-world
java
// ❌
interface UserRepository {
User findById(Long id);
List<User> findAll();
void save(User user);
void delete(Long id);
void exportToCsv(); // ❌ admin operation
void importFromLdap(); // ❌ migration operation
void sendNewsletterToAll(); // ❌ marketing
}→ UserService ที่ใช้แค่ findById ก็ต้อง depend on ทั้ง interface
java
// ✅ Split by client need
interface UserRepository {
Optional<User> findById(Long id);
List<User> findAll();
User save(User user);
void delete(Long id);
}
interface UserExporter {
void exportToCsv();
}
interface UserImporter {
void importFromLdap();
}
interface NewsletterService {
void sendToAll(String content);
}→ แต่ละ client (controller, batch job, marketing tool) depend แค่ที่ใช้
กฎ
Interface เล็ก + focused > Interface ใหญ่
แต่ — ไม่ใช่ "1 method per interface" — group by client need
5. DIP — Dependency Inversion Principle
"Depend on abstractions (แอบ-สแตร็ก-ชั่นส์ = สิ่งนามธรรม เช่น interface), not concretions (คอน-คริ-ชั่นส์ = สิ่งรูปธรรม เช่น class จริง)."
"High-level modules should not depend on low-level modules." (จงพึ่งพา "สิ่งที่เป็นนามธรรม" เช่น interface ไม่ใช่ class จริง ๆ — โมดูลระดับสูงไม่ควรพึ่งพาโมดูลระดับล่างโดยตรง)
⚠️ อย่าสับสน: DIP (Dependency Inversion — หลักการออกแบบ) ≠ DI (Dependency Injection — เทคนิคโค้ด)
- DIP = "ควร depend on abstraction" (แนวคิด)
- DI = "ส่ง dependency เข้ามาผ่าน constructor/setter แทนที่จะ new เอง" (วิธีทำ)
- DI เป็น วิธีหนึ่ง ที่ใช้ทำ DIP — แต่ DIP ทำได้แบบอื่นด้วย (เช่น service locator, factory)
java
// ❌ Direct dependency
class OrderService {
private MySQLOrderRepository repo = new MySQLOrderRepository(); // ← concrete
private SmtpEmailService email = new SmtpEmailService(); // ← concrete
public void createOrder(Order order) {
repo.save(order);
email.send(...);
}
}ปัญหา:
- เปลี่ยน DB → แก้ OrderService
- Test → mock MySQL ลำบาก
- Reuse → ไม่ได้
java
// ✅ DIP — depend on interface
interface OrderRepository {
void save(Order order);
}
interface EmailService {
void send(String to, String subject, String body);
}
class OrderService {
private final OrderRepository repo;
private final EmailService email;
public OrderService(OrderRepository repo, EmailService email) { // ⭐ inject
this.repo = repo;
this.email = email;
}
}
class MySQLOrderRepository implements OrderRepository { ... }
class MongoOrderRepository implements OrderRepository { ... }
class SmtpEmailService implements EmailService { ... }
class SendGridEmailService implements EmailService { ... }→ OrderService (high-level business) ไม่รู้ ว่า DB คือ MySQL/Mongo
DI (Dependency Injection) = วิธี implement DIP
🔤 คำอ่าน: Dependency Injection (ดี-เพน-เดน-ซี อิน-เจ๊ก-ชั่น) — เรียกย่อว่า DI (ดี-ไอ)
java
// Constructor injection (recommended)
class OrderService {
private final OrderRepository repo;
public OrderService(OrderRepository repo) {
this.repo = repo;
}
}
// Spring auto-wire — ใช้ constructor injection (recommended)
// (ตัวอย่างนี้เป็น Spring Framework — ถ้ายังไม่ผ่านบท Spring ดูบทที่ 16)
@Service
class OrderService {
private final OrderRepository repo;
public OrderService(OrderRepository repo) { // ✅ Spring auto-inject ผ่าน constructor
this.repo = repo;
}
}💡 Spring 4.3+ (2017 เป็นต้นมา): ถ้า class มี constructor เดียว ไม่ต้องใส่
@AutowiredSpring จะ inject ให้อัตโนมัติ
ข้อยกเว้น: ถ้ามี หลาย constructor ต้องระบุว่าใช้ตัวไหนด้วย@Autowiredหรือ@Primaryมิฉะนั้น Spring งง
⚠️ อย่าใช้ field injection (
@Autowired private OrderRepository repo;) — ใน production:
- field เป็น
finalไม่ได้ → object อยู่ใน "half-constructed" state ได้- test ลำบาก — ต้องใช้ reflection หรือ
@MockBeanเท่านั้น mock ไม่ได้ตรง ๆ- ซ่อน cyclic dependency — Spring ปล่อยให้รันแต่ runtime พัง
- ขัดหลัก DIP ที่เพิ่งสอน (dependency ถูกซ่อนไม่ปรากฏใน signature)
ใช้ constructor injection เสมอ — explicit, testable, immutable
"Inversion"
→ ทิศทาง dependency invert — low depend on abstraction ที่ high ออกแบบ
เทียบ 3 ภาษา — DIP คือหลักการ ไม่ผูกกับ Java
typescript
// TypeScript — interface + constructor injection เหมือน Java
interface OrderRepository { save(order: Order): Promise<void>; }
class OrderService {
constructor(private repo: OrderRepository) {} // ⭐ inject abstraction
}
// สลับ implementation ได้: new OrderService(new MySQLRepo()) / new MongoRepo()python
# Python — ใช้ duck typing (ดั๊ก-ไท-ปิ้ง) / Protocol (โปร-โต-คอล)
# duck typing = "ถ้ามันเดินเหมือนเป็ดและร้องเหมือนเป็ด ก็ถือว่าเป็นเป็ด" — ไม่สนชนิด ขอแค่มี method ที่ต้องใช้ (เช่น save()) ก็พอ
# Protocol = feature ของ Python ที่ทำงานเหมือน interface แบบเบา ๆ — ไม่ต้อง implements ตามแบบ Java (ดู type hints บทที่ 5)
from typing import Protocol
class OrderRepository(Protocol):
def save(self, order: Order) -> None: ...
class OrderService:
def __init__(self, repo: OrderRepository): # ⭐ inject อะไรก็ได้ที่มี save()
self.repo = repo💡 จับหลัก: "โค้ด business ระดับสูงพึ่ง abstraction ไม่ใช่ class จริง" — Java/TS บังคับผ่าน
interface, Python ใช้ Protocol หรือแค่ duck typing ก็ได้ เป้าหมายเดียวกันคือ สลับ implementation ได้โดยไม่แก้ business logic (ทำให้ test ง่าย + เปลี่ยน DB/service ได้)
6. SOLID — Putting it Together
เรียน 5 หลัก SOLID มาทีละตัวแล้ว มาดูว่ามันทำงานร่วมกันในระบบจริงยังไง — Order System ที่ใช้ทุกหลักพร้อมกัน (SRP แยกหน้าที่, OCP/DIP ผ่าน interface, ฯลฯ) เห็นว่า SOLID ไม่ใช่กฎแยกกันแต่เสริมกันให้โค้ดยืดหยุ่นและ test ได้:
ตัวอย่างจริง — Order System
java
// Domain
class Order {
// Rich domain model
public void confirm() { ... }
public BigDecimal total() { ... }
}
// Abstractions
interface OrderRepository { // DIP
Optional<Order> findById(Long id);
void save(Order order);
}
interface PaymentProcessor { // OCP - แต่ละ payment = impl
PaymentResult charge(BigDecimal amount, PaymentMethod method);
}
interface NotificationSender { // ISP - แค่ที่จำเป็น
void notify(User user, String message);
}
// Service — orchestrate
class OrderService { // SRP - business logic เท่านั้น
private final OrderRepository repo;
private final PaymentProcessor payment;
private final NotificationSender notifier;
public OrderService(OrderRepository repo, PaymentProcessor payment, NotificationSender notifier) {
this.repo = repo;
this.payment = payment;
this.notifier = notifier;
}
public void placeOrder(Order order, PaymentMethod method) {
PaymentResult result = payment.charge(order.total(), method);
if (result.success()) {
order.confirm();
repo.save(order);
notifier.notify(order.getUser(), "Order confirmed");
} else {
throw new PaymentFailedException(result.error());
}
}
}
// Implementations
class JpaOrderRepository implements OrderRepository { ... }
class StripeProcessor implements PaymentProcessor { ... }
class PayPalProcessor implements PaymentProcessor { ... }
class EmailNotificationSender implements NotificationSender { ... }
class SmsNotificationSender implements NotificationSender { ... }
// LSP - StripeProcessor + PayPalProcessor ใช้แทน PaymentProcessor ได้เสมอ→ SOLID applied → maintainable + extensible
7. SOLID Trade-offs
SOLID = guideline ไม่ใช่ dogma — มี cost:
| Cost | ผลกระทบ |
|---|---|
| More files | navigation ยาก |
| More abstractions | indirection — debug ลำบาก |
| Higher boilerplate | interface + impl + factory |
| Premature | over-engineer for simple problem |
กฎ Apply SOLID
✅ ใช้เมื่อ:
- Code base ใหญ่ขึ้น
- มี requirement เปลี่ยนบ่อย
- หลายคนทำ feature
- ต้อง mock ใน test
- เห็น pattern ซ้ำ 3+ ครั้ง
❌ อย่าใช้เมื่อ:
- Prototype / MVP
- Throw-away code
- ทำคนเดียว
- Domain ยังไม่ stable
คติพจน์ที่จำง่าย (ดัดแปลงจากคำพูดของ Donald Knuth (โดนัลด์ คะ-นูธ) เรื่อง premature optimization):
📜 คำพูดต้นฉบับของ Knuth (1974) — Computing Surveys, "Structured Programming with go to Statements": "Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%."
แปล: "โปรแกรมเมอร์เสียเวลามหาศาลกับการคิด/กังวลเรื่องความเร็วของส่วนที่ไม่สำคัญ และความพยายามเหล่านี้ส่งผลเสียอย่างหนักต่อการ debug และ maintain — เราควรลืมเรื่อง optimization เล็ก ๆ ไป 97% ของเวลา: premature optimization (พรี-มา-เจอร์ ออป-ติ-ไม-เซ-ชั่น) คือต้นตอของความชั่วร้ายทั้งปวง แต่ก็อย่าพลาดโอกาสใน 3% ที่สำคัญ"
💡 ประเด็นที่คนชอบลืม: Knuth พูดว่า "97% ของเวลา" — ไม่ใช่ "อย่า optimize เลย" — ยัง 3% สำคัญที่ต้อง optimize อยู่
Premature SOLID is the root of all over-engineering. (การรีบใช้ SOLID ก่อนเวลาอันควร คือต้นตอของการออกแบบเกินจำเป็นทั้งปวง)
🔤
Premature(พรี-มา-เจอร์ = ก่อนเวลาอันควร) ·over-engineering(โอ-เวอร์ เอ็น-จิ-เนีย-ริ่ง = ออกแบบเกินจำเป็น)
8. Anti-patterns ที่ดูเหมือน SOLID
A. Interface for Everything
java
// ❌ Interface แค่ตัวเดียวที่ implement
interface UserService { ... }
class UserServiceImpl implements UserService { ... }→ ไม่ช่วยอะไร — abstraction ที่ไม่มี value
กฎ: interface เมื่อมี 2+ implementations จริงใน production (ไม่นับ mock — เพราะ Mockito mock class ธรรมดาได้อยู่แล้ว ไม่ต้องมี interface)
💡 เกณฑ์ที่แม่นกว่า: สร้าง interface เมื่อต้องการ polymorphism จริง ๆ ใน production (สลับ strategy, มีหลาย impl คนละ environment, ฯลฯ) — ไม่ใช่ "เผื่อ test"
B. Manager Class Pattern
java
// ❌ Manager = god class
class UserManager {
void createUser() { ... }
void deleteUser() { ... }
void sendEmail() { ... }
void exportData() { ... }
// ... 30 methods
}→ ละเมิด SRP — split by concern
C. Helper / Util Class
java
// ❌ Static dump
class StringUtils {
static String capitalize(String s) { ... }
static String reverse(String s) { ... }
static int countWords(String s) { ... }
}OK สำหรับ pure function — แต่ business logic ใส่ใน Util = wrong
9. SOLID ใน Real Frameworks
SOLID ไม่ใช่ทฤษฎีลอย ๆ — framework ที่เราใช้ทุกวันออกแบบตามหลักนี้ Spring Boot แยก layer (SRP) และ inject by interface (DIP), React ใช้ composition ส่วนนี้ชี้ให้เห็นว่า SOLID ฝังอยู่ในเครื่องมือจริงตรงไหนบ้าง:
Spring Boot
java
// SRP — Controller, Service, Repository = different responsibility
@RestController class UserController { ... }
@Service class UserService { ... }
@Repository interface UserRepository extends JpaRepository<User, Long> {}
// DIP — Spring auto-inject by interface
@Service
class OrderService {
private final OrderRepository repo; // Spring inject impl
public OrderService(OrderRepository repo) { this.repo = repo; }
}
// OCP — multiple impl ของ interface
@Component class StripeProcessor implements PaymentProcessor { ... }
@Component class PayPalProcessor implements PaymentProcessor { ... }
// Spring inject all
@Service
class PaymentService {
private final Map<String, PaymentProcessor> processors;
public PaymentService(Map<String, PaymentProcessor> processors) {
this.processors = processors;
}
}React
typescript
// SRP - hook ที่ทำ 1 อย่าง
function useUser(id: number) { ... }
function useUserPosts(userId: number) { ... }
// OCP - props ที่ extendable
type ButtonProps = {
variant: 'primary' | 'secondary' | 'destructive';
onClick: () => void;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
// DIP - inject behavior via prop
type SearchProps = {
onSearch: (q: string) => Promise<Result[]>; // ← abstraction
};10. แบบฝึก — SOLID Smells
java
class ReportGenerator {
public Report generate(String type, List<Data> data) {
Report report = new Report();
if (type.equals("PDF")) {
// PDF generation logic
} else if (type.equals("CSV")) {
// CSV generation logic
} else if (type.equals("XLSX")) {
// XLSX logic
}
// Save to disk
File file = new File("/tmp/report." + type.toLowerCase());
FileOutputStream fos = new FileOutputStream(file);
// ...
// Send email
EmailUtil.send("admin@example.com", "Report ready", file);
// Log
Logger.log("Report generated: " + type);
return report;
}
}หาว่า violate SOLID อะไรบ้าง? → คำตอบด้านล่าง
เฉลย
- SRP: ทำหลายอย่าง — generation, file save, email, log
- OCP: เพิ่ม format ใหม่ต้อง modify
generate() - DIP: depend on concrete
File,EmailUtil,Logger
Refactor:
java
interface ReportFormatter {
byte[] format(List<Data> data);
}
class PdfFormatter implements ReportFormatter { ... }
class CsvFormatter implements ReportFormatter { ... }
class XlsxFormatter implements ReportFormatter { ... }
interface FileWriter {
void write(String name, byte[] content);
}
interface NotificationSender {
void notify(String message, Attachment attachment);
}
class ReportGenerator {
private final Map<String, ReportFormatter> formatters;
private final FileWriter fileWriter;
private final NotificationSender notifier;
public Report generate(String type, List<Data> data) {
ReportFormatter formatter = formatters.get(type);
byte[] content = formatter.format(data);
fileWriter.write("report." + type.toLowerCase(), content);
notifier.notify("Report ready", new Attachment(content));
return new Report();
}
}11. Beyond SOLID
SOLID = important แต่ไม่ใช่ทั้งหมด:
- DRY — Don't Repeat Yourself
- KISS — Keep It Simple
- YAGNI — You Aren't Gonna Need It
- Composition over Inheritance
- Law of Demeter
- Tell Don't Ask
- High Cohesion, Low Coupling
- CCP — Common Closure Principle (คอม-มอน โคล-เชอร์ = การปิดร่วมกัน — สิ่งที่เปลี่ยนพร้อมกันควรอยู่ package เดียวกัน)
- CRP — Common Reuse Principle (คอม-มอน รี-ยูส = การใช้ซ้ำร่วมกัน — สิ่งที่ถูก reuse พร้อมกันควรอยู่ package เดียวกัน)
- ADP — Acyclic Dependencies Principle (อะ-ไซ-คลิก ดี-เพน-เดน-ซีส์ = ไม่เป็นวงรอบ — graph dependency ระหว่าง package ห้ามมี cycle)
ดู Robert Martin (โร-เบิร์ต มาร์-ติน — รู้จักในนาม "อันเคิล บ็อบ" / Uncle Bob) 's "Clean Architecture" สำหรับ list ครบ
📣 เสียงค้าน Clean Code / SOLID ยุคใหม่ (ต้องรู้)
SOLID อยู่มา 25+ ปี — มีคนยุคใหม่ค้านบ้าง ควรอ่านควบคู่:
John Ousterhout (จอห์น เอาส-เตอร์-เฮาท์) — A Philosophy of Software Design (2018, 2nd ed 2021)
- เสนอแนวคิด "deep modules" (โมดูลใหญ่แต่ interface แคบ) ดีกว่าโมดูลเล็กเยอะ ๆ
- ค้านกฎ "function ≤ 20 บรรทัด" — function ที่ทำหน้าที่เดียวให้ครบ แม้ยาวกว่า ก็อ่านง่ายกว่าโดน split เป็น 5 ตัวที่ต้องโดดไปมา
DHH (David Heinemeier Hansson — เด-วิด ไฮ-เน-ไม-เออร์ ฮัน-สัน — ผู้สร้าง Ruby on Rails) ปี 2024
- เขียนบล็อก/ทวีตว่า "Clean Code is bad code" — ค้านว่า "extract method" สุดโต่งทำให้ trace logic ยากขึ้น
Kent Beck (เคนต์ เบ็ค — ผู้ร่วมสร้าง XP, TDD) — Tidy First? (2023)
- เสนอ "tidying" (จัดให้สะอาดทีละนิด) มี empirical มากกว่า "clean code" แบบ dogma — บอกชัดว่า "เมื่อไหร่ควร tidy ก่อน เมื่อไหร่ควรทำ feature ก่อน"
→ ไม่ได้แปลว่า SOLID/Clean Code ผิด — แค่อย่าทำเป็น dogma (กฎตายตัวที่ห้ามถาม) — context (บริบท) สำคัญกว่ากฎ
12. ⚠️ Pitfalls
| ❌ | ✅ |
|---|---|
| Apply SOLID ทุกที่ตั้งแต่ MVP | Apply เมื่อ pattern ปรากฏ |
| Interface ที่มี 1 impl | Inline หรือ delete interface |
| God class แบ่งเป็น 100 mini-class | Group by reason to change |
| Inherit เพื่อ reuse | Composition |
| Static utility มี business logic | Service + DI |
| Public field "เพื่อ simple" | Encapsulation |
| Subclass override เป็น no-op | Wrong inheritance — separate class |
| Catch + ignore เพื่อ "follow LSP" | Throw + caller handle |
13. Checkpoint
🛠️ Checkpoint 2.1 — Identify Violations
ดู codebase ของคุณ — หา:
- 1 class ที่ละเมิด SRP
- 1 method ที่ละเมิด OCP (มี if/else by type)
- 1 inheritance ที่ละเมิด LSP
🛠️ Checkpoint 2.2 — Refactor OCP
java
class DiscountCalculator {
public double calculate(String type, double price) {
if (type.equals("NONE")) return 0;
if (type.equals("PERCENT_10")) return price * 0.1;
if (type.equals("PERCENT_20")) return price * 0.2;
if (type.equals("FREE_SHIPPING")) return price > 1000 ? 50 : 0;
return 0;
}
}Refactor ตาม OCP
🛠️ Checkpoint 2.3 — Apply DIP
java
class WeatherWidget {
private OpenWeatherMapClient api = new OpenWeatherMapClient("key");
public Weather getCurrent(String city) {
return api.fetch(city);
}
}Refactor ให้ DI + testable
🛠️ Checkpoint 2.4 — Real Example
ออกแบบ "Notification System":
- Channels: email, SMS, push, slack
- Priority: high, medium, low
- Templates per channel
- User preference (turn off some channels)
Apply SOLID — ออกแบบ class + interface
14. สรุปบท
✅ S — SRP: 1 class = 1 reason to change (group by stakeholder)
✅ O — OCP: extend without modify — Strategy pattern + DI
✅ L — LSP: subclass substitutable — Square ≠ Rectangle
✅ I — ISP: interface เล็ก, focused — split by client need
✅ D — DIP: depend on interface (abstraction) — DI = วิธี implement
✅ SOLID = trade-off — apply เมื่อจำเป็น (project ใหญ่ + requirement เปลี่ยน)
✅ Interface ต้องมี 2+ implementations ถึงคุ้ม
✅ Composition + DI = backbone ของ SOLID code
✅ Spring framework = SOLID ตั้งแต่ออกแบบ — ใช้ตรง ๆ ได้
← บทที่ 1 | บทที่ 3 → Design Patterns
Glossary: ../glossary.md · Style guide: ../CONTRIBUTING.md last_verified: 2026-06-03 · review report: ../REVIEW-2026-06-03.md