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

Repositories

เมื่อ​เรา​ออกแบบ AggregateAggregateกลุ่ม​ของ Entity และ Value Object ที่​ถูก​มอง​เป็น​หนึ่ง​หน่วย​เดียว​เพื่อ​รักษา​ความ​ถูกต้อง​ของ​ข้อมูล มี​ขอบเขต​ชัดเจน และ​เป็น​หน่วย​ของ transactionTactical Design ที่​เต็ม​ไป​ด้วย​พฤติกรรม​ไว้​อย่าง​ประณีต​แล้ว คำถาม​ต่อ​ไป​คือ — เรา​จะ “สร้าง บันทึก และ​ดึง​กลับ” อ็อบเจ็กต์เหล่า​นี้​อย่างไร โดย​ไม่​ให้​เรื่อง​ของ​ฐาน​ข้อมูล​มา​ปะปน​กับ​ตรรกะ​ธุรกิจ? คำ​ตอบ​ของ DDD คือ RepositoryRepositoryตัว​ที่​ทำให้​เรา​เข้าถึง Aggregate ราวกับ​เป็น collection ใน​หน่วย​ความ​จำ ซ่อน​รายละเอียด​ฐาน​ข้อมูล มี1 repository ต่อ1 Aggregate RootTactical Design

RepositoryRepositoryตัว​ที่​ทำให้​เรา​เข้าถึง Aggregate ราวกับ​เป็น collection ใน​หน่วย​ความ​จำ ซ่อน​รายละเอียด​ฐาน​ข้อมูล มี1 repository ต่อ1 Aggregate RootTactical Design ทำ​หน้าที่​จัดการ​การ persist และ retrieve aggregate โดย​ซ่อน​กลไก​การ​จัด​เก็บ​ไว้​หลัง “หน้า​ฉาก​ที่​ทำตัว​เหมือน collection” (collection façade) ผล​คือ domain model ของ​เรา​ไม่​จำเป็น​ต้อง​รับรู้​เลย​ว่า​เบื้องหลัง​เป็น SQL, document store หรือ flat file — ความ​ซับซ้อน​ทาง​เทคนิค​ถูกกัน​ออก​ไป​จาก domain อย่าง​หมดจด

หัวใจ​ของ​บท​นี้

Repository ไม่​ได้​มี​ไว้​เพื่อ “ทดสอบ​ง่าย” หรือ “สลับ​ฐาน​ข้อมูล” เป็น​หลัก — เป้าหมาย​ที่แท้​จริง​คือ กั้น domain model ออก​จาก persistence framework เพื่อ​ให้ model ของ​เรา​วิวัฒน์​ได้​อย่าง​อิสระ โดย​ไม่​ปล่อย​ให้​เทคโนโลยี​การ​จัด​เก็บ​มา​เป็น​ตัว​กำหนด​รูปร่าง​ของ​มัน


Repository คือ​อะไร และ​ต่าง​จาก​การ​เข้าถึง​ข้อมูล​แบบ​เดิม​อย่างไร

หัวข้อ​ที่​มีชื่อ​ว่า “Repository คือ​อะไร และ​ต่าง​จาก​การ​เข้าถึง​ข้อมูล​แบบ​เดิม​อย่างไร”

Repository ให้​ความ​สามารถ​คล้าย collection ใน​หน่วย​ความ​จำ — เพิ่ม ลบ ค้นหา aggregate ได้ — แต่​ต่าง​จาก​ชั้น data access แบบ​ดั้งเดิม (เช่น DAO หรือ​การ​เรียก ORM ตรงๆ) ใน 3 ข้อ​สำคัญ

  1. จำกัด​การ​เข้าถึง​ไว้​ที่​ระดับ aggregate root เท่านั้น — เรา​ดึง​และ​บันทึก​ได้​แค่ Aggregate RootAggregate RootEntity หนึ่ง​ตัว​ที่​เป็น “ประตู​เดียว” เข้า​สู่ Aggregate การ​อ้างอิง​จาก​ภายนอก​ทำได้​กับ root เท่านั้น root จึง​ควบคุม​กฎ​ความ​ถูกต้อง​ของ​ทั้ง​กลุ่มTactical Design ทำให้การ​เปลี่ยนแปลง​และ InvariantInvariantกฎ​ทาง​ธุรกิจ​ที่ “ต้อง​เป็น​จริง​เสมอ” ภายใน Aggregate เช่น ยอด​รวม​คำ​สั่ง​ซื้อ​ต้อง​เท่ากับ​ผล​รวม​ของ​รายการ​สินค้า — ต้อง​ถูกต้อง​ภายใน transaction เดียวTactical Design ทั้งหมด​ถูก​ควบคุม​ผ่าน aggregate เสมอ
  2. คง​หน้า​ฉาก​แบบ persistence-ignorant — ซ่อน​เทคโนโลยี​ที่​ใช้ persist ไว้​เบื้องหลัง client ไม่รู้​และ​ไม่​จำเป็น​ต้อง​รู้​ว่า​ข้าง​ใน​เป็น​อะไร
  3. ขีด​เส้น​แบ่ง​ระหว่าง domain model กับ data model — ข้อ​นี้​สำคัญ​ที่สุด

interface ของ repository ถือ​เป็น ส่วน​หนึ่ง​ของ domain model จึง​อยู่​ใน namespace ของ domain ส่วน implementation จริง​ไป​อยู่​ใน​ชั้น infrastructure

ICustomerRepository.cs (อยู่​ใน​ชั้น Domain)
namespace DomainModel
{
// interface เป็นของ domain — เขียนด้วยภาษาของธุรกิจ
public interface ICustomerRepository
{
Customer FindBy(Guid id);
void Add(Customer customer);
void Remove(Customer customer);
}
}

ส่วน implementation อยู่​ใน​ชั้น infrastructure และ​มัก​มอบ​งาน​หนัก​ให้ persistence framework เช่น NHibernate ทำ​แทน โดย​มี application service เป็น client หลัก​ที่​เรียก​ใช้งาน repository

🚢 Repository ใน​ระบบ​ขนส่ง​สินค้า

ใน​ระบบ Cargo Shipping เรา​มี CargoRepository หนึ่ง​ตัว​ต่อ aggregate root Cargo หนึ่ง​ตัว เมื่อ application service ต้องการ​จัด​เส้นทาง​ใหม่​ให้​สินค้า มัน​จะ​เรียก cargoRepository.FindBy(trackingId) เพื่อ​ดึง Cargo กลับ​มา​เป็น object ที่​สมบูรณ์ — โดย​ไม่​รับรู้​เลย​ว่า​เบื้องหลัง​เป็นการ join หลาย​ตาราง​หรือ​การ​อ่าน JSON เพียง​ก้อน​เดียว นั่น​คือ​พลัง​ของ collection façade


มี​นัก​พัฒนา​จำนวน​มาก​เขียน block โจมตี​ว่า repository เป็น antipattern เพราะ​มัน “ซ่อน​ความ​สามารถ​ของ persistence framework” แต่ Millett และ Tune ชี้​ว่า — นั่น​คือ​คือ จุด​ประสงค์ ของ​มัน

แทนที่​จะ​เปิด interface กว้างๆ ให้ query หรือ​แก้ไข​ข้อมูล​ได้​ทุก​รูปแบบ repository ทำให้การ​ดึง​ข้อมูล ชัดเจน ผ่าน named query method และ​จำกัด​อยู่​ที่​ระดับ aggregate ข้อดี​คือ

  • ปรับ​จูน query ได้​ง่าย เพราะ​รู้​ชัด​ว่า​มี query อะไร​บ้าง
  • สื่อ “เจตนา” ของ query ด้วย​ภาษา​ที่ domain expert เข้าใจ ไม่ใช่ SQL ดิบๆ
  • เปิด​เฉพาะ method persist ที่​มี​ความหมาย ไม่ใช่​ปล่อย CRUD ทุก​ตัว​ออก​มา​อย่าง​ไร้​ระเบียบ
repository ไม่​เหมาะ​กับ​ทุก​สถานการณ์

ถ้า​คุณ ไม่​ได้ ใช้ Rich Domain ModelRich Domain Modelmodel domain ที่ “รวย​พฤติกรรม” — object ที่​เก็บ​ทั้ง​ข้อมูล​และ business logic ไว้​ด้วย​กัน เป็น​คู่​ตรง​ข้าม​ของ Anemic Domain Model (ที่​มี​แต่ getter/setter แล้ว​ผลัก logic ไป​ไว้​ที่​อื่น) tactical patterns ของ DDD จะ​คุ้ม​ค่า​ก็​ต่อ​เมื่อ model “รวย” พอ​เช่น​นี้Tactical Design — domain เป็น​แค่ CRUD ธรรมดา — repository จะ​กลาย​เป็น​พิธีรีตอง​ที่​ซับซ้อน​เกิน​เหตุ กรณี​นี้​การ​ใช้ DAO ง่ายๆ หรือ​เรียก persistence framework ตรงๆ ย่อม​ดี​กว่า เช่น​เดียว​กับ design pattern ทุก​ตัว การนำ​ไป​ใช้​กับ​ทุก​ปัญหา​มี​แต่​จะ “เพิ่ม” ความ​ซับซ้อน ไม่ใช่​ลด​ทอน

ถ้า​ใช้​ฐาน​ข้อมูล​เชิง​สัมพันธ์​ร่วม​กับ ORM อยู่​แล้ว หลาย​คน​สงสัย​ว่า​จะ​มี repository ไป​ทำไม ใน​เมื่อ ORM ก็ abstract เทคโนโลยี​การ​จัด​เก็บ​ให้​แล้ว — ประเด็น​คือ ORM abstract เฉพาะ data model มัน​เพียง​แปลง​ตาราง​และ column ให้​กลาย​เป็น object เพื่อ​ให้​จัดการ​ใน code ได้​สะดวก​ขึ้น​เท่านั้น

Persistence ModelPersistence Modelmodel การ​จัด​เก็บ — โครงสร้าง​เก็บ​สถานะ​ของ domain model ณ เวลา​หนึ่ง อยู่​ใน​รูป​ของ​ตาราง​และ column (หรือ document) ใน​ฐาน​ข้อมูล ต่าง​จาก domain model ที่​รวย​ด้วย​พฤติกรรม​และ​ภาษา​ธุรกิจ ORM map ไป​ยัง persistence model ส่วน repository มีหน้าที่​กั้น2 model นี้​ไม่​ให้​ปน​กันArchitecture ที่ ORM map ไป​นั้น ต่าง​จาก domain model ของ​เรา​โดย​สิ้นเชิง:

  • domain model คือ abstraction ของ problem domain ที่​เต็ม​ไป​ด้วย​พฤติกรรม​และ​ภาษา ประกอบ​ด้วย EntityEntityอ็อบเจ็กต์ที่​มี “ตัวตน” (identity) ต่อ​เนื่อง​ตาม​เวลา แม้​ค่า​ภายใน​เปลี่ยน ก็​ยัง​เป็น​สิ่ง​เดิม เช่น ลูกค้า, คำ​สั่ง​ซื้อ — แยกแยะ​ด้วย id ไม่ใช่​ด้วย​ค่าTactical Design และ Value ObjectValue Objectอ็อบเจ็กต์ที่​สำคัญ​ที่ “ค่า” ของ​มัน ไม่มี​ตัวตน​เฉพาะ สอง​ตัว​ที่​ค่า​เท่า​กัน​ถือว่า​เท่า​กัน ควร​เป็น immutable (เปลี่ยน​ค่า​ไม่​ได้) เช่น เงิน, วัน​ที่, พิกัดTactical Design
  • persistence model คือ โครงสร้าง​สำหรับ​เก็บ​สถานะ ณ ช่วง​เวลา​หนึ่ง ประกอบ​ด้วย​ตาราง​และ column

รูปที่ 21-1: ORM ทำหน้าที่ map ระหว่าง domain model (aggregate ทางซ้าย) กับ persistence model (data model ทางขวา) ส่วน repository เป็นตัวกลางที่กั้นไม่ให้2 model นี้ปนเป็นก้อนเดียวกัน

บาง​ครั้ง2 model อาจ​ดู​คล้าย​กัน​มาก​จน​แทบ​จะ​เหมือน​กัน แต่​ใน​เชิง​แนวคิด​แล้ว​เป็น​คนละ​เรื่อง — ORM ไม่ใช่ repository แต่ repository สามารถ ใช้ ORM เป็น​เครื่องมือ​ใน​การ persist ได้ บทบาท​ของ repository คือ​กั้น2 model ให้​แยก​จาก​กัน ไม่​ปล่อย​ให้​พร่า​เลือน​จน​รวม​เป็น​หนึ่ง​เดียว

ประโยชน์​ของ​การ​แยก2 model

เมื่อ data model แยก​จาก domain model เรา​จะ​สามารถ วิวัฒน์ domain model ได้​โดย​ไม่​ต้อง​พะวง​เรื่อง​การ​จัด​เก็บ​ตลอด​เวลา สุดท้าย​แล้ว​มัน​ก็​ต้อง​ถูก persist และ​อาจ​ต้อง​ประนีประนอม​อยู่​บ้าง — แต่​ควร​ประนีประนอม “เมื่อ​จำ​เป็นจริงๆ เท่านั้น” และ​ทำ หลัง จาก​ออกแบบ model เสร็จ​แล้ว ไม่ใช่​ตั้งแต่​ต้น​พร้อม​กับ​การ​สร้าง model


นัก​พัฒนา​ชอบ reuse code จึง​มัก​นิยาม IRepository<TAggregate, TId> แบบ generic ที่​มี​ทุก method ครบ รวม​ถึง query method แบบ​เปิด​ให้​ขยาย​อย่าง FindAllMatching(Expression<...>) แล้ว​ให้ repository แต่ละ​ตัว​สืบทอด​ไป — แต่​วิธี​นี้​มี​ปัญหา​ร้ายแรง​อยู่​สอง​ข้อ

ปัญหา​ของ Generic Repository
public class CustomerRepository : ICustomerRepository
{
// aggregate บางตัวไม่รองรับ Remove แต่ interface บังคับให้มี
public void Remove(Customer aggregate)
{
throw new NotImplementedException(
"ลูกค้าถูกลบออกจาก collection ไม่ได้");
}
}
  1. มัน​ตั้ง​สมมติฐาน​ว่า​ทุก aggregate มี​พฤติกรรม​เหมือน​กัน — บาง aggregate เป็น read-only บาง​ตัว​ไม่​รองรับ​การ​ลบ สุดท้าย​จึง​ต้อง​โยน NotImplementedException ทิ้ง​ไว้ ซึ่ง​เป็น​สัญญาณ​ว่า​สัญญา​นี้​ผิด​ตั้งแต่​ต้น
  2. FindAllMatching คือ leaky abstraction — การ​เปิด​ให้​ส่ง query แบบ ad hoc เข้า​มา ทำให้​ควบคุม query ไม่​ได้ ปรับ fetching strategy ไม่​ได้ และ​นำ​ไป​สู่​ปัญหา performance ที่​รุนแรง​ใน​ฐาน​ข้อมูล​เชิง​สัมพันธ์

ทาง​ที่​ดี​กว่า​คือ นิยาม repository ตาม​ความ​ต้องการ​เฉพาะ​ของ​มัน และ​ตั้ง​ชื่อ query method ให้​ชัดเจน สื่อ​เจตนา

ICustomerRepository.cs — สัญญา​ที่​ชัดเจน สื่อ​เจตนา
public interface ICustomerRepository
{
Customer FindBy(Guid id);
IEnumerable<Customer> FindAllThatAreDeactivated();
void Add(Customer customer);
}
generic ยัง​มี​ที่ทาง​ของ​มัน — แค่​ไม่ใช่​ใน interface

generic repository ยัง​ใช้ได้​ดี แต่​ให้​ใช้ เบื้องหลัง implementation ผ่าน composition (ไม่ใช่ inheritance) เพื่อ​เลี่ยง code ซ้ำซ้อน คือ CustomerRepository ถือ IRepository<Customer, Guid> ไว้​ภายใน​แล้ว​เรียก​ใช้ — วิธี​นี้​ทำให้​ได้​ทั้ง​สัญญา​ที่​ชัดเจน และ การ reuse code

และ​ถ้า​คุณ​ใช้ persistence framework อยู่​แล้ว ไม่​จำเป็น​ต้อง​ไป abstract มัน​ซ้ำ​อีก​ชั้น ใน concrete implementation ของ repository สามารถ​เรียก RavenDB หรือ Entity Framework ตรงๆ ได้​เลย เพราะ​เรา​เพียง​ต้องการ​กัน “เรื่อง​เทคนิค” ออก​จาก domain model เท่านั้น ส่วน​อื่น​ของ app ไม่​จำเป็น​ต้อง​ซ่อน implementation — การ​ซ่อน​เกิน​เหตุ​มี​แต่​จะ​ทำให้​ผู้​อ่าน code สับสน


รูปร่าง​ของ aggregate, ขีด​ความ​สามารถ​ของ persistence framework และ​ข้อ​ที่​ว่า​เป็น​งาน greenfield หรือ brownfield — ทั้งหมด​ล้วน​มี​ผล​ต่อ​วิธี persist หลัก​ที่​ต้อง​ยึด​ไว้​คือ ออกแบบ domain object โดย​ไม่​คิดถึง​เรื่อง persist ให้​เป็น POCO/POJO ที่​ปลอด​จาก infrastructure code ให้​มาก​ที่สุด แล้ว​จึง​ค่อย​ประนีประนอม “เป็น​ทาง​เลือก​สุดท้าย”

ถ้า​เป็น​งาน greenfield และ​ใช้ ORM ที่​รองรับ persistence-ignorant object (อย่าง NHibernate ที่​ใช้ reflection ใน​การ persist object ที่ encapsulate อย่าง​เต็ม​ที่ และ​มี private getter/setter ได้) เรา​จะ map domain model ไป​ยัง data model ได้​โดยตรง บาง​ตัว​ขอ​เพียง parameterless constructor — เป็น​ราคา​เล็กน้อย​ที่​จ่าย​ได้

ถ้า​ใช้ document store ก็​ยิ่ง​ง่าย​ขึ้น​ไป​อีก — framework จะ serialize ทั้ง aggregate เป็น JSON เก็บ​ได้​เลย แทบ​ไม่​ต้อง​มี data model แยก​ต่างหาก​ด้วย​ซ้ำ

ถ้า framework ไม่​เอื้อ​ให้ domain model เป็น persistence-ignorant เรา​ต้อง​เลือก​หนึ่ง​ใน​กลยุทธ์​ต่อ​ไป​นี้ ซึ่ง​ล้วน ส่ง​ผลกระทบ ต่อ​รูปร่าง​ของ aggregate ไม่​มาก​ก็​น้อย

กลยุทธ์วิธี​ทำราคา​ที่​จ่าย
Public getters/settersเปิด property ทุก​ตัว​ให้​อ่าน/เขียน​ได้ เพื่อ​ให้ repository map สถานะ​ได้​ง่ายclient อาจ bypass method ธุรกิจ ทำให้ aggregate ตก​อยู่​ใน​สถานะ​ที่​ละเมิด invariant
Memento patternaggregate สร้าง snapshot ของ​สถานะ​ตัวเอง​ให้​นำ​ไป persist และ​รู้​วิธี hydrate กลับ​จาก snapshot นั้นต้อง​เขียน code snapshot/restore เพิ่ม แต่​ยัง​คง encapsulate ได้​อย่าง​เต็ม​ที่
Event streamrepository persist ทุก event ที่​เกิด​กับ aggregate (ดู Event SourcingEvent Sourcingเก็บ​ทุก​การ​เปลี่ยน​สถานะ​เป็น event ที่​เปลี่ยน​ไม่​ได้ แล้ว​สร้าง​สถานะ​ปัจจุบัน​ด้วย​การ​เล่น event ซ้ำ มัก “ต้อง​ใช้​คู่​กับ CQRS” (Greg Young) มี event stream ต่อ1 AggregateArchitecture ใน​บท​ถัด​ไป) แล้ว replay เพื่อ​สร้าง aggregate กลับซับซ้อน​กว่า แต่​ได้​ประวัติการ​เปลี่ยนแปลง​ครบถ้วน

จุด​สำคัญ​ที่​หลาย​คน​พลาด: snapshot ใน memento ไม่ใช่ data model — มัน​คือ “สถานะ​ของ aggregate” ที่​ยัง​ปลอด​จาก persistence framework โดย repository ยัง​คง​ต้อง map snapshot นั้น​ไป​ยัง data model อีก​ชั้น​หนึ่ง

🛒 Memento กับ​ตะกร้า​สินค้า (Basket)

สมมติ​ว่า Basket ต้องการ encapsulate property ทั้งหมด (ไม่​ต้องการ​เปิด public setter) เรา​จึง​ให้​มัน​สร้าง BasketSnapshot ที่​บรรจุ​รายการ​สินค้า​และ​ค่า​จัด​ส่ง จาก​นั้น BasketRepository จะ​รับ snapshot ไป map ลง data model และ​เมื่อ​ดึง​กลับ repository จะ​อ่าน data model → แปลง​เป็น snapshot → เรียก Basket.CreateBasketFrom(snapshot) ผล​คือ Basket ไม่​เคย​รู้จัก​ฐาน​ข้อมูล​เลย แต่​ก็​ไม่​ต้อง​เปิด property ให้​โลก​ภายนอก​มา​ทำลาย​กฎ​ทาง​ธุรกิจ

เริ่ม​จาก​บริสุทธิ์​ก่อน แล้ว​ค่อย​ปฏิบัติ​นิยม

เริ่ม​จาก​บริสุทธิ์​ก่อน (start pure) แล้ว​ค่อย​เป็น​นัก​ปฏิบัติ​นิยม​เมื่อ​จำเป็น (be pragmatic when you need to)

— Scott Millett & Nick Tune

อย่า​ปล่อย​ให้ technical framework มา​ขับ​เคลื่อน​การ​ออกแบบ จง​คิด​ใน​รูป​ของ aggregate และ​การ​ดึง​ข้อมูล​ที่​ชัดเจน ไม่ใช่ ad hoc query


สัญญา​ของ repository เป็น​มากกว่า interface แบบ CRUD — มัน​คือ ส่วน​ต่อ​ขยาย​ของ domain model ที่​เขียน​ด้วย​ภาษา​ที่ domain expert เข้าใจ และ​ควร​สร้าง​ขึ้น​จาก “ความ​ต้องการ​ของ use case” ไม่ใช่​จาก​มุมมอง​ของ data access

เทียบ​สอง​สัญญา​นี้

แย่: query method เปิด​ให้​ขยาย — ไม่​บอก​อะไร​เลย
public interface ICustomerRepository
{
Customer FindBy(Guid id);
IEnumerable<Customer> FindAllThatMatch(Query query); // กว้างเกินไป
IEnumerable<Customer> FindAllThatMatch(String hql); // รั่ว HQL ออกมา
void Add(Customer customer);
}
ดี: query method ชัดเจน สื่อ​เจตนา​ธุรกิจ
public interface ICustomerRepository
{
Customer FindBy(Guid id);
IEnumerable<Customer> FindAllThatAreDeactivated();
IEnumerable<Customer> FindAllThatAreOverAllowedCredit();
void Add(Customer customer);
}

สัญญา​แบบ​หลัง​ใช้​หลัก Tell, Don’t Ask — สั่ง​ให้ repository ทำงาน​หนัก​โดย abstract ตัว query เอา​ไว้ ทำให้​เห็น​ชัด​ว่า​จะ optimize query แต่ละ​ตัว​ที่​ชั้น infrastructure ได้​อย่างไร repository จึง​ไม่ใช่ object ทั่วไป แต่​เป็น ขอบเขต​เชิง​กระบวนการ (procedural boundary) ที่​ต้อง​ตั้ง​ชื่อ method อย่าง​พิถีพิถัน​ไม่​แพ้ object ใน domain


repository หนึ่ง​ตัว​ดูแล aggregate root เพียง ชนิด​เดียว แต่ business case หนึ่ง​อาจ​ต้อง​อัปเดต aggregate หลาย​ชนิด การ​จัดการ transaction จึง​เป็น​หน้าที่​ของ Unit of Work ซึ่ง​คอย​ติดตาม​การ​เปลี่ยนแปลง​ทั้งหมด​ใน​ระหว่าง​ทำ task หนึ่ง แล้ว​ประสาน​การ​บันทึก​ลง store ภายใน transaction เดียว — หาก​มี​อะไร​พลาด​กลาง​ทาง ก็จะ rollback ทั้งหมด​เพื่อ​รักษา​ความ​ถูกต้อง​ของ​ข้อมูล

ใน NHibernate นั้น unit of work ถูก implement ด้วย ISession รูป​ด้าน​ล่าง​แสดง​ลำดับ​การ​ทำงาน​เมื่อ application service หนึ่ง​ใช้ repository สอง​ตัว (OrderRepository และ LoyaltyAccountRepository) ร่วม​กัน​ใน​การ​สะสม​แต้ม

รูปที่ 21-6: ลำดับการทำงานของ Unit of Work — application service เปิด transaction, repository ดึง aggregate ผ่าน session เดียวกัน, session คอย track การเปลี่ยนแปลง แล้ว commit ครั้งเดียวเพื่อ persist ทั้ง Order และ LoyaltyAccount ภายใน transaction เดียว

กุญแจ​สำคัญ​คือ repository ทุก​ตัว ต้อง​ใช้ session instance เดียวกัน กับ​ที่ application service ใช้ จึง​จะ​อยู่​ใน transaction เดียวกัน ซึ่ง​ทำได้​ด้วย​การ inject session ผ่าน constructor หรือ setter

แยก​ความ​รับผิดชอบ Unit of Work ออก​จาก Repository

unit of work เป็น “เรื่อง​เทคนิค​ล้วนๆ” ไม่​เกี่ยว​กับ domain การ​ยัด method อย่าง EnlistIn(session) ลงใน interface ของ repository โดยตรง​จึง​ไม่​สม​เหตุ​สม​ผล ให้​ใช้​หลัก interface segregation แยก​ออก​เป็น IEnlistInAUnitOfWork ต่างหาก เพื่อ​ไม่​ให้​สัญญา​ของ domain แปดเปื้อน​ด้วย​เรื่อง session

ใน​อุดมคติ repository ควร​ทำตัว​เหมือน collection ใน .NET — collection ถือ reference ไป​ยัง object ดังนั้น​การ​แก้ object จึง​ไม่​จำเป็น​ต้อง “อัปเดต” กลับ​เข้า collection นั่น​หมายความ​ว่า ไม่​ควร​มี method Save/Update แต่​ใน​ทาง​ปฏิบัติ​สิ่ง​นี้​ขึ้น​อยู่​กับ framework

  • framework ที่ track การ​เปลี่ยนแปลง​ได้ (เช่น NHibernate) — ไม่​จำเป็น​ต้อง​มี Save เพราะ​การ​แก้ aggregate จะ​ถูก push ลง store ให้​เอง​เมื่อ unit of work ถูก commit
  • framework ที่ track ไม่​ได้ (micro ORM, raw ADO.NET) — จำเป็น​ต้อง​มี Save บน​สัญญา และ application service ต้อง​เรียก​มัน​ด้วย​ตัวเอง​หลัง​จาก​แก้ aggregate
อย่า​ใส่ dirty flag ลงใน domain object

อย่า​เผลอ​ใส่ flag ที่​บอกว่า object “ถูก​แก้​แล้ว” ลงใน domain object เพราะ​นั่น​คือ​การนำ​เรื่อง persistence เข้าไป​ทำให้ domain model ขุ่น​มัว จง​ปล่อย​ให้การ track การ​เปลี่ยนแปลง​เป็น​หน้าที่​ของ unit of work และ repository ทั้งหมด


ใน​งาน brownfield ที่​ต้อง​เชื่อม​กับ persistence store เดิม repository ช่วย​รักษา domain model ให้​บริสุทธิ์​ได้ โดย​ทำตัว​เป็น Anticorruption LayerAnticorruption Layer (ACL)ชั้น​กั้น​ที่​ทีม​ปลาย​น้ำ​สร้าง​ขึ้น​เพื่อ​แปลง model ของ​ระบบ​อื่น​ให้​เป็น​ภาษา​ของ model ตัวเอง ป้องกัน​ไม่​ให้ model ภายนอก​มา “ปน​เปื้อน” model เราStrategic Design — สร้าง model ใหม่​ที่​รูปร่าง​ไม่​ถูก​กำหนด​โดย​ความ​ซับซ้อน​ของ infrastructure เดิม

repository map ไป​ยัง aggregate ไม่ใช่​ตาราง — entity หลาย​ตัว​อาจ​อยู่​ใน​ตาราง​เดียวกัน หรือ aggregate หนึ่ง​อาจ​กระจาย​อยู่​หลาย​ตาราง หลาย​ฐาน​ข้อมูล หรือ​แม้แต่ flat file, web service และ NoSQL ปะปน​กัน ไม่​ว่า store จะ​มีหน้าตา​อย่างไร มัน​ต้อง​ไม่​เป็น​ตัว​กำหนด​รูปร่าง​ของ domain model

รูปที่ 21-7: repository ทำตัวเป็น anticorruption layer — รับ aggregate ที่บริสุทธิ์จากฝั่ง domain แล้วประสานการ persist ไปยัง legacy data store หลายแห่ง โดย domain model ไม่ต้องรับรู้ความซับซ้อนของระบบเก่าเลย


นอกจาก persist และ retrieve แล้ว repository ยัง​เปิดเผย​ข้อมูล​สรุปอื่นๆ เกี่ยว​กับ collection ของ​มัน​ได้ เช่น

  • นับ​จำนวน aggregate — เพิ่ม method int Count() ลง​ใน​สัญญา
  • สร้าง entity ID — ถ้า ID ถูก seed โดย​ฐาน​ข้อมูล​หรือ infrastructure service เรา​สามารถ abstract มัน​ไว้​หลัง repository เช่น int GenerateId() ให้ application service เรียก​ใช้​อย่าง​ชัดเจน หรือ​จะ​ให้ store สร้าง ID ให้​ตอน persist ก็ได้
ICustomerRepository.cs — method สรุป collection และ​สร้าง ID
public interface ICustomerRepository
{
Customer FindBy(Guid id);
IEnumerable<Customer> FindAllThatAreDeactivated();
void Add(Customer customer);
int Count();
int GenerateId();
}
สรุป​แก่น​ของ Repository

Repository คือ collection façade ที่​กั้น domain model ออก​จาก persistence model โดย​เข้าถึง​ได้​ผ่าน aggregate root เท่านั้น สัญญา​ของ​มัน​เขียน​ด้วย​ภาษา​ธุรกิจ​และ​สื่อ​เจตนา​อย่าง​ชัดเจน (ไม่ใช่ CRUD ดิบๆ) หลัก​คือ ออกแบบ domain ให้​บริสุทธิ์​ก่อน แล้ว​จึง​เลือก​กลยุทธ์ persist (map ผ่าน ORM โดยตรง, memento หรือ event stream) ตาม​ขีด​ความ​สามารถ​ของ framework — และ​ประนีประนอม​เมื่อ​จำ​เป็นจริงๆ เท่านั้น


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

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

  • Repository — คำ​อธิบาย​แก่น​ของ pattern นี้​โดยตรง ตรง​กับ​สิ่ง​ที่​บท​นี้​พูด​ถึง​ตลอด: collection façade ที่​กั้น domain model ออก​จาก persistence
  • Unit of Work — ขยาย​ความ​เรื่อง​การ​ประสาน transaction ข้าม repository หลาย​ตัว ที่​บท​นี้​อธิบาย​ไว้​ใน​หัวข้อ “Transaction และ Unit of Work”
  • Anti-Corruption Layer — บท​นี้​กล่าว​ถึง repository ใน​ฐานะ anticorruption layer สำหรับ​งาน brownfield โดยตรง หน้า​นี้​ขยาย​ความ​ว่า ACL คือ​อะไร​ใน​บริบท​กว้าง​ขึ้น

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

ข้อ 1 / 4

ตามหนังสือ เป้าหมาย ‘ที่แท้จริง’ ของ repository pattern คืออะไร?