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

async/await และ Task — I/O อยู่​ที่​ขอบ

เจ็ด​บท​ที่​ผ่าน​มา​เรา​ปั้น domain Order จน​ครบ — Money, OrderId, Quantity, OrderLine, ตัว Order ที่​รักษา​กฎ​ธุรกิจ, OrderStatus ที่​คุม​สถานะ และ port (IPaymentGateway, IOrderRepository) ที่​ให้ domain คุย​กับ​โลก​ภายนอก​ได้​โดย​ไม่​ผูก​กับ​ของ​จริง แต่​ใน​ทุก port มี​ของ​สาม​อย่าง​ที่​เรา​เลื่อน​คำ​อธิบาย​มา​ตลอด: Task, async/await และ CancellationToken

บท​สุดท้าย​นี้​เก็บ​ปม​นั้น​ให้​จบ แล้ว​ปิด​คอร์ส​ด้วย​แผนที่​ว่า feature ทุก​อย่าง​ที่​เรียน​มา​ไป​ต่อยอด​ที่​คอร์ส​ไหน​ของ​สาย .NET/DDD หัวใจ​ของ​บท​มี​ประโยค​เดียว: แกน domain ตัดสิน​ใจ​ล้วนๆ จึง​เป็น synchronous ส่วน async อยู่​ที่ “ขอบ” ที่​ต้อง​คุย​กับ I/O จริง

📦 code ตัวอย่าง (กำลัง​จัด​ทำ)

code เต็ม​ของ​คอร์ส​นี้​อยู่​ที่ repo kaen-food-ordering (กำลัง​จัด​ทำ) — บท​นี้​เกี่ยวข้อง​กับ port ใน FoodOrdering.Domain/Orders/ และ​ตัว​ประสาน​งาน​ที่ FoodOrdering.Application/Orders/

งาน​บาง​อย่าง​เสร็จ​ทันที เช่น บวก​เลข​หรือ​เทียบ​ค่า แต่​บาง​งาน “ใช้​เวลา” เพราะ​ต้อง​รอ​คน​อื่น เช่น ถาม​ฐาน​ข้อมูล​หรือ​ยิง API ไป​ตัด​เงิน​ที่​ธนาคาร ระหว่าง​ที่​รอ ถ้า​เรา​ปล่อย​ให้ thread (สาย​การ​ทำงาน​หนึ่ง​เส้น​ของ​โปรแกรม) ยืน​เฉยๆ รอ​เป็น​วินาที ก็​เท่ากับ​เปลือง​แรงงานไปฟรีๆ — thread นั้น​ทำ​อย่าง​อื่น​ได้​อีก​เยอะ​ระหว่าง​รอ

Async and AwaitAsync and Awaitkeyword คู่​ที่​ทำให้​เขียน code รอ​ผลลัพธ์​ที่​ใช้​เวลา (เช่น เรียก​ฐาน​ข้อมูล​หรือ API ภายนอก) แบบ​ไม่ block thread — method ที่​มี `async` คืน​ค่า​เป็น `Task`/`Task<T>` และ `await` คือ​จุด 'หยุด​รอ' ผล​โดย​ไม่​ค้าง thread ไว้​เฉย ๆ จำเป็น​เมื่อ Repository หรือ port อื่น​ต้อง​คุย​กับ​โลก​ภายนอกArchitecture คือ keyword คู่​ของ C# ที่​แก้​ปัญหา​นี้ method ที่​ติด​คำ​ว่า async จะ​คืน​ค่า​เป็น Task (“งาน​ที่​ยัง​ทำ​ไม่​เสร็จ”) และ await คือ​จุด “หยุด​รอ​ผล” ที่ ปล่อย thread ให้​ไป​ทำงาน​อื่น​ก่อน แล้ว​ค่อย​กลับ​มา​ทำ​ต่อ​เมื่อ​ผล​พร้อม ไม่ใช่​ยืน​ตรึง​รอ

// async บอกว่า method นี้มีจุด "หยุดรอ" ข้างใน และคืนค่าเป็น Task
public async Task<int> CountPlacedAsync(IOrderRepository orders, CancellationToken ct)
{
// await = หยุดรอผลของงานที่ใช้เวลา โดยปล่อย thread ให้ไปทำอย่างอื่นระหว่างรอ
IReadOnlyList<Order> all = await orders.FindByStatusAsync(OrderStatus.Placed, ct);
return all.Count; // พอผลกลับมาแล้ว บรรทัดนี้ค่อยทำงานต่อ
}

แกะ​ที​ละ​ส่วน:

  • async หน้า method บอก compiler ว่า “ข้าง​ใน​มี await นะ” — เป็น​ใบอนุญาต​ให้​ใช้ await ได้
  • Task<int> คือ​ชนิด​ที่​คืน​ออก อ่าน​ว่า “งาน​ที่​พอ​เสร็จ​แล้ว​จะ​ได้ int หนึ่ง​ตัว” ถ้า method ไม่มี​ผลลัพธ์​จะ​คืน​แค่ Task เฉยๆ (งาน​ที่​เสร็จ​แล้ว​ไม่​ได้​ค่า​อะไร​กลับ​มา)
  • await orders.FindByStatusAsync(...)FindByStatusAsync คืน Task<IReadOnlyList<Order>> การ​ใส่ await ข้าง​หน้า​แปล​ว่า “หยุด​ตรง​นี้​จนกว่า​งาน​จะ​เสร็จ แล้ว​แกะ​ผล​ข้าง​ใน​ออก​มา” ผล​ที่​ได้​จึง​เป็น IReadOnlyList<Order> ตรงๆ ไม่ใช่ Task อีก​ต่อ​ไป

ข้อ​สำคัญ​ที่​ต้อง​จำ: await ไม่​ได้​ทำให้​โปรแกรม “ช้า​ลง” มัน​แค่​ทำให้ thread ไม่​ต้อง​ยืน​รอ​เปล่าๆ ระหว่าง​ที่​งาน I/O กำลัง​ดำเนิน​อยู่ — เป็นการ​ใช้​แรงงาน​ที่​มี​อยู่​ให้​คุ้ม​ขึ้น ไม่ใช่​เพิ่ม​เวลา

Task คือ “กล่อง” ที่​ห่อ​งาน​ซึ่ง​ยัง​ทำ​ไม่​เสร็จ​เอา​ไว้ มี​สอง​แบบ​ที่​เจอ​บ่อย:

  • Task — งาน​ที่​ทำ​เสร็จ​แล้ว ไม่มี ค่า​กลับ​มา เช่น ChargeAsync (ตัด​เงิน​เสร็จ​ก็​จบ ไม่​ต้อง​คืน​อะไร)
  • Task<T> — งาน​ที่​ทำ​เสร็จ​แล้ว มี ผล​เป็น​ชนิด T เช่น Task<Order?> (หา​ออเดอร์​เสร็จ​แล้ว​ได้ Order? กลับ​มา)

มี​ญาติ​อีก​ตัว​ชื่อ ValueTask / ValueTask<T> หน้าตา​ใช้งาน​เหมือน Task เป๊ะ แต่​ออกแบบ​มา​ให้ “เบา​กว่า” ใน​กรณี​ที่​ผลลัพธ์​มัก พร้อม​อยู่​แล้ว​ทันที โดย​ไม่​ต้อง​รอ​จริง เช่น ค่าที่ cache ไว้​ใน​หน่วย​ความ​จำ การ​ใช้ ValueTask ใน​เคส​นั้น​ช่วย​ลด​การ​สร้าง​อ็อบเจ็กต์ (allocation) ที่​ไม่​จำเป็น สำหรับ​ตอน​นี้​จำ​แค่​ว่า ค่า​เริ่มต้น​ให้​ใช้ Task ก่อน​เสมอ แล้ว​ค่อย​เปลี่ยน​เป็น ValueTask เฉพาะ​จุด​ที่​วัด​แล้ว​ว่า​คุ้ม — อย่า​เพิ่ง​เสีย​เวลา​กับ​มัน​ใน​ฐานะ​ผู้​เริ่มต้น

สังเกต​ว่า​ทุก port ใน​บท​ที่ 7 รับ parameter ตัว​สุดท้าย​ชื่อ ct เสมอ นั่น​คือ Cancellation TokenCancellation Tokenอ็อบเจ็กต์ (`CancellationToken`) ที่​ส่ง​ต่อ​เข้าไป​ใน method async เพื่อ​ให้​ผู้​เรียก 'ยกเลิก' งาน​ที่​กำลัง​ทำ​อยู่​ได้ เช่น ผู้​ใช้​ปิด​หน้า​เว็บ​ระหว่าง​รอ​ผล — method ที่​ดี​ควร​รับ parameter นี้​และ​ส่ง​ต่อ​ไป​ให้​ทุก call ข้าง​ใน​ด้วย (เช่น query ฐาน​ข้อมูล)Architecture — อ็อบเจ็กต์ที่​ทำ​หน้าที่​เป็น “ปุ่ม​ยกเลิก” ส่ง​ต่อ​เข้าไป​ใน​งาน async เพื่อ​ให้​ผู้​เรียก สั่ง​เลิก งาน​ที่​กำลัง​ทำ​อยู่​ได้​ก่อน​มัน​จะ​เสร็จ

ทำไม​ถึง​ต้อง​มี? ลอง​นึก​ภาพ: ผู้​ใช้​กด​โหลด​รายการออเดอร์ แล้ว​ปิด​หน้า​เว็บ​ทิ้ง​ไป​ก่อน​ผล​จะ​กลับ​มา ถ้า​ไม่มี​ปุ่ม​ยกเลิก server ก็​ยัง​ขยัน​ถาม​ฐาน​ข้อมูล​ต่อ​ทั้ง​ที่​ไม่มี​ใคร​รอ​ผล​แล้ว — เปลืองทรัพยากรฟรีๆ CancellationToken แก้​เรื่อง​นี้: พอ​ผู้​ใช้​ยกเลิก token จะ​ถูก “จุด​สัญญาณ” และ​งาน​ที่​กำลัง​ทำ​อยู่​จะ​หยุด​เอง​อย่าง​สุภาพ

กฎ​ง่ายๆ ของ​การ​เขียน method async ที่​ดี คือ รับ CancellationToken เข้า​มา​แล้ว​ส่ง​ต่อ​ให้​ทุก call ข้าง​ใน​ด้วย อย่า​ปล่อย​ให้​มัน​ตกหล่น​กลาง​ทาง:

public async Task<Money> LoadTotalAsync(
IOrderRepository orders, OrderId id, CancellationToken ct)
{
// ส่ง ct ต่อเข้าไปในทุกงาน I/O เพื่อให้ยกเลิกได้ทั้งสาย
Order order = await orders.FindAsync(id.Value, ct)
?? throw new InvalidOperationException("ไม่พบออเดอร์นี้");
return order.Total; // ตรงนี้ sync ล้วน — แค่คำนวณ ไม่มี I/O ให้ยกเลิก
}

?? throw ... ที่​เห็น​คือ null-coalescing (บท​ที่ 3) — FindAsync คืน Order? ถ้า​เป็น null (ไม่​เจอ) ก็​โยน exception ทันที ไม่​ปล่อย​ให้ null ไหล​ต่อ

Task<IReadOnlyList<Order>> เหมาะ​กับ “โหลด​ผล​มา​ให้​ครบ​เป็น​ก้อน​เดียว” แต่​บาง​ครั้ง​ผล​มี​เป็น​หมื่น​แถว และ​เรา​อยาก​ประมวล​ผล ที​ละ​ชิ้น​ตาม​ที่​มัน​ไหล​เข้า​มา โดย​ไม่​ต้องอม​ทั้งหมด​ไว้​ใน​หน่วย​ความ​จำ​ก่อน — นี่​คือ​งาน​ของ IAsyncEnumerable<T> คือ “ลำดับ​ของ​ค่าที่​ทยอย​มา​แบบ async”

ฝั่ง port ประกาศ​เป็น​แบบ​นี้ (ไม่มี Task ห่อ เพราะ​มัน​คืน “สายพาน​ที่​ไหล​เรื่อยๆ” ไม่ใช่ “ผล​ก้อน​เดียว”):

public interface IOrderStream
{
IAsyncEnumerable<Order> StreamPlacedAsync(CancellationToken ct);
}

ฝั่ง​เรียก​ใช้ ใช้ await foreach — เหมือน foreach ธรรมดา แต่​เติม await เพราะ​แต่ละ​ชิ้น​ทยอย​มา​แบบ async:

await foreach (Order order in stream.StreamPlacedAsync(ct))
{
// จัดการทีละออเดอร์ที่ไหลเข้ามา ไม่ต้องรอโหลดครบทั้งหมื่นแถวก่อน
Console.WriteLine(order.Total);
}

สำหรับ​ผู้​เริ่มต้น จำ​แค่​ว่า IAsyncEnumerable = “stream ข้อมูล async” และ​คู่หู​ของ​มัน​คือ await foreach ส่วน​ใหญ่​คุณ​จะ​ได้​ใช้​ตอน​อ่าน​ผลลัพธ์​จำนวน​มาก​จาก​ฐาน​ข้อมูล ซึ่ง​จะ​เจอ​เต็มๆ ใน​คอร์ส EF Core

ทวน port จาก​บท​ที่ 7 อีก​ครั้ง คราว​นี้​เข้าใจ​ทุก​ส่วน​แล้ว:

namespace FoodOrdering.Domain.Orders;
public interface IPaymentGateway
{
Task ChargeAsync(Money amount, CancellationToken ct);
Task RefundAsync(Money amount, CancellationToken ct);
}
public interface IOrderRepository : IRepository<Order>
{
// สืบทอด FindAsync/AddAsync จาก IRepository<Order> มาแล้ว
Task<IReadOnlyList<Order>> FindByStatusAsync(OrderStatus status, CancellationToken ct);
}

ทุก method ของ port มี​สาม​ลักษณะ​ร่วม​กัน และ​ตอน​นี้​เรา​รู้​แล้ว​ว่า​ทำไม:

  • คืน Task หรือ Task<T> — เพราะ​ปลายทาง​จริง​คือ I/O (ตัด​เงิน​ที่​ธนาคาร, อ่าน/เขียน​ฐาน​ข้อมูล) ซึ่ง​ใช้​เวลา จึง​ต้อง​เป็น​งาน async
  • ลงท้าย​ชื่อ​ด้วย Async — เป็น​ธรรมเนียม​ของ .NET ที่​ทำให้​เห็น​ปุ๊บ​ว่า “นี่​คือ​งาน async ต้อง await” เช่น ChargeAsync, FindAsync, AddAsync (method บันทึก​ลง​ที่​เก็บ บางที​ก็​ตั้ง​ชื่อ SaveAsync)
  • รับ CancellationToken เป็น parameter สุดท้าย — เพื่อ​ให้​ยกเลิก​งาน​ที่​กำลัง​คุย​กับ​โลก​ภายนอก​ได้

port จึง​เป็น “ประตู​ออก​สู่ I/O” ของ domain โดย​ธรรมชาติ และ​เพราะ I/O เป็น async ตัว​ประตู​จึง​พูด​ภาษา async ทั้งหมด — แต่​โปรด​สังเกต​ว่า Money, Order, OrderStatus ที่​มัน​รับ​ส่ง​นั้น​ไม่มี​ตัว​ไหน​เป็น async เลย ของ​พวก​นั้น​คือ​แกน domain ที่​เป็น sync ล้วน

นี่​คือ​หัวใจ​ของ​บท ลอง​แยก​งาน​สอง​ชนิด​ออก​จาก​กัน:

  • การ​ตัดสิน​ใจ — “ตะกร้า​ว่าง​สร้าง​ออเดอร์​ไม่​ได้”, “ราคา​รวม​เท่าไร”, “จาก​สถานะ​นี้​ไป​สถานะ​ไหน​ได้” งาน​พวก​นี้​เป็น​แค่​การ​คำนวณ​และ​เช็กกฎ ใน​หน่วย​ความ​จำ​ล้วนๆ ไม่​รอ​ใคร ไม่​แตะ I/O จึง​เป็น synchronous ธรรมชาติ ไม่มี await สัก​ตัว
  • การ​คุย​กับ​โลก​ภายนอก — อ่านออเดอร์​จาก​ฐาน​ข้อมูล, ตัด​เงิน​ที่​ธนาคาร, บันทึก​ผล งาน​พวก​นี้ ใช้​เวลา​และ​รอ​คน​อื่น จึง​ต้อง​เป็น async

หลักการ​คือ: เก็บ​การ​ตัดสิน​ใจ​ไว้​ใน​แกน domain ที่ sync ล้วน แล้ว​ผลัก I/O ออก​ไป​ไว้​ที่​ขอบ​ให้ async จัดการ ตัว​ประสาน​งาน (handler) ที่​ขอบ​ทำ​หน้าที่​เหมือน​แซนด์วิช — await โหลด​ข้อมูล​เข้า​มา, เรียก domain ตัดสิน​ใจ​แบบ sync, แล้ว await ส่ง​ผลออก​ไป:

namespace FoodOrdering.Application.Orders;
public class CheckoutHandler(IOrderRepository orders, IPaymentGateway payments)
{
public async Task CheckoutAsync(OrderId id, CancellationToken ct)
{
// 1) ขอบ (async): อ่านออเดอร์จากฐานข้อมูล — งาน I/O ต้อง await
Order order = await orders.FindAsync(id.Value, ct)
?? throw new InvalidOperationException("ไม่พบออเดอร์นี้");
// 2) แกน domain (sync): คำนวณยอดที่ต้องจ่าย — ตัดสินใจล้วน ไม่มี await
Money total = order.Total;
// 3) ขอบ (async): ตัดเงินแล้วบันทึก — งาน I/O ต้อง await อีกครั้ง
await payments.ChargeAsync(total, ct);
await orders.AddAsync(order, ct);
}
}

สังเกต​บรรทัด 2: order.Total ไม่มี await เพราะ​มัน​คือ​การ​บวก​ราคา​ใน​หน่วย​ความ​จำ (LINQ Sum จาก​บท​ที่ 6) — เป็นการ​ตัดสิน​ใจ ไม่ใช่ I/O การ​ที่ domain ไม่​ต้อง​รู้จัก async เลย​ทำให้​มัน test ง่าย​มาก: เรียกกฎธุรกิจตรงๆ แล้ว​เช็ก​ผล​ได้​ทันที ไม่​ต้อง​ตั้ง​ฐาน​ข้อมูล​ปลอม​หรือ mock อะไร

flowchart TB
  subgraph EDGEIN["ขอบขาเข้า — async"]
    HANDLER["CheckoutHandler.CheckoutAsync<br/>await ทุกจุดที่แตะ I/O"]
  end
  subgraph CORE["แกน domain — synchronous<br/>ตัดสินใจล้วน ไม่มี await"]
    DEC["Order.Place · Total<br/>คำนวณและรักษากฎในหน่วยความจำ"]
  end
  subgraph EDGEOUT["ขอบขาออก — async · port/adapter"]
    REPO["IOrderRepository<br/>FindAsync · AddAsync"]
    PAY["IPaymentGateway<br/>ChargeAsync"]
  end
  HANDLER -->|await FindAsync| REPO
  REPO -->|ข้อมูลดิบ| HANDLER
  HANDLER -->|เรียกแบบ sync| DEC
  DEC -->|ผลการตัดสินใจ| HANDLER
  HANDLER -->|await ChargeAsync| PAY
  HANDLER -->|await AddAsync| REPO

คำ​บรรยาย​ภาพ: กล่อง​กลาง (แกน domain) เป็น synchronous ล้วน — Order.Place กับ Total แค่​คำนวณ​และ​รักษา​กฎ​ใน​หน่วย​ความ​จำ ไม่มี await เลย ส่วน​งาน I/O ทั้งหมด​ถูก​ผลัก​ออก​ไป​อยู่​ที่ ขอบ: CheckoutHandler ที่​ขอบ​เป็น​ตัว​เดียว​ที่ await — มัน await โหลด​ข้อมูล​เข้า​มา​จาก port, ยื่น​ให้​แกน domain ตัดสิน​ใจ​แบบ sync, แล้ว await ส่ง​ผลออก​ไป​ตัด​เงิน​และ​บันทึก ทิศทาง​ชัดเจน: async ห่อ​อยู่​รอบ​นอก, การ​ตัดสิน​ใจ​อยู่​แกน​ใน สลับ​กัน​ไม่​ได้

มี​กับดัก​หนึ่ง​ที่​ผู้​เริ่มต้น​ตก​บ่อย​มาก: มี Task อยู่​ใน​มือ​แล้ว​อยาก​ได้​ผล “เดี๋ยวนี้” เลย​เผลอ​เรียก .Result หรือ .Wait() เพื่อ​ดึง​ผลออก​มา​แบบ sync นี่​คือ ❌ version ดิบ ที่​ควร​เลิก:

// ❌ version ดิบ: block thread ด้วย .Result / .Wait() — เสี่ยง deadlock
public class BlockingCheckout(IOrderRepository orders, IPaymentGateway payments)
{
public void Checkout(OrderId id)
{
// .Result สั่ง "หยุดตรงนี้จนกว่างานจะเสร็จ" โดยตรึง thread ไว้เฉย ๆ
Order order = orders.FindAsync(id.Value, CancellationToken.None).Result
?? throw new InvalidOperationException("ไม่พบออเดอร์นี้");
// .Wait() ก็ block แบบเดียวกัน
payments.ChargeAsync(order.Total, CancellationToken.None).Wait();
}
}

ต่าง​จาก await ตรง​ที่ await ปล่อย thread ให้​ไป​ทำงาน​อื่น​ระหว่าง​รอ แต่ .Result และ .Wait() ตรึง thread ไว้​เฉยๆ จนกว่า​งาน​จะ​เสร็จ ปัญห​า​มี​สอง​ชั้น:

  • เปลือง thread — ตรึง thread ไว้​รอ​เป็น​วินาที ทั้ง​ที่​มัน​ไป​รับ​งาน​คน​อื่น​ได้ พอ​โหลด​เยอะๆ thread หมด ระบบ​ก็​อืด​หรือ​ค้าง
  • เสี่ยง deadlock (ค้าง​ถาวร) — ใน​บาง​บริบท (เช่น UI หรือ ASP.NET แบบ​เดิม) งาน​ที่ await ข้าง​ใน​รอ “คิว” เดิม​กลับ​มา​ทำงาน​ต่อ แต่​คิว​นั้น​ถูก .Result ตรึง​ไว้​ไม่​ปล่อย — ต่าง​ฝ่าย​ต่าง​รอ​กัน​ไป​มา​แบบ​ไม่มี​วัน​หลุด โปรแกรม ค้าง​สนิท ไม่ใช่​แค่​ช้า

ทาง​แก้​ไม่มี​อะไร​ซับซ้อน: async ต้อง​ต่อ async ไป​ตลอด​สาย ถ้า method ของ​คุณ​เรียก​งาน async ก็​ทำให้ method นั้น​เป็น async แล้ว​ใช้ await — อย่า​ตัด​จบ​ด้วย .Result/.Wait() เขียน version ที่​ถูก​ก็​แค่:

// ✅ async ต่อ async: ปล่อย thread ระหว่างรอ ไม่ตรึง ไม่ค้าง
public async Task CheckoutAsync(OrderId id, CancellationToken ct)
{
Order order = await orders.FindAsync(id.Value, ct)
?? throw new InvalidOperationException("ไม่พบออเดอร์นี้");
await payments.ChargeAsync(order.Total, ct);
}

จบ​แปดบท คุณ​ปั้น domain Order ขึ้น​มา​จาก​ศูนย์ พร้อม​กับ​เรียน​ไวยากรณ์ C# สมัย​ใหม่​ที​ละ​ชิ้น ทุก feature ที่​เรียน​ไม่ใช่​ของ​จบ​ใน​ตัว — มัน​คือ “ภาษา” ที่​คอร์ส​อื่น​ใน​สาย .NET/DDD หยิบ​ไป​ใช้​ต่อลึกๆ ตาราง​นี้​คือ​แผนที่​ว่า​อัน​ไหน​ไป​โผล่​ที่ไหน:

feature C# ที่​เรียน (บท)ต่อยอด​ลึกๆ ที่​คอร์ส
record → Value Object (บท 1-3)DDD in Code — ออกแบบ Value Object ที่​มี behavior เต็ม​รูป
pattern matching, ทำ illegal states ให้ compile ไม่​ผ่าน (บท 4-5)DDD in Code — model state machine ของ aggregate
collection + LINQ, aggregate Order (บท 6)DDD in Code — ออกแบบ aggregate boundary และ invariant
interface (port), generics, DI (บท 7)Clean Architecture .NET — ports & adapters, dependency inversion เต็ม​รูป
async/await, Task, repository async (บท 8)EF Core — บันทึก​ออเดอร์​ลง​ฐาน​ข้อมูล​จริง​หลัง port

รูปทรง​เดียว​ที่​ร้อย​ทุก​บท​เข้า​ด้วย​กัน​คือ แกน domain บริสุทธิ์​อยู่​ตรง​กลาง ส่วน I/O และ​รายละเอียด​ของ​จริง​อยู่​ที่​ขอบ — record ที่​ปลอดภัย, pattern matching ที่​ครบ​ทุก​เคส, aggregate ที่​รักษา​กฎ, port ที่​ซ่อน infrastructure และ async ที่​ห่อ​อยู่​รอบ​นอก ทั้งหมด​รับใช้​แนวคิด​เดียวกัน​นี้

ตอน​นี้​คุณ​อ่าน​ทั้ง​อาร์ค​ได้​แล้ว — ไวยากรณ์ C# ไม่ใช่​กำแพง​อีก​ต่อ​ไป ไป​ลุย​คอร์ส​ถัด​ไป​ใน​สาย​ได้​เลย


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

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

  • Persistence Ignorance — หลักการ​เบื้องหลัง​ทั้ง​บท: domain ไม่​ควร​รู้เรื่อง​การ​เก็บ​ข้อมูล​หรือ I/O เลย นี่​คือ​เหตุผล​เชิง​หลักการ​ว่า​ทำไม​แกน domain ถึง​บริสุทธิ์​และ sync ส่วน async/persistence ถูก​ผลัก​ไป​อยู่​ที่​ขอบ
  • Repository Pattern — pattern เบื้องหลัง IOrderRepository: ห่อ​งาน​เก็บ/ดึง​ข้อมูล (ที่​เป็น async) ไว้​หลัง interface ให้ domain ไม่รู้จัก​ฐาน​ข้อมูล​จริง

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

ข้อ 1 / 3

ทำไมแกน domain ถึงยังเป็น synchronous ในขณะที่ขอบเป็น async?