Skip to content

บทที่ 2 — SOLID Principles

← บทที่ 1 | สารบัญ | บทที่ 3 →

SOLID = 5 หลัก OOP design ที่ทำให้ code maintainable + extensible

🔤 คำอ่านชื่อหลัก (สำหรับคนไทย):

  • Single Responsibility (ซิง-เกิล รี-สปอน-ซิ-บิ-ลิ-ตี้) = ความรับผิดชอบเดียว
  • Open/Closed (โอ-เพ่น/โคลส-ด) = เปิด/ปิด
  • Liskov Substitution (ลิส-คอฟ ซับ-สติ-ทู-ชั่น) = การแทนที่แบบลิสคอฟ (ตั้งตามชื่อ Barbara Liskov ผู้เสนอแนวคิด ปี 1987)
  • Interface Segregation (อิน-เทอร์-เฟส เซก-รี-เกชั่น) = การแยกอินเทอร์เฟส
  • Dependency Inversion (ดี-เพน-เดน-ซี อิน-เวอร์-ชั่น) = การกลับทิศ dependency
LetterPrincipleคำอ่าน
SSingle Responsibilityซิง-เกิล รี-สปอน-ซิ-บิ-ลิ-ตี้
OOpen/Closedโอ-เพ่น/โคลส-ด
LLiskov Substitutionลิส-คอฟ ซับ-สติ-ทู-ชั่น
IInterface Segregationอิน-เทอร์-เฟส เซก-รี-เกชั่น
DDependency 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 กลุ่ม)

ตัวอย่าง: ถ้า Employee class มี method calculatePay() (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:

  1. เปลี่ยน DB schema → save()
  2. เปลี่ยน email format → sendWelcomeEmail()
  3. เปลี่ยน JSON format → toJson()
  4. เพิ่ม 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"

ตัวอย่าง — เหตุผลที่อาจเปลี่ยน

ClassReason
UserRepositoryDB schema เปลี่ยน
UserApiControllerAPI contract เปลี่ยน
UserServiceBusiness rule เปลี่ยน
UserDomain 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):

java
enum 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 ทุกตัวที่ implement PaymentProcessor ไว้ใน 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

แก้:

  1. ไม่ใช้ inheritance — Square + Rectangle = 2 class แยก หรือ
  2. ทำให้ 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 เดียว ไม่ต้องใส่ @Autowired Spring จะ 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 filesnavigation ยาก
More abstractionsindirection — debug ลำบาก
Higher boilerplateinterface + impl + factory
Prematureover-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 ทุกที่ตั้งแต่ MVPApply เมื่อ pattern ปรากฏ
Interface ที่มี 1 implInline หรือ delete interface
God class แบ่งเป็น 100 mini-classGroup by reason to change
Inherit เพื่อ reuseComposition
Static utility มี business logicService + DI
Public field "เพื่อ simple"Encapsulation
Subclass override เป็น no-opWrong 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