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

Composition Root — ต่อ​สาย DI ให้ dependency ชี้​เข้า​ใน

Program.cs โผล่​มา​ให้​เห็น​แล้ว​สอง​ครั้ง​ใน​คอร์ส​นี้ — บท​ที่ 4 เพิ่ม builder.Services.AddScoped<IOrderRepository, EfOrderRepository>() เข้าไป​หนึ่ง​บรรทัด บท​ที่ 5 เพิ่ม AddMediatR, AddValidatorsFromAssemblyContaining, AddExceptionHandler เข้าไป​อีก​สี่​บรรทัด แต่​ไม่มี​บท​ไหน​เคย​หยุด​มอง file นี้​ทั้ง file พร้อม​กัน​สัก​ที ก่อน​จะ​ไป​เขียน test พิสูจน์​ใน​บท​ถัด​ไป​ว่า​ทุก​อย่าง​ที่​พูด​มา​ตลอด​ห้า​บท​เป็น​จริง บท​นี้​ขอ​พา​ไป​ยืน​มอง​จุด​เดียว​ที่ port ทุก​ตัว​จาก Domain กับ Application — IOrderRepository, IMenuCatalog, IPaymentGateway — มา​เจอ​กับ adapter จริง​ของ​มัน​พร้อม​กัน​ทั้งหมด​เป็น​ครั้ง​แรก

📦 code ตัวอย่าง

code เต็ม​ของ​บท​นี้​อยู่​ที่ repo kaen-food-ordering (กำลัง​จัด​ทำ) — path: FoodOrdering.Web/Program.cs

ลอง​ไล่​ดู​สิ่ง​ที่​เรา​รู้​อยู่​แล้ว​จาก​ห้า​บท​ที่​ผ่าน​มา: FoodOrdering.Domain ไม่รู้จัก FoodOrdering.Infrastructure เลย​แม้แต่​ชื่อ​เดียว, FoodOrdering.Application ประกาศ IOrderRepository/IMenuCatalog/IPaymentGateway ไว้​เป็น interface โดย​ไม่รู้​ว่า​ใคร​จะ​มา implement, และ FoodOrdering.Infrastructure ก็ implement EfOrderRepository โดย​ไม่รู้​ว่า​ใคร​จะ​มา​เรียก​ใช้​มัน — ทุก project ต่าง​รู้จัก “ครึ่ง​เดียว” ของ​ความ​สัมพันธ์​เสมอ คำถาม​คือ​แล้ว​ใครประกอบทั้ง​สอง​ครึ่ง​เข้า​ด้วย​กัน​ตอน app รัน​จริง

คำ​ตอบ​คือ composition rootComposition Rootจุด​เดียว​ใกล้​ขอบ​สุด​ของ app (ใน .NET คือ Program.cs / DI container) ที่​ประกอบ interface เข้า​กับ implementation จริง เป็น​ที่​เดียว​ที่ 'รู้จัก​ทุก​อย่าง'Architecture — จุด​เดียว​ใกล้​ขอบ​สุด​ของ app ที่​ได้​รับ​อนุญาต​ให้ “รู้จัก​ทุก​อย่าง” พร้อม​กัน ทั้ง interface ที่​วงใน​ประกาศ​ไว้​และ implementation จริง​ที่​วงนอก​เตรียม​ไว้ ใน .NET จุด​นั้น​คือ Program.cs ของ FoodOrdering.Web เพราะ​มัน​คือ​วงนอก​สุด​ที่ ProjectReference ชี้​เข้าหา​ทั้ง Application และ Infrastructure (ตาม​ที่​บท​ที่ 1 วาง​ไว้) และ DI container ที่ WebApplicationBuilder สร้าง​ให้​ก็​คือ​กลไก​ที่​ทำ​หน้าที่ “ประกอบ” นั้นจริงๆ ข้อ​สำคัญ​คือ composition root ไม่ใช่​แค่​ที่​ที่​มี code DI เยอะ​ที่สุด แต่​มัน​ควร​เป็นที่​เดียวใน​ทั้ง​โซลูชัน​ที่​ทำ​แบบ​นี้ — ถ้า​เริ่ม​มี​การ new EfOrderRepository(...) ตรงๆ กระจาย​อยู่​หลาย​ที่ นั่น​คือ​สัญญาณ​ว่า composition root กำลัง​รั่ว​ออก​ไป​นอก​ขอบ​ของ​มัน

flowchart LR
  IOR["IOrderRepository «interface»"] --> DI{{"DI Container<br/>Program.cs"}}
  IMC["IMenuCatalog «interface»"] --> DI
  IPG["IPaymentGateway «interface»"] --> DI
  ICL["IClock «interface»"] --> DI
  DI --> EOR["EfOrderRepository"]
  DI --> SMC["StaticMenuCatalog"]
  DI --> MPG["MockPaymentGateway"]
  DI --> SCL["SystemClock"]
  classDef inner fill:#16a34a,stroke:#065f46,color:#f8fafc;
  class IOR,IMC,IPG,ICL inner;

คำ​บรรยาย​ภาพ: interface ทั้ง​สี่​ตัว​ทาง​ซ้าย (วงใน — เขียว​เข้ม) ไม่รู้จัก implementation ทาง​ขวา​เลย และ implementation ทาง​ขวา​ก็​ไม่รู้จัก​กันเอง​ด้วย​ซ้ำ — มี​แค่ DI container ตรง​กลาง​ที่ Program.cs เท่านั้น​ที่​เห็น​ทั้ง​สอง​ฝั่ง​พร้อม​กัน แล้ว​ผูก​เส้น​ให้​ตอน app ส​ตาร์ต

กลไก​ที่ DI container ใช้​ผูก​สอง​ฝั่ง​เข้า​ด้วย​กัน​เรียก​ว่า dependency injectionDependency Injectionเทคนิค​ส่ง dependency (ที่ implement ตาม interface) เข้า​มา​จาก​ภายนอก​แทนที่​จะ​สร้าง​เอง ทำให้​วงใน​พึ่งพา abstraction ไม่ใช่​ของ​จริง และ​สลับ/ทดสอบ​ได้​ง่ายArchitecture — แทนที่ PlaceOrderHandler จะ new EfOrderRepository() ขึ้น​มา​เอง (ซึ่ง​จะ​ทำให้ Application ต้อง reference EF Core ทันที ผิด Dependency Rule ตั้งแต่​บท​ที่ 1) เรา​ส่ง implementation ที่​พร้อม​ใช้งาน​แล้ว​เข้าไป​ให้​จากภายนอกผ่าน​คอนสต​รัก​เตอร์แทน — ตัว handler เอง​ไม่​ต้อง​รู้​ด้วย​ซ้ำ​ว่า​มัน​ได้ EfOrderRepository ตัว​จริง​มา หรือ fake ตัว​หนึ่ง​สำหรับ test ทุก​การ​ลง​ทะเบียน​แบบ​นี้​ก็​คือ​การ​บอก container ว่า “เวลา​มี​ใคร​ขอ IOrderRepository ให้​เอา EfOrderRepository มา​ให้”

// FoodOrdering.Infrastructure/Menu/StaticMenuCatalog.cs — adapter แบบง่าย รอต่อเมนูจริงจาก repo ตัวอย่าง
public sealed class StaticMenuCatalog : IMenuCatalog
{
public Task<Money> GetPriceAsync(ProductId productId, CancellationToken ct) =>
Task.FromResult(Money.Thb(100)); // ราคาคงที่ชั่วคราว — ของจริงยิง HTTP ไปหาระบบเมนูร้านค้า
}
// FoodOrdering.Infrastructure/Payments/MockPaymentGateway.cs — adapter แบบง่าย รอต่อ payment gateway จริง
public sealed class MockPaymentGateway : IPaymentGateway
{
public Task ChargeAsync(Money amount, CancellationToken ct) =>
Task.CompletedTask; // ของจริงเรียก SDK ของผู้ให้บริการชำระเงินจริง
}

ทั้ง2 class ข้าง​บน​เป็น adapterAdaptercode วงนอก​ที่ implement port เพื่อ​เชื่อม​กับ​เทคโนโลยี​จริง เช่น EF Core repository หรือ HTTP payment clientArchitecture ตาม​นิยาม​เดียว​กับ EfOrderRepository ใน​บท​ที่ 4 — code วงนอก​ที่ implement portPortinterface ที่​วงใน​ประกาศ​ไว้​เพื่อ​คุย​กับ​โลก​ภายนอก (เช่น IPaymentGateway) โดย​ไม่รู้จัก implementation จริงArchitecture เพื่อ​คุย​กับ​เทคโนโลยี​จริง เพียง​แต่​ตอน​นี้​ยัง​เป็น version ตัวอย่าง รอ​วัน​ที่​ต่อ HTTP client เข้าหา​ระบบ​เมนู​ร้าน​ค้า​และ​ผู้​ให้​บริการ​ชำระ​เงิน​จริง​ใน code ตัวอย่าง​เต็ม​ของ​คอร์ส ประเด็น​ของ​บท​นี้​ไม่ใช่​รายละเอียด​ข้าง​ใน​ของ adapter พวก​นี้ แต่​คือที่​ที่​มัน​ถูก​ประกอบ​เข้า​กับ interface — ซึ่ง​เกิด​ขึ้น​ที่​เดียวกัน​หมด​ใน Program.cs:

// FoodOrdering.Web/Program.cs — composition root: ทุก port จากบทที่ 1–5 มาบรรจบกันที่นี่
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<FoodOrderingDbContext>(opt =>
opt.UseNpgsql(builder.Configuration.GetConnectionString("FoodOrdering"))); // บทที่ 4
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>(); // บทที่ 4
builder.Services.AddScoped<IMenuCatalog, StaticMenuCatalog>(); // บทที่ 3 ประกาศ port, บทนี้ผูก adapter
builder.Services.AddScoped<IPaymentGateway, MockPaymentGateway>(); // บทที่ 3 ประกาศ port, บทนี้ผูก adapter
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<PlaceOrderHandler>()); // บทที่ 3/5
builder.Services.AddValidatorsFromAssemblyContaining<PlaceOrderCommandValidator>(); // บทที่ 3/5
builder.Services.AddExceptionHandler<DomainExceptionHandler>(); // บทที่ 5
builder.Services.AddProblemDetails(); // บทที่ 5
var app = builder.Build();
app.UseExceptionHandler();
app.MapPost("/orders", /* endpoint เต็มจากบทที่ 5 — ไม่แก้อะไรเลย */ (PlaceOrderRequest r, ISender s, CancellationToken ct) => { });
app.MapGet("/orders/{id:guid}", /* endpoint เต็มจากบทที่ 5 — ไม่แก้อะไรเลย */ (Guid id, ISender s, CancellationToken ct) => { });
app.Run();

สังเกต​ว่า​ทุก​บรรทัด​ใน​นี้​เคย​เห็น​มา​แล้ว​ทั้งนั้น ไม่​ว่า​จะ​เป็น type ที่​ประกาศ​ไว้​ก่อนหน้า (IOrderRepository บท​ที่ 3, FoodOrderingDbContext บท​ที่ 4) หรือ endpoint ทั้ง​สอง​ตัว (บท​ที่ 5) — บท​นี้​แค่​รวบ​ทุก​อย่าง​มา​ไว้​ใน file เดียว​ให้​เห็น​ภาพ​เต็มๆ เป็น​ครั้ง​แรก ISender ที่ endpoint ขอ​มา​ก็​มา​จาก MediatRMediatRlibrary .NET ยอด​นิยม​สำหรับ​ส่ง command/query/notification ไป​ยัง handler แบบ in-process ช่วย​แยก Application layer ออก​จาก WebArchitecture ที่ AddMediatR(...) ลง​ทะเบียน​ไว้​ข้าง​บน — endpoint ไม่​ต้อง new PlaceOrderHandler(...) เอง​เลย​สัก​ที่ เพราะ MediatR ใช้ container ตัว​เดียวกัน​นี้​หา handler ที่​ถูกต้อง​ให้​อัตโนมัติ

รูปแบบ​ที่ PlaceOrderHandler ไม่ เรียก new EfOrderRepository() เอง แต่​รอ​ให้​ใคร​สัก​คน​ส่ง​มัน​เข้า​มา​ให้​แทน มีชื่อ​เรียก​ว่า Inversion of Control (IoC) — กลับ​ทิศ​การ​ควบคุม​จาก “code ของ​เรา​เป็น​ฝ่าย​เรียก library” มา​เป็น “framework/container เป็น​ฝ่าย​เรียก code ของ​เรา” ตัวอย่าง​ที่​จับ​ต้อง​ง่าย​ที่สุด​ของ IoC ก็​คือ ASP.NET Core เอง​นั่น​คือ — เรา​ไม่​ได้​เขียน loop มา​รอ​รับ HTTP request เอง แต่​เขียน endpoint ทิ้ง​ไว้​แล้ว​ปล่อย​ให้ framework เป็น​ฝ่าย​เรียก​มัน​ตอน​มี request เข้า​มา​จริง

หลักการ​เดียวกัน​นี้​มีชื่อเล่น​ที่​จำ​ง่าย​กว่า​คือ Hollywood Principle — “Don’t call us, we’ll call you” ชวน​ให้​นึกถึง​ตอน​ไป​คัด​ตัว​นัก​แสดง​แล้ว​สตูดิโอบอกว่า​อย่า​โทร​ตาม เดี๋ยว​เรา​โทร​หา​เอง PlaceOrderHandler ก็​เหมือน​นัก​แสดง​ที่ “สมัคร” ไว้​ผ่าน constructor ว่า​ต้องการ IOrderRepository, IMenuCatalog, IPaymentGateway แบบ​ไหน แล้ว​ปล่อย​ให้ composition root เป็น​ฝ่าย “โทร​กลับ​มา” ตอน​จำเป็น — ไม่ใช่ handler ที่​วิ่ง​ไป​เสาะ​หา​ของ​พวก​นี้​เอง Dependency Injection ที่​เรา​เขียน​ใน Program.cs ข้าง​บน​ก็​คือ​การ​ทำ IoC ให้​เป็น​รูปธรรม​ส่วน Hexagonal ArchitectureHexagonal ArchitecturePorts & Adapters ของ Alistair Cockburn — แกน app อยู่​กลาง คุย​กับ​ภายนอก​ผ่าน port/adapter เท่านั้น พูด​เรื่อง​เดียว​กับ Clean Architecture ด้วย​รูปทรง​ต่าง​กันArchitecture ที่​บท​ที่ 1 พูด​ถึง​ไว้​ก็​มอง​เรื่อง​เดียวกัน​นี้​ผ่าน​รูป​หก​เหลี่ยม — core อยู่​กลาง ส่วน port/adapter ล้อม​รอบ แล้ว​มี​จุด​ประกอบ (composition root) ที่​ทำให้​ทั้ง​รูป​นั้น​ขยับ​ได้​จริง

จนถึง​ตอน​นี้ ports ทุก​ตัว​ที่​เรา​ผูก​ใน composition root คุย​กับ “ของนอก​ที่​ต้อง​อ่าน/เขียน​ข้อมูล” — ฐาน​ข้อมูล, เมนู​ร้าน​ค้า, payment gateway แต่​ยัง​มี dependency ภายนอก​อีก​แบบ​หนึ่ง​ที่​มัก​ถูก​มอง​ข้าม นั่น​คือ เวลา ลอง​นึก​ภาพ​นโยบาย​ที่​ทีม​ธุรกิจ​อยาก​ได้​ต่อ​ไป: “ถ้า​ร้าน​อาหาร​ไม่​กด​ยืนยัน​ออเดอร์​ภายใน 5 นาที ให้​ระบบ​ยกเลิก​ออเดอร์​นั้น​ให้​อัตโนมัติ” — นี่​คือ​ตัวอย่าง​ของ​เคส​ซับซ้อน​ที่​คอร์ส​นี้​เรียก​ว่า C2 (ปัญหา​เรื่อง lifecycle/timeout ของ aggregate) ที่​ต่าง​จาก C1 (โปรโมชัน​ใน​บท​ที่ 3) ตรง​ที่​มัน​ไม่​ได้​เกิด​จาก “input ที่​ซับซ้อน​ขึ้น” แต่​เกิด​จาก “เวลา​ที่​ผ่าน​ไป​เฉยๆ โดย​ไม่มี​ใคร​ทำ​อะไร​เลย”

จุด​ที่​น่า​สนใจ​คือ​นโยบาย​นี้​ประกอบ​ด้วย​สอง​ส่วน​ที่ควร​อยู่​คนละ​วง:

  1. กฎ — ตัวเลข “5 นาที” และ “สิ่ง​ที่​เกิด​ขึ้น​เมื่อ​ครบ​กำหนด” (ยกเลิก​ออเดอร์) เป็นการ​ตัดสิน​ใจ​ทาง​ธุรกิจ​ล้วนๆ ไม่​ต่าง​จาก invariant “ห้าม​ตะกร้า​ว่าง” ที่ Order.Place(...) รักษา​มา​ตั้งแต่​บท​ที่ 2 เลย — มัน​ควร​อยู่​ใน Domain หรือ Application เพราะ​เป็น​ความ​จริง​ของ​ธุรกิจ ไม่ใช่​รายละเอียด​ทาง​เทคนิค
  2. นาฬิกา/ตัว​จับ​เวลา — กลไก​ที่​บอกว่า “ตอน​นี้​เวลา​เท่าไร” และ “ตั้ง​เวลา​ให้​ทำงาน​ถัด​ไป​ยังไง” เป็น​เรื่อง​ของ​โครงสร้าง​พื้นฐาน ไม่ใช่​กฎ​ธุรกิจ — DateTime.UtcNow หรือ background job ที่​ใช้​จริง​เปลี่ยน​เทคโนโลยี​ได้ (เช่น สลับ​จาก in-process timer ไป​เป็น background job library) โดยที่​ตัวเลข “5 นาที” ไม่​ต้อง​เปลี่ยน​ตาม​เลย

ถ้า​เผลอ​เขียน DateTime.UtcNow ตรงๆ ลง​ไป​ใน Order หรือ PlaceOrderHandler (เหมือน​ที่​โปรโมชัน “ลด​พิเศษ​ช่วง​เที่ยง” ใน​บท​ที่ 3 เคย​เผลอ​เรียก DateTime.Now.Hour ตรงๆ) จะ​เกิด​ปัญหา​เดียว​กับ​ที่​เกิด​ไป​แล้ว — code วงใน​จะ​ผูก​ติด​กับ​นาฬิกา​ของ​เครื่อง​จริง ทดสอบ​ยาก (จะ test “ครบ 5 นาที​แล้ว” ยังไง​ถ้า​ต้อง​รอ 5 นา​ทีจริงๆ) และ​เปลี่ยน​กลไก​จับ​เวลา​ทีหลัง​ไม่​ได้​โดย​ไม่​แตะ​กฎ​ธุรกิจ ทาง​แก้​จึง​เหมือน​กับ​ที่​ทำ​มา​ตลอด​คอร์ส​นี้​ทุก​ครั้ง — ห่อ​มัน​เป็น port ให้​วงใน​ประกาศ แล้ว​ให้​วงนอก implement:

// FoodOrdering.Application/Time/IClock.cs — port: "เวลาปัจจุบันของระบบคือเท่าไร"
public interface IClock
{
DateTimeOffset UtcNow { get; }
}
// FoodOrdering.Application/Time/IScheduler.cs — port: "อีกเท่านี้ ให้เรียกงานนี้ให้หน่อย"
public interface IScheduler
{
void ScheduleAfter(TimeSpan delay, Func<CancellationToken, Task> action);
}
// FoodOrdering.Infrastructure/Time/SystemClock.cs — adapter ที่คุยกับนาฬิกาของเครื่องจริง
public sealed class SystemClock : IClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
// FoodOrdering.Infrastructure/Time/TimerScheduler.cs — adapter อย่างง่ายด้วย System.Threading.Timer
// (พรีวิวเท่านั้น — คอร์สถัดไปในซีรีส์ที่ว่าด้วยเคสซับซ้อน C1–C3 จะสลับไปใช้ background job library จริง)
public sealed class TimerScheduler : IScheduler
{
public void ScheduleAfter(TimeSpan delay, Func<CancellationToken, Task> action) =>
_ = new Timer(_ => action(CancellationToken.None), null, delay, Timeout.InfiniteTimeSpan);
}

แล้ว​ผูก​ทั้ง​สอง​เข้า​กับ composition root เดียว​กับ​ที่​ผูก IOrderRepository/IMenuCatalog/IPaymentGateway ไป​แล้ว​ข้าง​บน:

// FoodOrdering.Web/Program.cs — เติมต่อจาก block ลงทะเบียนก่อนหน้านี้ในบทนี้
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddSingleton<IScheduler, TimerScheduler>();
ทำไม​จึง​เป็น AddSingleton ไม่ใช่ AddScoped

IClock และ IScheduler ไม่​ได้​ผูก​กับ request หรือ transaction ใดๆ เป็น​พิเศษ​เหมือน IOrderRepository ที่​ผูก​กับ DbContext ต่อ request หนึ่ง​ครั้ง — นาฬิกา​ของ​เครื่อง​เป็น​ของกลาง​ที่​ทุก request ใช้​ร่วม​กัน​ได้​อย่าง​ปลอดภัย​ตลอด​อายุ app จึง​ลง​ทะเบียน​แบบ AddSingleton แทน AddScoped

❌ ถ้า​เผลอ​เขียน​กฎ 5 นาที​ปน​กับ​นาฬิกา​ไว้​ใน​ที่​เดียวกัน
// อย่าทำแบบนี้ — เลข "5 นาที" (กฎธุรกิจ) กับ DateTime.UtcNow (กลไกจับเวลา) ถูกฝังปนกันไว้ใน Infrastructure
public sealed class TimeoutCancellationJob
{
public bool ShouldCancel(DateTimeOffset placedAt) =>
DateTimeOffset.UtcNow - placedAt > TimeSpan.FromMinutes(5);
}

code ข้าง​บน​ผิด​สอง​ต่อ: หนึ่ง — เลข “5 นาที” ซึ่ง​เป็น​กฎ​ธุรกิจ​ไป​ฝัง​อยู่​ใน FoodOrdering.Infrastructure แทนที่​จะ​อยู่​วงใน สอง — มัน​เรียก DateTimeOffset.UtcNow ตรงๆ แทนที่​จะ​ผ่าน IClock ทำให้ test เรื่อง “ครบ 5 นาที​หรือ​ยัง” ต้อง​พึ่ง​นาฬิกา​จริง​ของ​เครื่อง​เสมอ (จะ test ยังไง​ถ้า​ต้อง​รอ 5 นา​ทีจริงๆ ทุก​ครั้ง) ทาง​ที่​ถูก​คือ​ให้​ทั้ง​ค่า​คงที่ 5 นาที​และ​ตรรกะ​ตัดสิน​ใจ​แบบ​นี้​ย้าย​ไป​อยู่​วงใน​เป็น​ส่วน​หนึ่ง​ของ Domain/Application ที่​รับ IClock เข้า​มา​ผ่าน constructor (เหมือน​ที่ PlaceOrderHandler ถาม IMenuCatalog/IPaymentGateway อยู่​แล้ว) แทนที่​จะ​ปล่อย​ให้ Infrastructure ตัดสิน​ใจ​เรื่อง​นี้​เอง

ขอบเขต​ของ​บท​นี้

บท​นี้​ตั้งใจ​แสดง​แค่ seam ของ C2 — จุด​ต่อ​ที่​กฎ (5 นาที, ยกเลิก​อัตโนมัติ) กับ​กลไก (IClock/IScheduler) ควร​แยก​จาก​กัน ยัง​ไม่​ลง​รายละเอียด​ว่า state machine ของ​สถานะ​ออเดอร์ (PlacedPendingConfirmationConfirmed/Cancelled) หน้าตา​เป็น​ยังไง หรือ Order ควร​มี property/method อะไร​เพิ่ม​เพื่อ​รองรับ​มัน — นั่น​เป็น​เนื้อหา​ที่​ลึก​กว่า​ขอบเขต​ของ​คอร์ส​ที่​เน้น​โครงสร้าง Clean Architecture คอร์ส​นี้ คอร์ส​ถัด​ไป​ใน​ซีรีส์ (ที่​ว่าด้วย​เคส​ซับซ้อน C1–C3) จะ​กลับ​มา​แกะ C2 นี้​อีก​ครั้ง​แบบ​เต็มๆ พร้อม state machine จริง

บท​นี้​เรา​หยุด​มอง Program.cs ทั้ง file เป็น​ครั้ง​แรก​ใน​ฐานะ composition root — จุด​เดียว​ใน​ทั้ง​โซลูชัน​ที่​รู้จัก​ทั้ง port ที่​วงใน​ประกาศ​ไว้ (IOrderRepository, IMenuCatalog, IPaymentGateway, IClock, IScheduler) และ adapter จริง​ที่​วงนอก implement (EfOrderRepository, StaticMenuCatalog, MockPaymentGateway, SystemClock, TimerScheduler) พร้อม​กัน​ทั้งหมด ผูก​เข้า​ด้วย​กัน​ผ่าน dependency injection ตาม​หลัก Inversion of Control และ Hollywood Principle ที่​ให้ container เป็น​ฝ่าย​เรียก code ของ​เรา ไม่ใช่ code ของ​เรา​วิ่ง​ไป​เสาะ​หา dependency เอง และ​สุดท้าย​คือพรีวิว​ของ C2 — seam ของ​นโยบาย timeout ที่​สอน​บทเรียน​เดิม​ซ้ำ​อีก​ครั้ง​ใน​บริบท​ใหม่: กฎ​ธุรกิจ (5 นาที, ยกเลิก​อัตโนมัติ) อยู่​วงใน ส่วน​กลไก​ที่​บอก​เวลา (IClock/IScheduler) เป็น​แค่ adapter อีก​คู่​หนึ่ง​ที่มา​บรรจบ​กัน​ที่ composition root เดียวกัน​นี้

บท​ถัด​ไป​เรา​จะ​พิสูจน์​สิ่ง​ที่​พูด​มา​ตลอด​หกบท​ว่า​เป็น​จริง — เขียน test ที่​แสดง​ให้​เห็น​ว่า Domain กับ Application ทดสอบ​ได้​โดย​ไม่​ต้อง​พึ่ง ASP.NET Core, EF Core หรือ​ฐาน​ข้อมูล​จริง​เลย​สัก​บรรทัด เพราะ​สิ่ง​เดียว​ที่​รู้จัก​ทั้ง​สอง​ฝั่ง​พร้อม​กัน​คือ composition root ที่​เรา​เพิ่ง​ประกอบ​เสร็จ​ใน​บท​นี้


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

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

  • Dependency Injection — เทคนิค​ส่ง dependency เข้า​มา​จาก​ภายนอก​แทนที่​จะ​ให้ class สร้าง​ขึ้น​เอง ราก​ของ​ทุก​บรรทัด AddScoped/AddSingleton
  • Inversion of Control — หลัก​ที่​กลับ​ทิศ​การ​ควบคุม​ให้ container/framework เป็น​ผู้​เรียก code ของ​เรา แทนที่ code ของ​เรา​จะ​เรียก library เอง
  • Hollywood Principle — “อย่า​โทร​หา​เรา เดี๋ยว​เรา​โทร​หา​คุณ” ชื่อเล่น​ที่​จำ​ง่าย​ของ IoC
  • Hexagonal Architecture (Ports and Adapters) — บทความ​เต็ม​ของ Cockburn ที่​มอง​ภาพ​เดียวกัน​นี้​ผ่าน​รูป​หก​เหลี่ยม core อยู่​กลาง ล้อม​ด้วย port/adapter

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

ข้อ 1 / 3

composition root คืออะไร?