ข้าม​ไป​ยัง​เนื้อหา

รูปแบบ​การ implement Domain Model

หัวใจ​ของ Domain-Driven DesignDomain-Driven Design (DDD)แนวทาง​พัฒนา​ซอฟต์แวร์​ที่​เน้น​สร้าง domain model ซึ่ง​เข้าใจ​กฎ​และ​กระบวนการ​ของ​ธุรกิจ​อย่าง​ลึกซึ้ง แล้ว​เขียน​ลง​ใน code จริง บัญญัติ​โดย Eric Evans (2003) แบ่ง​เป็น Strategic และ Tactical DesignStrategic Design คือ​การ จัดการ​ความ​ซับซ้อน โดย​วาง โมเดล​ของ domainDomain ModelModel ของ Domain ที่​ฝัง​กฎ​และ​กระบวนการ​ทาง​ธุรกิจ​ไว้​ใน code จริง หัวใจ​ของ DDD คือ​การ​พัฒนา domain model ที่ “เข้าใจ​ธุรกิจ​อย่าง​ลึกซึ้ง”Strategic Design ไว้​ตรง​กลาง​ของ​ซอฟต์แวร์​เพื่อ​รองรับ​กระบวนการ​ธุรกิจ (business process) ของ app แต่​เมื่อ​ถึง​เวลา​เขียน model นั้น “ลงใน code จริง” เรา​มี​รูปแบบ (pattern) ให้​เลือก​หลาย​แบบ และ — นี่​คือ​ประเด็น​สำคัญ — ไม่มี​รูปแบบ​ใด​เป็นกระสุน​เงิน (silver bullet) ที่​ดี​ที่สุด​เสมอ​ไป

ใน​บท​ที่ 3 เรา​เห็น​แล้ว​ว่า app ขนาด​ใหญ่​หนึ่ง​ตัว​อาจ​มี​หลาย model อยู่​พร้อม​กัน และ model เหล่า​นั้น ไม่​ได้​ซับซ้อน​หรือ​สำคัญ​เท่า​กัน​หมด บาง​ส่วน​เต็ม​ไป​ด้วย​ตรรกะ​ธุรกิจ​ที่​ลึกซึ้ง ใน​ขณะ​ที่​บาง​ส่วน​ทำ​หน้าที่​เพียง​จัดการ​ข้อมูล​ธรรมดา ฉะนั้น​การ​เลือก design pattern ที่ “พอดี” กับ​แต่ละ model จึง​เป็น​ทักษะ​ที่​คุ้ม​ค่า​แก่​การ​ฝึกฝน

กฎ​ข้อ​เดียว​ที่​ต้อง​จำ

ตราบ​ใด​ที่​คุณ แยก​ตรรกะ​ของ domain ออก​จาก​เรื่อง​เทคนิค (infrastructure) ได้ คุณ​ก็​ทำ Model-Driven Design — และ​จึง​ทำ DDD — ได้​แล้ว ไม่​ว่า​จะ​เลือก pattern แบบ​ใด​ก็ตาม นั่น​คือ​เกณฑ์​ตัดสิน​ที่แท้​จริง ไม่ใช่​ข้อ​บังคับ​ว่า “ต้อง​ใช้ rich object model เท่านั้น”


domain layerDomain ModelModel ของ Domain ที่​ฝัง​กฎ​และ​กระบวนการ​ทาง​ธุรกิจ​ไว้​ใน code จริง หัวใจ​ของ DDD คือ​การ​พัฒนา domain model ที่ “เข้าใจ​ธุรกิจ​อย่าง​ลึกซึ้ง”Strategic Design คือ​ชั้น​ของ code ที่​อยู่​ใจกลาง app และ​เป็น​ที่​อยู่​ของ domain model หน้าที่​หลัก​ของ​มัน​คือ กั้น​ความ​ซับซ้อน​ของ​ธุรกิจ​ออก​จาก​ความ​ซับซ้อน​ทาง​เทคนิค​ที่​เกิด​ขึ้น​โดย​บังเอิญ (accidental technical complexity) ไม่​ให้​เรื่อง​อย่าง​การ​จัดการ transaction หรือ​การ​บันทึก state รั่ว​เข้า​มา​ปะปน​กับ​กฎ​ธุรกิจ​จน​กฎ​เหล่า​นั้น​พร่า​เลือน

สิ่ง​ที่​หลาย​คน​แปลก​ใจ​คือ ใน app ส่วน​ใหญ่ domain layer กิน​พื้นที่ code เพียง​นิดเดียว ที่​เหลือ​เป็น​เรื่อง infrastructure และ​การ​แสดง​ผล (presentation) ดัง​รูป​ที่ 5-1

รูปที่ 5-1: code ที่เป็น domain model กินพื้นที่เพียงส่วนเล็กๆ ของ codebase ทั้งหมด — มี User Interface Layer, Application Layer, Domain Layer และ Persistence Layer โดยมี Infrastructure พาดอยู่ด้านข้าง

ทำไม domain layer ถึง​เล็ก แต่​สำคัญ​ที่สุด

code domain ที่​ดูเหมือน “ผอม” เมื่อ​เทียบ​กับ infrastructure ทั้งหลาย — แต่​มัน​คือ​ส่วน​ที่​ฝัง กฎ​และ​ความ​ได้​เปรียบ​ใน​การ​แข่งขัน ของ​ธุรกิจ​ไว้ จึง​คุ้ม​ค่าที่สุด​ที่​จะ​ทุ่มเท​ออกแบบ การ​ปกป้อง​ไม่​ให้​มัน​ปน​กับ​เรื่อง​เทคนิค​คือ​เป้าหมาย​ของ Layered Architecture ทั้งหมด


ระบบ​ใหญ่​ไม่​ได้​ถูก​สร้าง​ด้วย​วิธี​เดียวกัน​ทั้งหมด ส่วน​หนึ่ง​สำคัญ​น้อย​กว่า​อีก​ส่วน และ​อาจ​มี​หลาย model เพื่อ​รองรับ context หรือ​ทีม​ที่​ต่าง​กัน รูป​ที่ 5-2 แสดง​ให้​เห็น​ว่า app หนึ่ง​ตัว​อาจ​ถูก​แบ่ง​ออก​เป็น​หลาย Bounded ContextBounded Contextขอบเขต​ที่​ชัดเจน (มัก​เป็น​ระบบ​ย่อย​หรือ​ทีม​หนึ่ง) ที่ model หนึ่ง​ใช้ได้​และ​มี​ความหมาย​แน่นอน ภายใน​ขอบเขต​นี้ Ubiquitous Language สอดคล้อง​กัน 100%Strategic Design โดย​แต่ละ context เลือก​ใช้ pattern คนละ​แบบ ใน​การ implement model ของ​ตน

รูปที่ 5-2: หลาย domain model ที่ implement ด้วย pattern ต่างกันภายใน app เดียว — Bounded Context A ใช้ Domain Model Pattern, B ใช้ Transaction Script, C ใช้ Table Module

รูปแบบ​สาม​ตัว​แรก​ที่​จะ​พูด​ถึง​ต่อ​ไป​นี้​ถูก​บันทึก​ไว้​ครั้ง​แรก​โดย Martin Fowler ใน​หนังสือ Patterns of Enterprise Application Architecture:

  • Domain Model — model เชิง​วัตถุ​ที่​รวม​ทั้ง​ข้อมูล​และ​พฤติกรรม
  • Transaction Script — จัด​ตรรกะ​เป็น​ขั้นตอน​ตาม​ลำดับ (procedural)
  • Table Module — 1 object แทน​หนึ่ง​ตาราง​ใน​ฐาน​ข้อมูล

นอกจาก​ของ Fowler เรา​จะ​เพิ่ม Active Record, Anemic Domain Model และ​แนวทาง functional เข้า​มา​ด้วย แต่ละ​ตัว​มี​ประโยชน์​ขึ้น​กับ​ความ​ซับซ้อน​ของ model แต่ละ​ก้อน​ใน app ของ​คุณ

🚢 ระบบ​ขนส่ง — ผสม pattern ใน app เดียว

ใน​ระบบ​ขนส่ง​สินค้า การ “วางแผน​เส้นทาง” ของ Cargo เต็ม​ไป​ด้วย​กฎ​ธุรกิจ (ต้นทาง ปลายทาง กำหนด​เวลา ผ่าน​ท่าเรือ​ไหน​บ้าง) — เหมาะ​กับ Domain Model pattern เต็ม​ตัว

แต่ context ที่​แค่​ดูแล “ตาราง​อ้างอิง​ท่าเรือ” (รหัส​ท่า ชื่อ​ท่า ประเทศ) เป็น​แค่​ฟอร์ม​กรอก​ข้อมูล​ธรรมดา — ใช้ Table Module หรือ Active Record ก็​เพียงพอ ไม่​ต้อง​ลาก​ทุก​อย่าง​เข้า model เชิง​วัตถุ​ที่​ซับซ้อน


Domain Model pattern เป็น pattern ที่ “เป็น​เนื้อ​เดียว​กับ DDD” มาก​ที่สุด เพราะ​มัน​เหมาะ​กับ domain ที่​ซับซ้อน​และ รวย​ตรรกะ​ธุรกิจ (Rich Domain ModelRich Domain Modelmodel domain ที่ “รวย​พฤติกรรม” — object ที่​เก็บ​ทั้ง​ข้อมูล​และ business logic ไว้​ด้วย​กัน เป็น​คู่​ตรง​ข้าม​ของ Anemic Domain Model (ที่​มี​แต่ getter/setter แล้ว​ผลัก logic ไป​ไว้​ที่​อื่น) tactical patterns ของ DDD จะ​คุ้ม​ค่า​ก็​ต่อ​เมื่อ model “รวย” พอ​เช่น​นี้Tactical Design) มัน​คือ model เชิง​วัตถุ​ที่​รวม​ทั้ง พฤติกรรม​และ​ข้อมูล ไว้​ด้วย​กัน เมื่อ​มอง​ผ่านๆ อาจ​ดู​คล้าย data schema ของ​ฐาน​ข้อมูล แต่จริงๆ แล้ว​มัน​บรรจุ​กระบวนการ​ธุรกิจ (business process) ความ​สัมพันธ์ กฎ และ​ตรรกะ​ที่​ลึก​กว่า​นั้น​มาก

หัวใจ​ของ pattern นี้​คือ​สมมติฐาน​ที่​ว่า “ทำ​เหมือน​ไม่มี​ฐาน​ข้อมูล” — model ถูก​ออกแบบ​แบบ persistence-ignorant (ไม่รู้​และ​ไม่​สน​ว่า​จะ​ถูก​เก็บ​ลง​ที่ไหน) เรา​เริ่ม​จาก code model ไม่ใช่ data model — นี่​คือ model-driven แทนที่​จะ​เป็น data-driven จะ​ค่อย​มา​ประนีประนอม​เรื่อง​การ​เก็บ​ข้อมูล​ทีหลัง​ก็ได้ object ภายใน model จึง​เป็น “Plain Old Object” (POCO/POJO) ที่​ปลอด​จาก infrastructure

รูปที่ 5-3: Domain Model pattern — Client เรียกผ่าน Application Service เข้าสู่ Domain Layer ที่มี Domain Model อยู่แกนกลาง โดย Infrastructure และ Data Store ถูกแยกออกไปอยู่วงนอก

การ​ไม่​ต้อง​คิด​เรื่อง persistence ระหว่าง​ออกแบบ ทำให้​เรา​สร้าง model ที่ สื่อ​ความหมาย (expressive) และ​โฟกัส​ที่​ปัญหา​ของ domain ล้วนๆ ได้

โฟกัส​ที่ ‘กริยา’ ไม่ใช่​แค่ ‘คำ​นาม’

เวลา​วาด model หลาย​คน​เริ่ม​จาก​คำ​นาม (Order, Customer, Product) ซึ่ง​เสี่ยง​จะ​ได้​แค่​ภาพ​สะท้อน​ของ​ตาราง​ข้อมูล หนังสือ​แนะนำ​ให้​โฟกัส​ที่ กริยา​และ​การกระทำ ของ domain ด้วย เช่น “เสนอ​ราคา​ประมูล (place bid)”, “โอน​เงิน”, “ใช้​คูปอง​ส่วนลด” — เพราะ​นั่น​จะ​ดึง​ความ​สนใจ​ไป​ที่ พฤติกรรม แทนที่​จะ​เป็น​เพียง สถานะ

ใน model ที่​ดี แต่ละ object รับผิดชอบ​งาน​เฉพาะ​ของ​ตน และ มอบหมาย​งาน (delegate) ให้​กัน​เพื่อ​ทำ use case ให้​สำเร็จ ตัวอย่าง​จาก​หนังสือ​คือ​เว็บ​ประมูล: class Auction ไม่​ได้​คำนวณ​ราคา​บิด​ถัด​ไป​เอง แต่​มอบ​ให้ WinningBid จัดการ

Auction.cs
public class Auction
{
public void PlaceBidFor(Bid bid, DateTime currentTime)
{
if (StillInProgress(currentTime))
{
if (FirstOffer())
PlaceABidForTheFirst(bid);
else if (BidderIsIncreasingMaximumBid(bid))
// มอบหมายให้ WinningBid ตัดสินใจเรื่องการเพิ่มราคาสูงสุด
WinningBid = WinningBid.RaiseMaximumBidTo(bid.MaximumBid);
else if (WinningBid.CanBeExceededBy(bid.MaximumBid))
Place(WinningBid.DetermineWinningBidIncrement(bid));
}
}
}
🛒 e-commerce — เมื่อ Order ‘มี​พฤติกรรม’

ใน​ร้าน​ออนไลน์ Order ไม่​ได้​มี​แค่ property อย่าง​วัน​ที่​สร้าง สถานะ และ​เลข​ที่​คำ​สั่ง​ซื้อ — มัน​ต้อง​บรรจุ ตรรกะ​การ​ใช้​คูปอง​ส่วนลด พร้อม​กฎ​รอบๆ ทั้งหมด: คูปอง​นี้​ยัง​ใช้ได้​ไหม? ใช้​กับ​สินค้า​ใน​ตะกร้า​นี้​ได้​หรือ​เปล่า? มี​โปรโมชัน​อื่น​ที่​ทำให้​คูปอง​นี้​ใช้​ไม่​ได้​หรือ​ไม่?

เมื่อ​ตรรกะ​แบบ​นี้ “อยู่​ใน​ตัว Order เอง” นั่น​คือ Domain Model pattern ที่​ทำงาน​ได้​จริง

ไม่ใช่​กระสุน​เงิน — อย่า​ใช้​กับ​ทุก​อย่าง

Domain Model pattern ท้าทาย​ทาง​เทคนิค​ที่สุด ต้อง​ใช้​นัก​พัฒนา​ที่​เข้าใจ OOP เป็น​อย่าง​ดี และ​มี​ต้นทุน​สูง ระบบ​ย่อย​ส่วน​ใหญ่​เป็น​เพียง CRUD — มี​เพียง Core DomainCore Domainsubdomain ที่​สำคัญ​และ​สร้าง​ความ​ได้​เปรียบ​ใน​การ​แข่งขัน​มาก​ที่สุด เป็น​เหตุผล​ที่​เรา​ต้อง​สร้าง​ซอฟต์แวร์​เอง​แทน​การ​ซื้อ ควร​ทุ่ม​ทีม​เก่ง​ที่สุด​ลง​ตรง​นี้Strategic Design เท่านั้น​ที่​คุ้ม​กับ​การ​ลงทุน​ทำ rich domain model

ถ้า​ส่วน​ที่​คุณ​ทำ​เป็น​เพียง “ฟอร์ม​กรอก​ข้อมูล” ที่​ตรรกะ​แทบ​ไม่​เปลี่ยนแปลง การ​ฝืน​ใช้ Domain Model pattern อย่าง​ดี​ที่สุด​ก็​เสียแรง​เปล่า อย่าง​เลว​ร้าย​คือ​เพิ่ม​ความ​ซับซ้อน​ที่​ไม่​จำเป็น​เข้าไป


ใน​บรรดา pattern ทั้งหมด​ใน​บท​นี้ Transaction ScriptTransaction Scriptรูปแบบ​ที่​จัด​ตรรกะ​ธุรกิจ​เป็น​ชุด​คำ​สั่ง​ตาม​ขั้นตอน (procedural) แทน​การกระจายลง​ใน​อ็อบเจ็กต์ เหมาะ​กับ​งาน​ง่าย ๆ แต่​จะ​รก​เมื่อ domain ซับซ้อน​ขึ้นArchitecture เข้าใจ​ง่าย​และ​เริ่ม​ใช้ได้​เร็ว​ที่สุด มัน​เป็น​สไตล์ procedural ไม่ใช่ OOP โดย​ทั่วไป​จะ​สร้าง “1 procedure ต่อ1 business transaction” แล้ว​จัด​กลุ่ม​ไว้​ใน manager หรือ service class แต่ละ procedure บรรจุ​ตรรกะ​ธุรกิจ ทั้งหมด ที่​ต้อง​ใช้​ใน​การ​ทำ​ธุรกรรม​นั้น​ให้​จบ — ตั้งแต่ workflow, กฎ​ธุรกิจ, การ validate ไป​จนถึง​การ​บันทึก​ลง​ฐาน​ข้อมูล

ใน​ตัวอย่าง​เว็บ​ประมูล แต่ละ use case ถูก​ห่อ​เป็น class เดียว​ที่ implement interface ICommand ดัง​โครงสร้าง​นี้:

classDiagram
    class ICommand {
        +Execute()
    }
    class CreateAuction {
        +Execute()
    }
    class BidOnAuction {
        +Execute()
    }
    ICommand <|.. CreateAuction
    ICommand <|.. BidOnAuction

ทั้ง use case จะ​ถูก​ห่อ​ไว้​ใน method เดียว — รับผิดชอบ​ทุก​อย่าง​ปน​กัน ตั้งแต่​ดึง​ข้อมูล บันทึก​ข้อมูล จัดการ transaction ไป​จนถึง​ตรรกะ​ธุรกิจ​ของ​การ​วาง​บิด:

BidOnAuction.cs
public class BidOnAuction : ICommand
{
public void Execute()
{
using (var scope = new TransactionScope())
{
ThrowExceptionIfNotValid(auctionId, bidderId, amount, timeOfBid);
ThrowExceptionIfAuctionHasEnded(auctionId);
if (IsFirstBid(auctionId))
PlaceFirstBid(auctionId, bidderId, amount, timeOfBid);
else if (IsIncreasingMaximumBid(auctionId, amount, bidderId))
IncreaseMaximumBidTo(amount);
else if (CanMeetOrExceedBidIncrement(amount))
UpdatePrice(auctionId, bidderId, amount, timeOfBid);
}
}
}

จุด​แข็ง: เข้าใจ​ง่าย พาสมาชิก​ใหม่​เข้า​ทีม​ได้​เร็ว​โดย​ไม่​ต้อง​รู้ pattern มา​ก่อน เพิ่ม​ความ​ต้องการ​ใหม่​ก็​เพียง​เพิ่ม method โดย​แทบ​ไม่​กระทบ​ของ​เดิม จึง​เหมาะ​กับ​ทีม​ที่​มี junior ซึ่ง​ยัง​ไม่​ถนัด OOP และ​เหมาะ​กับ​ส่วน​ของ domain ที่​มี​ตรรกะ​น้อยหรือ​แทบ​ไม่มี​เลย

กับดัก​ของ Transaction Script: การ​ซ้ำซ้อน

เมื่อ app โต​ขึ้น​และ​ตรรกะ​ซับซ้อน​ขึ้น Transaction Script จะ​กลาย​เป็น​ปัญหา​อย่าง​รวดเร็ว เพราะ​ธรรมชาติ​ของ​มัน​ทำให้​เกิด code ซ้ำซ้อน (duplication) ได้​ง่าย — API บวม​ขึ้น​ด้วย method ละเอียด​ยิบ​ที่​ทำงาน​ทับซ้อน​กัน คุณ​ใช้ sub-method ลด​การ​ซ้ำซ้อน​ใน validation และ​กฎ​ธุรกิจ​ได้​บ้าง แต่​การ​ซ้ำซ้อน​ใน workflow นั้น​หลีก​เลี่ยง​ได้​ยาก

เมื่อ​ความ​ซ้ำซ้อน​เริ่ม​มาก​เกิน​ไป นั่น​คือ​สัญญาณ​ชัดเจน​ว่า​ถึง​เวลา refactor ไป​สู่ Domain Model pattern


Table Module จับ​คู่ object model เข้า​กับ database model โดย 1 object แทน​หนึ่ง​ตาราง (หรือ view) object นั้น​รับผิดชอบ​ทั้ง​การ persist และ​พฤติกรรม​ทาง​ธุรกิจ ข้อดี​คือ​ไม่มี​ความ​เหลื่อม (mismatch) ระหว่าง object model กับ database model มัน​เหมาะ​กับ Database-Driven Design จึง​ดูเหมือน​จะ​ไม่​เข้า​กับ DDD เมื่อ​มอง​ผ่านๆ — แต่​สำหรับ​ส่วน​ของ domain ที่​ง่ายๆ ถูก​ล้อม​ด้วย bounded context และ​เป็น​แค่ “ฟอร์ม​กับ​ข้อมูล” มัน​คือ​ทาง​เลือก​ที่​ดี​และ​เรียนรู้​ง่าย​กว่า Domain Model pattern

Active Record เป็น​ญาติ​ของ Table Module ต่าง​กัน​ตรง​ที่ object แทน “แถว” (row) ของ​ตาราง ไม่ใช่​ตัว​ตาราง​เอง แต่ละ business object คือ​หนึ่ง​ระเบียน บรรจุ​ทั้ง​ข้อมูล พฤติกรรม และ​วิธี persist ตัวเอง รวม​ถึง method สำหรับ​เพิ่ม instance ใหม่​และ​ค้นหา collection

เมื่อไร​ที่ Active Record เปล่ง​ประกาย

Active Record เหมาะ​มาก​กับ app เรียบ​ง่าย​ที่ data model กับ business model จับ​คู่ หนึ่ง​ต่อ​หนึ่ง เช่น ระบบ block หรือ​ฟอรัม และ​เหมาะ​ถ้า​คุณ​มี database model อยู่​แล้ว หรือ​ชอบ​สร้าง app แบบ “data first” เพราะ​ทุก object มี CRUD เหมือน​กัน​หมด จึง ใช้​เครื่องมือ code generation สร้าง model ให้​อัตโนมัติ​ได้​เลย

ข้อ​ระวัง: ถ้า​เมื่อไร object model กับ database model เริ่ม “แยก​ทาง​กัน” (diverge) นั่น​คือ​สัญญาณ​ให้ refactor ไป​ทาง Domain Model pattern


Anemic Domain ModelAnemic Domain Modelanti-pattern ที่อ็อบเจ็กต์มี​แต่ getter/setter ไร้​พฤติกรรม ตรรกะ​ถูก​ดึง​ไป​ไว้​ใน service ทั้งหมด — จ่าย​ค่า domain model แต่​ไม่​ได้​ประโยชน์​ของ​มัน​เลยTactical Design บาง​ครั้ง​ถูก​เรียก​ว่า anti-pattern เมื่อ​มอง​ผ่านๆ มัน​คล้าย Domain Model มาก — มี domain object ที่​แทน​แนวคิด​ทาง​ธุรกิจ​เหมือน​กัน แต่​พฤติกรรม​ทั้งหมด​ไม่​ได้​อยู่​ใน​ตัว object มัน​ถูก​ย้าย​ออก​ไป​อยู่​นอก model ทิ้ง​ให้ domain object เป็น​เพียง class รับ​ส่ง​ข้อมูล (Data Transfer Object) ที่​มี​แต่ getter/setter

ปัญหา​หลัก​คือ domain service จะ​กลับ​ไป​เขียน​แบบ procedural คล้าย Transaction Script — พร้อม​ปัญหา​ทั้งหมด​ของ​มัน หนึ่ง​ใน​นั้น​คือ​การ​ละเมิด​หลัก “Tell, Don’t Ask” ที่​ว่า object ควร บอก client ว่า​ทำ​อะไร​ได้/ไม่​ได้ แทนที่​จะ​เปิดเผย property ให้ client ไป​เดา​เอา​เอง​ว่า object อยู่​ใน​สถานะ​ที่​ทำงาน​นั้น​ได้​หรือ​ไม่

Anemic Domain Model — จ่าย​ค่า model แต่​ไม่​ได้​ประโยชน์​ของ​มัน

ถ้า​คุณ ตั้งใจ จะทำ rich domain model แต่​กลับ​ลงเอย​ด้วย object ที่​ไร้​พฤติกรรม​และ service ที่​แบกรับ​ทุก​อย่าง — นั่น​คือ​คือ anti-pattern ตัว​จริง คุณ​จ่าย​ค่า​ความ​ซับซ้อน​ของ​การ​มี model ไป​เต็มๆ แต่​ไม่​ได้​ความ​ชัดเจน​หรือ​การ​ห่อ​หุ้ม​กฎ​กลับ​มา​แม้แต่​น้อย

แต่ Anemic Domain Model ไม่​ได้​แย่​เสมอ​ไป มัน​เป็น​จุด​เริ่มต้น​ที่​ดี​สำหรับ​ส่วน​ที่​มี​ตรรกะ​น้อย หรือ​สำหรับ​ทีม​ที่​ยัง​ไม่​ถนัด OOP และ​ยัง​สามารถ​สะท้อน Ubiquitous LanguageUbiquitous Languageภาษา​กลาง​ที่​ทุก​คนใน​ทีม (ทั้ง​นัก​พัฒนา​และ​ผู้เชี่ยวชาญ​ธุรกิจ) ใช้​ร่วม​กัน อิง​กับ domain model โดยตรง คำ​เดียวกัน​ต้อง​หมาย​ถึง​สิ่ง​เดียวกัน และ​สะท้อน​ลง​ใน codeStrategic Design ได้ จึง​เป็น ก้าว​แรก ที่​ดี​ใน​การ​ค่อยๆ พัฒนา​ต่อยอด​ไป​สู่ rich domain model


จุด​ที่​น่า​สนใจ​คือ DDD เปิด​กว้าง​เต็ม​ที่ สำหรับ​คน​ที่​ชอบ functional มากกว่า OOP เรา​สร้าง domain model ด้วย​แนวคิด functional อย่าง immutability และ referential transparency ได้​สบาย object ที่​รวย​พฤติกรรม​ไม่ใช่​สิ่ง​จำเป็น และ​ใน​บริบท​นี้ Anemic Domain Model กลับ​กลาย​เป็น​แนวคิด​พื้นฐาน​ที่​มี​ประโยชน์ ไม่ใช่ anti-pattern

ฟัง​ดู​ขัด​กัน เพราะ model มี​ไว้​สื่อสาร​กับ domain expert แต่ anemic model กลับ​ดูเหมือน​จะ​ไม่​แทน​แนวคิด domain ด้วย object — อย่างไร​ก็ตาม นัก​ปฏิบัติ DDD สมัย​ใหม่​หลาย​คน​ยืนยัน​ว่า แนวคิด​ที่​สำคัญ​ที่สุด​ของ domain คือ “กริยา” ไม่ใช่​คำ​นาม​อย่าง “บัญชี​ธนาคาร” แต่​เป็นกริยา​อย่าง “การ​โอน​เงิน” ด้วย functional programming เรา​ยัง​แสดง​กริยา​เหล่า​นี้​ได้​เต็ม​ที่ และ​คุย​กับ domain expert ได้​อย่าง​มี​ความหมาย

ลอง​ดู entity BankAccount แบบ OOP ที่​รวย​พฤติกรรม:

BankAccount (OOP)
public class BankAccount
{
public Guid Id { get; private set; }
public Money Balance { get; private set; }
public Money OverdraftLimit { get; private set; }
public void Withdraw(Money amount) { /* ... */ }
public void Deposit(Money amount) { /* ... */ }
public void IncreaseOverdraft(Money amount) { /* ... */ }
}

ใน​แบบ functional มัน​จะ​กลาย​เป็น โครงสร้าง​ข้อมูล​บริสุทธิ์​ที่ immutable ไร้​พฤติกรรม แล้ว​พฤติกรรม​จะ​อยู่​ใน​รูป pure function แทน ความ​ท้าทาย​คือ​การ​จัด​กลุ่ม function เหล่า​นั้น​ให้​สอดคล้อง​กับ model เชิง​แนวคิด — ทาง​เลือก​ที่​ดี​คือ​จัด​กลุ่ม​ตาม AggregateAggregateกลุ่ม​ของ Entity และ Value Object ที่​ถูก​มอง​เป็น​หนึ่ง​หน่วย​เดียว​เพื่อ​รักษา​ความ​ถูกต้อง​ของ​ข้อมูล มี​ขอบเขต​ชัดเจน และ​เป็น​หน่วย​ของ transactionTactical Design

เพราะ functional บังคับ immutability ทุก function จึง​ต้อง คืน​ค่า​โครงสร้าง​ข้อมูล​ชุด​ใหม่ แทน​การ​แก้ state ของ object เดิม เปรียบเทียบ ShoppingBasket สอง​แบบ:

Basket (Functional)
// โครงสร้างข้อมูล immutable บริสุทธิ์
public struct ShoppingBasket
{
public ShoppingBasket(Guid id, IImmutableList<BasketItem> items)
{ this.Id = id; this.Items = items; }
public Guid Id { get; private set; }
public IImmutableList<BasketItem> Items { get; private set; }
}
// รวม function ทั้งหมดของ Basket aggregate ไว้ด้วยกัน
public static class Basket
{
// pure function — ไม่ผูกกับ instance ของ object ใด
public static ShoppingBasket AddItem(BasketItem item, ShoppingBasket basket)
{
if (basket.Items.Contains(item))
throw new DuplicateItemSelected();
// การเพิ่มของจะสร้าง collection ใหม่ที่ immutable
var items = basket.Items.Add(item);
// และคืน basket ใหม่ที่ immutable เช่นกัน
return new ShoppingBasket(basket.Id, items);
}
}

สังเกต​ว่า collection Items ห่อ​หุ้ม​เป็น private ไม่​ได้​แล้ว เพราะ function อยู่​นอก object จึง​เข้าถึง state ส่วนตัว​ไม่​ได้

ระวัง shallow copy เมื่อ​คัด​ลอก object

ใน​บาง​ภาษา การ​คัด​ลอก object อาจ​ได้​แค่ reference ที่​ชี้​ไป​ยัง object เดิม (shallow copy) เช่น ถ้า​คัด​ลอก ShoppingBasket ทั้ง​ต้นฉบับ​และ​สำเนา​อาจ​ชี้​ไป​ที่ collection Items เดียวกัน — แก้​สำเนา = แก้​ต้นฉบับ​ไป​ด้วย ทาง​แก้​ที่​ง่าย​ที่สุด​คือ ใช้​โครงสร้าง​ข้อมูล​และ collection ที่ immutable เท่านั้น (ภาษา​สมัย​ใหม่​ส่วน​ใหญ่​รองรับ immutable collection ใน​ตัว​อยู่​แล้ว)

อยาก​ลอง functional? คุ้ม​ที่​จะ​เรียน​พื้นฐาน

ภาษา​อย่าง Haskell, Scala, Clojure ทำให้ functional เป็น first-class แต่​คุณ​ก็​สร้าง functional domain model ใน​ภาษา OOP อย่าง C# หรือ Java ได้ ถ้า​ยัง​ไม่​คุ้น functional การ​เรียน​แนวคิด​พื้นฐาน​สัก​หน่อย​คุ้ม​ค่า​เวลา​แน่นอน


ไม่มี pattern ใด “ถูก” เสมอ — เลือก​ตาม ความ​ซับซ้อน​ของ​ตรรกะ และ ความ​สำคัญ ของ​แต่ละ​ส่วน:

Patternเหมาะ​เมื่อระวัง
Domain Modelตรรกะ​ธุรกิจ​ซับซ้อน รวย​พฤติกรรม (core domain)ต้นทุน​สูง ต้อง​เก่ง OOP — อย่า​ใช้​กับ CRUD ธรรมดา
Transaction Scriptตรรกะ​น้อย/แทบ​ไม่มี ทีม juniorcode ซ้ำ​เมื่อ​โต​ขึ้น → refactor ไป Domain Model
Table Moduleฟอร์ม​กับ​ข้อมูล จับ​คู่​ตา​รางตรงๆถ้า model เริ่ม diverge → refactor
Active Recordหนึ่ง​ต่อ​หนึ่ง​กับ​ตาราง CRUD เยอะ ตรรกะ​ต่ำไม่​เหมาะ​กับ​ตรรกะ​ซับซ้อน
Anemic + Functionalชอบ immutability/pure functionระวัง shallow copy; ถ้า​ตั้งใจ​ทำ rich แต่​ได้ anemic = anti-pattern
หัวใจ​ของ​บท​นี้

domain layer กั้น model ของ​ธุรกิจ​ออก​จาก infrastructure และ presentation — และ model นั้น implement ได้​ด้วย​หลาย pattern project ใหญ่​มัก​มี​หลาย model และ​หลาย pattern อยู่​ร่วม​กัน เกณฑ์​เดียว​ที่​ตัดสิน​คือ pattern นั้น​แยก​ตรรกะ domain ออก​จาก code เทคนิค​ได้​หรือ​ไม่ ถ้า​ได้ ก็​ถือว่า​เข้า​กับ DDD ส่วน Domain Model pattern นั้น​จง​เก็บ​แรง​ไว้​ทุ่ม​ให้ core domain ที่​ซับ​ซ้อนจริงๆ เท่านั้น

หมายเหตุ: Entity และ Aggregate

EntityEntityอ็อบเจ็กต์ที่​มี “ตัวตน” (identity) ต่อ​เนื่อง​ตาม​เวลา แม้​ค่า​ภายใน​เปลี่ยน ก็​ยัง​เป็น​สิ่ง​เดิม เช่น ลูกค้า, คำ​สั่ง​ซื้อ — แยกแยะ​ด้วย id ไม่ใช่​ด้วย​ค่าTactical Design และ AggregateAggregateกลุ่ม​ของ Entity และ Value Object ที่​ถูก​มอง​เป็น​หนึ่ง​หน่วย​เดียว​เพื่อ​รักษา​ความ​ถูกต้อง​ของ​ข้อมูล มี​ขอบเขต​ชัดเจน และ​เป็น​หน่วย​ของ transactionTactical Design คือ tactical building block ที่​นัก​ปฏิบัติ DDD ใช้​แทน​แนวคิด domain อย่าง​บัญชี​ธนาคาร​หรือ​ตะกร้า​สินค้า เรา​จะ​ลง​ลึก​เรื่อง​เหล่า​นี้​ใน Part III ของ​หนังสือ (บทถัดๆ ไป)

🔗 อ้างอิง​เพิ่มเติม​ใน DevIQ

เจาะ​ลึก​แนวคิด​ใน​บท​นี้​ต่อ​ได้ที่​คลัง​อ้างอิง DevIQ:

  • Domain Model — นิยาม​อย่าง​เป็น​ทางการ​ของ Domain Model pattern ที่​บท​นี้​ใช้​เป็น​แกน​หลัก​ใน​การ​เปรียบเทียบ​กับ pattern อื่นๆ
  • Anemic Domain Model — ขยาย​ความ​ว่า​ทำไม​จึง​ถูก​มอง​เป็น anti-pattern ใน​บริบท OOP แต่​กลับ​มี​ประโยชน์​ใน​บริบท functional ตาม​ที่​บท​นี้​อธิบาย
  • Aggregate — หน่วย​ที่​ใช้​จัด​กลุ่ม pure function เข้า​ด้วย​กัน​เวลา​สร้าง model แบบ functional (เช่น​ตัวอย่าง ShoppingBasket)
  • Entity — building block ที่ domain object อย่าง Auction หรือ BankAccount ใน​บท​นี้​เป็น​ตัวอย่าง​ของ​แนวคิด​นี้

เช็กความเข้าใจ — บทที่ 5

ข้อ 1 / 4

ตามหนังสือ เกณฑ์ที่แท้จริงในการตัดสินว่า ‘เข้ากับ DDD หรือไม่’ คืออะไร?