Devfavor

Back

GRASP คืออะไร — 9 หลักตัดสินว่าโค้ดควรอยู่ class ไหน อย่างมีเหตุผลBlur image

เคยนั่งเถียงกับเพื่อนในทีมไหม — ว่า method calculateTotal() ควรอยู่ใน class Order หรือควรแยกไป OrderService ดี? ทั้งสองฝั่งฟังดูมีเหตุผล แต่ไม่มีใครชี้ชัดได้ว่าทำไมแบบหนึ่งถึงดีกว่าอีกแบบ

คำถามแบบนี้เกิดขึ้นทุกวันในงานออกแบบ OO และนี่คือคำถามที่ลึกที่สุดข้อหนึ่ง — “ความรับผิดชอบ (responsibility) นี้ควรไปอยู่ที่ class ไหน” เพราะวางผิดที่ ผลพวงคือโค้ดที่แก้ทีไรกระทบไปทั้งระบบ

GRASP คือชุดหลักการที่พยายามตอบคำถามนี้อย่างเป็นระบบ ไม่ใช่ด้วยสัญชาตญาณ ทำให้เราอธิบายได้ว่าทำไมถึงวางตรงนี้ ไม่ใช่แค่ “รู้สึกว่าถูก”


🧭 GRASP คืออะไร และทำไมถึงต่างจาก SOLID#

GRASP ย่อมาจาก General Responsibility Assignment Software Patterns (บางคนเรียก Principles ก็ได้ ใช้สลับกันได้) เป็นชุดหลักการ 9 ข้อสำหรับการออกแบบ OO

แก่นของ GRASP คือการตอบคำถามเดียว — “ความรับผิดชอบนี้ควรอยู่ที่ class ไหน” โดยมีเหตุผลรองรับ ความสามารถในการมอบหมายความรับผิดชอบได้ดีถือเป็นทักษะหลักอย่างหนึ่งของการออกแบบ OO

จุดที่มักสับสนกันคือ GRASP ไม่ใช่ design pattern แบบ Gang of Four (เช่น Singleton, Factory) ที่เป็น solution สำเร็จรูปหยิบไปใช้ได้ทันที GRASP เป็น “วิธีคิด” ในการตัดสินใจมากกว่า เหมือน mental toolkit ที่ติดตัวไว้ตอนออกแบบ

แล้ว GRASP ต่างจาก SOLID อย่างไร? พูดง่าย ๆ คือ SOLID เป็นเกณฑ์ว่าโค้ดที่ดีควรมีหน้าตาแบบไหน ส่วน GRASP อยู่ลึกลงไปอีกชั้น — คือการอธิบาย “เหตุผล” ว่าทำไมถึงควรวางความรับผิดชอบไว้ตรงนั้น ทั้งสองไม่ได้ขัดกัน แต่เสริมกัน และหลายข้อทับซ้อนกันด้วย (จะเล่าให้ฟังตอนท้าย)

ทั้ง 9 ตัวคือ Information Expert, Creator, Controller, Low Coupling, High Cohesion, Polymorphism, Pure Fabrication, Indirection และ Protected Variations ฟังดูเยอะ แต่ถ้าจัดกลุ่มดี ๆ จะเห็นภาพง่ายขึ้นมาก


🧰 กลุ่มที่ 1 — สามตัวที่เราใช้ทุกวันโดยไม่รู้ตัว#

สามตัวนี้ตอบคำถามตรง ๆ ว่า “method นี้ไปไว้ที่ไหน” และเราน่าจะเคยใช้ทั้งสามตัวนี้แบบไม่รู้ชื่อมาแล้ว

Information Expert — ให้งานกับคนที่ถือข้อมูล#

หลักการพื้นฐานที่สุด คือมอบความรับผิดชอบให้ class ที่ “มีข้อมูล” ที่จำเป็นต่องานนั้น

ขั้นตอนคิดง่ายมาก — ดูว่างานนี้คืออะไร แล้วถามว่าต้องใช้ข้อมูลอะไร ข้อมูลนั้นเก็บอยู่ที่ class ไหน ก็วางงานไว้ที่นั่น

# order.py — การคำนวณยอดรวมควรอยู่ที่ Order เพราะ Order ถือ items อยู่แล้ว
class Order:
    def __init__(self, items):
        self._items = items

    def total(self):
        # ข้อมูล _items อยู่ที่นี่ งานคำนวณก็ควรอยู่ที่นี่
        return sum(item.price * item.qty for item in self._items)
python

ผลที่ได้คือ behavior กระจายไปอยู่กับ class ที่ถือข้อมูลนั้น ๆ ทำให้ encapsulation ดีและไม่ต้องเปิดข้อมูลภายในออกไปข้างนอก

ลองนึกภาพแบบตรงข้าม ถ้าให้ class อื่นไปดึง _items ของ Order ออกมาคำนวณยอดเอง นั่นคือการวางงานผิดที่ — Order ต้องเปิดข้อมูลภายในให้คนนอกเห็น และถ้าโครงสร้าง items เปลี่ยน ทุกที่ที่ดึงไปคำนวณก็พังตามกันหมด

ข้อควรระวัง — Information Expert ไม่ได้แปลว่ายัดทุกอย่างเข้า domain object เสมอ ถ้าทำตามตรง ๆ แล้ว object เริ่มแบกงานเยอะเกิน เราจะถ่วงดุลด้วยตัวอื่น (อีกเดี๋ยวจะเจอ Pure Fabrication)

Creator — ใครควรเป็นคนสร้าง object#

เมื่อต้องสร้าง object A ขึ้นมาสักตัว ใครควรเป็นคนทำ? ตามหลักการแล้ว ให้ class B เป็นคนสร้าง A เมื่อเข้าเงื่อนไขข้อใดข้อหนึ่ง เช่น B บรรจุหรือรวม A ไว้ (aggregates), B บันทึก A, B ใช้งาน A อย่างใกล้ชิด หรือ B มีข้อมูลตั้งต้นที่ต้องใช้ตอนสร้าง A

# customer.py — Customer เป็น Creator ของ Order เพราะ Customer รวบรวม orders ไว้ (aggregates)
#               และถือข้อมูลตั้งต้นที่ Order ต้องใช้
class Customer:
    def __init__(self, customer_id):
        self._customer_id = customer_id
        self._orders = []

    def place_order(self, products):
        # เข้าเงื่อนไข Creator: Customer รวบรวม orders + มีข้อมูลตั้งต้น (customer_id)
        order = Order(self._customer_id, products)
        self._orders.append(order)
        return order
python

ช่วยให้ความเป็นเจ้าของ object และการ initialize ชัดเจน ไม่ต้องสร้าง object กระจัดกระจายไปทั่ว

Controller — ใครรับ event ที่เข้ามาจากนอกระบบ#

เมื่อมี system event (เหตุการณ์ที่เข้ามาจากนอกระบบ เช่น ผู้ใช้กดปุ่ม หรือมี request เข้ามา) ใครควรเป็นคนรับด่านแรก? Controller คือ non-UI object ที่รับ event นั้นแล้ว “ส่งต่อ (delegate)” งานให้ส่วนอื่นทำ ไม่ใช่ลงมือทำ business logic เอง

# customer_orders_controller.py — controller บาง: รับแล้ว delegate ทันที
class CustomerOrdersController:
    def __init__(self, order_service):
        self._order_service = order_service

    def add_customer_order(self, customer_id, products):
        # ไม่ทำ business logic เอง ส่งต่อให้ service จัดการ
        order = self._order_service.place_order(customer_id, products)
        return Created(order.id)
python

ในระบบใหญ่ ๆ การ delegate อาจผ่านตัวกลางอย่าง Mediator หรือ Command bus แต่ในระบบเล็ก ๆ แค่เรียก service method ตรง ๆ แบบนี้ก็ถือว่าทำตาม Controller ครบแล้ว ไม่จำเป็นต้องมีของหนักเสมอ

จุดที่มักตีความได้หลายแบบคือ Controller ของ GRASP ไม่ใช่ UI controller และไม่เท่ากับ Controller ใน web MVC โดยตรง หัวใจของ Controller คือ “รับแล้วส่งต่อ” ถ้าเรายัด business logic ลงไปจนกลายเป็น fat controller นั่นคือหลุดจากเจตนาเดิม


📏 กลุ่มที่ 2 — สองไม้บรรทัดที่ใช้วัดว่าตัดสินใจดีหรือยัง#

Low Coupling กับ High Cohesion ต่างจากสามตัวแรกตรงที่ไม่ได้บอกโดยตรงว่าวาง method ไว้ไหน แต่เป็น “ตัววัด” ที่เอาไว้ประเมินว่าการตัดสินใจของเราดีหรือไม่ จึงไม่ได้สั่งให้ทำอะไรเป็นขั้นเป็นตอน แต่คอยบอกว่าสิ่งที่เราทำลงไปนั้นเข้าท่าหรือเปล่า

Low Coupling — ให้ class พึ่งพากันน้อยที่สุด#

Coupling คือระดับที่ class หนึ่งต้องพึ่งพา class อื่น ยิ่งพึ่งพากันมาก แก้ที่หนึ่งก็กระทบอีกที่ การออกแบบที่ coupling ต่ำจะแก้ง่ายกว่า เอาไปใช้ซ้ำได้มากกว่า และผลกระทบจากการเปลี่ยนแปลงจำกัดวงแคบ

เกณฑ์เชิงปฏิบัติง่าย ๆ ที่ใช้วัดได้จริง — ลองนับดูว่าถ้าจะเปลี่ยน feature เดียว ต้องเปิดแก้กี่ class พร้อมกัน ถ้าแก้เรื่องเดียวแต่ต้องไล่แก้ห้าหกที่ นั่นคือสัญญาณว่า coupling สูงเกินไปแล้ว

High Cohesion — ให้แต่ละ class โฟกัสเรื่องเดียว#

Cohesion คือระดับที่ความรับผิดชอบใน class เดียวสัมพันธ์กันแน่นแฟ้นและโฟกัสเรื่องเฉพาะ class ที่ cohesion สูงจะเข้าใจง่าย ทดสอบง่าย เพราะทำเรื่องเดียวจริง ๆ วิธีเช็คง่าย ๆ คือลองอธิบายหน้าที่ของ class เป็นประโยคเดียว ถ้าต้องใช้คำว่า “และ” หลายครั้ง แสดงว่า class นั้นแบกหลายเรื่องเกินไป

สองตัวนี้มักถูกมองว่าขัดกัน แต่จริง ๆ แล้วส่งเสริมกัน — เมื่อแต่ละ class โฟกัสเรื่องของตัวเอง (cohesion สูง) ก็ไม่ต้องไปยุ่งกับ class อื่นมาก (coupling ต่ำ) ตามไปด้วย และที่น่าสนใจคือ ถ้าเราทำตาม Information Expert ดี ๆ มักจะได้ทั้งสองอย่างนี้แถมมาเอง

# bad: cohesion ต่ำ — Order แบกทุกเรื่องตั้งแต่คำนวณ ตัดบัตร ยันส่งเมล
class Order:
    def total(self): ...
    def charge_card(self): ...      # เรื่อง payment ไม่ควรอยู่ที่นี่
    def send_email(self): ...       # เรื่อง notification ก็ไม่ควร

# good: แต่ละ class โฟกัสเรื่องเดียว coupling ต่ำลง
class Order:
    def total(self): ...
class PaymentService:
    def charge(self, order): ...
class NotificationService:
    def send(self, order): ...
python

นี่คือไม้บรรทัดที่จะคอยเตือนเราตอนใช้ตัวอื่น — เช่นเวลา Information Expert เริ่มทำให้ class บวมเกิน


🛡️ กลุ่มที่ 3 — สี่ตัวสำหรับรับมือความเปลี่ยนแปลง#

อีกสี่ตัวที่เหลือเข้ามาช่วยตอนงานเริ่มซับซ้อน โดยเฉพาะเมื่อต้องรองรับการเปลี่ยนแปลงในอนาคต

Polymorphism — แทน if/switch เช็ค type ด้วย polymorphic method#

เมื่อพฤติกรรมแปรผันตาม type ของ object แทนที่จะเขียน if/else หรือ switch ไล่เช็คชนิด ให้ย้ายพฤติกรรมที่ต่างกันไปไว้ที่แต่ละ subtype เป็น method ที่ override กัน

# discounts.py — ส่วนลดแต่ละชนิดรู้วิธีคำนวณของตัวเอง ไม่ต้องไล่เช็คชนิดข้างนอก
from abc import ABC, abstractmethod

class Discount(ABC):
    @abstractmethod
    def apply(self, amount): ...

class PercentageDiscount(Discount):
    def __init__(self, percent):
        self._percent = percent
    def apply(self, amount):
        return amount * (1 - self._percent / 100)

class FixedDiscount(Discount):
    def __init__(self, value):
        self._value = value
    def apply(self, amount):
        return max(0, amount - self._value)
python

ข้อดีคือเพิ่มชนิดส่วนลดใหม่ได้โดยไม่ต้องไปแก้ if/switch เดิมทุกที่ ประเด็นไม่ได้อยู่ที่ “มี inheritance” แต่อยู่ที่ใช้ polymorphic dispatch แทนการไล่เช็คชนิดเอง

Pure Fabrication — สร้าง class ที่ไม่มีในโลกจริงเพื่อรักษาคุณภาพ#

บางครั้งทำตาม Information Expert ตรง ๆ แล้วได้ผลลัพธ์ไม่ดีนัก เช่น ถ้าให้ Order รู้วิธี save ตัวเองลง database Order ก็ต้องไปรู้จัก DB ทำให้ coupling พุ่งและ cohesion ตก

ทางออกคือสร้าง “Pure Fabrication” — class ที่ไม่ได้แทนแนวคิดใน domain จริง แต่ประดิษฐ์ขึ้นมาเพื่อรักษา low coupling กับ high cohesion ใน Domain-Driven Design class แบบนี้มักเป็นพวก Service หรือ Repository

# order_repository.py — class ที่ไม่มีในโลกจริง แต่จำเป็นทางสถาปัตยกรรม
class OrderRepository:
    def __init__(self, db):
        self._db = db

    def save(self, order):
        # Order ไม่ต้องรู้จัก DB ความรู้เรื่อง persistence อยู่ที่นี่
        self._db.insert("orders", order.to_row())
python

ตรงนี้จะรู้สึกขัดสัญชาตญาณนิดหน่อย เพราะสวนกับความเคยชินที่ว่า class ควรสะท้อนของจริงใน domain แต่เราจงใจแลก domain representation เพื่อให้ได้การแยกส่วนที่ดีกว่า

Indirection — แทรกตัวกลางไม่ให้สองฝั่งผูกกันตรง ๆ#

เมื่อไม่อยากให้สอง component ผูกกันโดยตรง ให้แทรก object ตัวกลางเข้าไปคั่น ตัวอย่างที่เห็นบ่อยคือ Mediator หรือ EventBus ที่ทำให้ผู้ส่งกับผู้รับไม่ต้องรู้จักกันโดยตรง นี่คือกลไกเบื้องหลังที่ช่วยให้ coupling ต่ำลง

# event_bus.py — ผู้ส่ง publish ผ่าน EventBus โดยไม่รู้จักผู้รับเลย
class EventBus:
    def __init__(self):
        self._subscribers = {}

    def subscribe(self, event_type, handler):
        self._subscribers.setdefault(event_type, []).append(handler)

    def publish(self, event):
        for handler in self._subscribers.get(type(event), []):
            handler(event)

# order_service.py — ฝั่งสั่งซื้อแค่ประกาศว่า "มีออเดอร์ใหม่" ไม่ต้องรู้ว่าใครฟังอยู่
class OrderService:
    def __init__(self, bus):
        self._bus = bus

    def place_order(self, order):
        # ไม่ได้เรียก EmailService หรือ InventoryService ตรง ๆ
        self._bus.publish(OrderPlaced(order))
python

ฝั่งที่สนใจ (เช่น ส่งเมล หรือตัด stock) ก็แค่ subscribe เข้ามาเอง ผู้ส่งกับผู้รับจึงไม่ต้องรู้จักกันเลย เปลี่ยนหรือเพิ่มผู้รับได้โดยไม่ต้องแตะโค้ดฝั่งผู้ส่ง

Protected Variations — ห่อจุดที่จะเปลี่ยนด้วย interface เสถียร#

ระบุจุดที่คาดว่าจะแปรผันหรือไม่เสถียร แล้วห่อจุดนั้นไว้ด้วย interface ที่เสถียร เพื่อให้ความเปลี่ยนแปลงข้างในไม่ลามไปกระทบส่วนอื่น

# payment.py — ห่อช่องทางจ่ายเงินที่อาจเพิ่ม/เปลี่ยนได้ ไว้หลัง interface เดียว
class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount): ...

class CreditCard(PaymentStrategy):
    def pay(self, amount): ...

class PayPal(PaymentStrategy):
    def pay(self, amount): ...
# เพิ่มช่องทางใหม่ได้โดยโค้ดที่เรียกใช้ไม่ต้องแก้
python

จะสังเกตว่า Protected Variations เกี่ยวพันกับ Polymorphism อย่างใกล้ชิด — คือการใช้ polymorphic operation ห่อจุดที่ไม่แน่นอนไว้นั่นเอง


⚠️ ข้อควรระวัง — อย่าทำตามจนเกินพอดี#

ทุกตัวมีราคาที่ต้องจ่าย ถ้าใช้แบบไม่ดูบริบทก็อาจย้อนกลับมาสร้างปัญหาได้

ทำตาม Information Expert แบบ literal เกินไป จะได้ God Class ที่แบกทุกอย่างจน cohesion ตก ต้องคอยถ่วงด้วย Low Coupling/High Cohesion และ Pure Fabrication เสมอ

Controller ก็เช่นกัน รับงานเยอะเกินจะกลายเป็น bloated controller ทางแก้คือแตกเป็น use-case controller แยกตาม scenario แทนที่จะมีตัวเดียวคุมทั้งระบบ

ส่วน Pure Fabrication กับ Indirection ซื้อ low coupling มาด้วยราคาของชั้น indirection ที่เพิ่มขึ้น อ่านโค้ดยากขึ้น และเสียการ map กับ domain ดังนั้นคำเตือนใหญ่ของทุกแหล่งคือ — ใส่ pattern เมื่อแก้ปัญหาจริงเท่านั้น ไม่ใช่ใส่ไว้ก่อนเผื่ออนาคต เพราะนั่นคือ over-engineering


🔗 9 ตัวไม่ได้แยกกันอยู่#

ตอนต้นเกริ่นไว้ว่าหลายข้อทับซ้อนกัน ตรงนี้คือภาพที่ลากเส้นเชื่อมให้เห็นชัด ๆ เพราะพอมองออกว่าแต่ละตัวโยงกันยังไง ก็ไม่ต้องท่องแยกทีละข้อ แต่จะจำได้เป็นภาพเดียวที่หมุนรอบเป้าหมายเดียวกัน

เริ่มจากแกนกลาง — Information Expert เป็นจุดตั้งต้น พอวางงานไว้กับ class ที่ถือข้อมูล cohesion มักสูงและ coupling มักต่ำตามมาเอง สองไม้บรรทัด (Low Coupling กับ High Cohesion) จึงเหมือนผลพลอยได้ของการทำ Information Expert ให้ดี ไม่ใช่กฎที่ต้องไปทำแยกต่างหาก

พอ Information Expert เริ่มทำให้ object แบกงานเยอะเกิน Pure Fabrication กับ Indirection ก็เข้ามาถ่ายงานออกและแทรกตัวกลางคั่น ทั้งคู่มีเป้าหมายเดียวกันคือกด coupling ลง ต่างกันแค่วิธี — ตัวหนึ่งสร้าง class ใหม่มารับงาน อีกตัวเพิ่มชั้นคั่นระหว่างสองฝั่ง

อีกคู่ที่แทบเป็นเรื่องเดียวกันคือ Polymorphism กับ Protected Variations — Protected Variations คือเป้าหมาย (กันความเปลี่ยนแปลงไม่ให้ลามออกไป) ส่วน Polymorphism คือหนึ่งในวิธีไปถึงเป้านั้น คือใช้ polymorphic method ห่อจุดที่จะแปรผันไว้

แม้แต่ Controller ก็ยืมกลไกของ Indirection มาใช้ — ตัวที่รับ event แล้วส่งต่อ ก็คือตัวกลางที่คั่นไม่ให้โลกภายนอกผูกกับ business logic ตรง ๆ นั่นเอง

พอเห็นเส้นเชื่อมพวกนี้แล้วจะรู้สึกว่าทั้ง 9 ข้อไม่ใช่ลิสต์ที่ต้องจำเป็นรายตัว แต่เป็นมุมมองหลายด้านของเป้าหมายเดียว — วางความรับผิดชอบให้ระบบยืดหยุ่นและแก้ง่าย เมื่อจับเป้าหมายนี้ได้ การเลือกหยิบแต่ละตัวมาใช้ร่วมกันก็เป็นธรรมชาติขึ้นเอง


☕ สรุป + ชี้ทางต่อ#

GRASP คือชุดวิธีคิด 9 ข้อที่ช่วยตอบคำถามพื้นฐานที่สุดของ OO ว่า “ความรับผิดชอบนี้ควรอยู่ class ไหน” โดยมีเหตุผลรองรับ

ถ้าจะจำแค่ใจความ — เริ่มจาก Information Expert, Creator, Controller สามตัวนี้ตอบคำถามตรง ๆ ว่างานไปไว้ที่ไหน แล้วใช้ Low Coupling กับ High Cohesion เป็นไม้บรรทัดวัดว่าตัดสินใจดีหรือยัง ส่วนอีกสี่ตัวเก็บไว้ใช้ตอนต้องรับมือความเปลี่ยนแปลง

เวลาออกแบบจริง ลองถือคำถามสามข้อนี้ติดมือไว้ — งานนี้ต้องใช้ข้อมูลอะไร / ข้อมูลนั้นอยู่ที่ class ไหน / แล้ว class ที่จะรับงานบวมเกินไปหรือยัง สามข้อนี้ครอบคลุมการตัดสินใจส่วนใหญ่ในชีวิตประจำวันได้แล้ว

และถ้าให้เลือกฝึกตัวเดียวก่อน แนะนำ Information Expert — เพราะเป็นจุดตั้งต้นที่ใช้บ่อยที่สุด และพอทำได้คล่อง มักจะลาก coupling ต่ำกับ cohesion สูงตามมาเองโดยไม่ต้องคิดเยอะ

ที่สำคัญคือ GRASP ไม่ใช่กฎตายตัว แต่เป็นเครื่องมือฝึกคิด ใส่เมื่อมีปัญหาจริงเท่านั้น และถ้าอยากต่อยอด GRASP เข้ากันได้ดีกับ SOLID มาก หลายข้อแทบจะเป็นเรื่องเดียวกัน — Low Coupling สะท้อน DIP, High Cohesion สะท้อน SRP, Polymorphism กับ Protected Variations โยงเข้า OCP ลองอ่านบทความ SOLID, DRY, KISS, YAGNI ต่อ จะเห็นภาพการออกแบบโค้ดที่ดีครบขึ้นอีกชั้น

😎 ☕

Code, coffee, and calm — the holy trinity of a good day. ☕

GRASP คืออะไร — 9 หลักตัดสินว่าโค้ดควรอยู่ class ไหน อย่างมีเหตุผล
Author Coffee Stack
Published at 23 มิถุนายน 2026