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

Interface, generics และ DI — port และ​การ​พึ่งพา abstraction

หกบท​ที่​ผ่าน​มา​เรา​ปั้น​ก้อน​อิฐ​ของ domain Order จน​แน่น — Money, OrderId, Quantity, OrderLine และ​ตัว Order เอง​ที่​รักษา​กฎ​ธุรกิจ​ของ​มัน ทั้งหมด​นี้​ยัง​เป็น​โลก “ใน​หัว” ล้วนๆ คือ​คำนวณ​และ​ตัดสิน​ใจ​อยู่​ใน​หน่วย​ความ​จำ ไม่​ได้​แตะ​โลก​ภายนอก​เลย

แต่ app จริง​ต้อง​แตะ​โลก​ภายนอก: ตัด​เงิน​ผ่าน​ผู้​ให้​บริการ​ชำระ​เงิน (เช่น Stripe) และ​บันทึก​ออเดอร์​ลง​ฐาน​ข้อมูล (เช่น SQL) คำถาม​ใหญ่​ของ​บท​นี้​คือ — จะ​ให้ domain สั่ง​งาน​พวก​นี้​ได้ โดยที่ domain ไม่​ต้อง​รู้จัก Stripe หรือ SQL เลย​ได้​ยังไง? คำ​ตอบ​คือ​เครื่องมือ​สาม​ชิ้น​ของ C# ที่​ทำงาน​ร่วม​กัน: interface, generics และ dependency injection

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

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

InterfaceInterfaceสัญญา (contract) ที่​ประกาศ​ว่า 'ต้อง​มี method/พฤติกรรม​อะไร​บ้าง' โดย​ไม่​ผูก​กับ​วิธี implement เช่น `IOrderRepository` — ทำให้ domain พึ่งพา​แค่ abstraction ส่วน implementation จริง (เช่น​ต่อ​ฐาน​ข้อมูล) ใส่​เข้า​มา​ทีหลัง​ผ่าน Dependency InjectionArchitecture คือ “สัญญา” (contract) ที่​ประกาศ​ว่า type ที่​ทำ​ตาม​สัญญา​นี้ ต้อง​มี method อะไร​บ้าง แต่​ไม่​บอกว่า method นั้น​ทำงาน​ยังไง​ข้าง​ใน ลอง​ดู​สัญญา​สำหรับ “ตัว​ตัด​เงิน”:

namespace FoodOrdering.Domain.Orders;
public interface IPaymentGateway
{
Task ChargeAsync(Money amount, CancellationToken ct);
Task RefundAsync(Money amount, CancellationToken ct);
}

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

  • interface คือ keyword ที่​บอก compiler ว่า “นี่​คือ​สัญญา ไม่ใช่​ของ​จริง”
  • ชื่อ​ขึ้น​ต้น​ด้วย​ตัว I (เช่น IPaymentGateway) เป็น​ธรรมเนียม​ของ C# ที่​ทำให้​เห็น​ปุ๊บ​ว่า​นี่​คือ interface
  • ข้าง​ใน​มี​แต่ หัว method — ชื่อ, สิ่ง​ที่​รับ​เข้า, สิ่ง​ที่​คืน​ออก — แต่ ไม่มี​เนื้อ​ใน (ไม่มี { ... } ตาม​หลัง) เพราะ interface ไม่​สนใจ​ว่า​จะ​ตัด​เงิน​ยังไง สน​แค่​ว่า “ต้อง​ตัด​เงิน​ได้” กับ “ต้อง​คืน​เงิน​ได้”
  • Task ที่​เห็น​แปล​ว่า “งาน​ที่​ใช้​เวลา” เพราะ​การ​คุย​กับ​ธนาคาร​ไม่​ได้​เสร็จ​ทันที ส่วน CancellationToken คือ “ปุ่ม​ยกเลิก” — ยัง​ไม่​ต้อง​เข้าใจ​ลึก​ตอน​นี้ บท​ที่ 8 จะ​เจาะ async/await เต็มๆ ตรง​นี้ ตอน​นี้​แค่​รู้​ว่า​ทั้ง​สอง​ตัว​เป็น​ส่วน​หนึ่ง​ของ​หัว​สัญญา

interface แบบ​นี้​เรียก​ใน​ภาษา DDD ว่า port (port) — เป็น​ช่อง​เสียบ​ที่ domain ใช้​คุย​กับ​โลก​ภายนอก domain ถือ​แค่ “สัญญา” IPaymentGateway ไว้ ไม่​เคย​รู้​เลย​ว่า​ปลายทาง​จริง​คือ Stripe, PayPal หรือ​ตัว​ปลอม​ตอน test นี่​คือ​หัวใจ: domain พึ่งพา abstraction ไม่ใช่​ของ​จริง

การ​เก็บ​และ​ดึง​ออเดอร์​จาก​ฐาน​ข้อมูล​ก็​เป็น port แบบ​เดียวกัน — สัญญา​ว่า “ต้อง​เก็บ​และ​ดึง​ออเดอร์​ได้” โดย​ไม่​บอกว่า​เก็บ​ใน SQL, MongoDB หรือ file แต่ port ตัว​นั้น​เขียน​สวย​ที่สุด​ด้วย​เครื่องมือ​ชิ้น​ถัด​ไป เรา​จึง​เก็บ IOrderRepository ไว้​ปั้น​ใน​หัวข้อ generics เลย

ก่อน​จะ​เห็น​ว่า port ช่วย​อะไร ลอง​ดู​สิ่ง​ที่​เกิด​ขึ้น​ถ้า ไม่ ใช้​มัน นี่​คือ ❌ version ดิบ ที่ domain สร้าง​ของ​จริง​ขึ้น​มา​เอง​ข้าง​ใน:

// ❌ version ดิบ: domain ผูกตายกับ infrastructure จริง
public class CheckoutHandler
{
public void Checkout(OrderId id)
{
var orders = new SqlOrderRepository("Server=prod;Database=orders;..."); // ผูกตายกับ SQL
var payments = new StripePaymentGateway("sk_live_51H..."); // ผูกตายกับ Stripe
// ...ตรรกะ checkout...
}
}

ตรง new SqlOrderRepository(...) และ new StripePaymentGateway(...) ดู​ไม่มี​พิษ​ภัย แต่​มัน​ทำให้ domain พัง​ใน​สาม​ทาง:

  • ผูก​ตาย​กับ​ของ​จริง — code นี้​เรียก​ใช้ SQL และ Stripe ตรงๆ จะ​สลับ​ไป​ใช้​ที่​อื่น (หรือ​ใช้​ตัว​ปลอม​ตอน test) ไม่​ได้​เลย นอกจาก​ไป​แก้ code domain ทิ้ง
  • test แทบ​ไม่​ได้ — ทุก​ครั้ง​ที่​รัน test Checkout จะ​ต่อ​ฐาน​ข้อมูล​จริง​และ รูด​บัตร​จริง เพราะ​มัน​สร้าง​ของ​จริง​ขึ้น​มา​เอง​เสมอ
  • dependency ซ่อน​อยู่ — คน​อ่าน​หัว class CheckoutHandler มอง​ไม่​ออก​เลย​ว่า​มัน​ต้อง​พึ่ง repository กับ payment gateway จนกว่า​จะ​ไล่​อ่าน​ข้าง​ใน method แถม connection string กับ secret key ยัง​หลุด​เข้า​มา​ปน​อยู่​ใน domain อีก

ราก​ของ​ปัญหา​คือ “domain สร้าง dependency ของ​ตัวเอง” ทาง​แก้​คือ​กลับ​ด้าน​มัน — ให้ dependency ถูก​ส่ง​เข้า​มา​จาก​ภายนอก ผ่าน port ที่​เหลือ​ของ​บท​นี้​คือ​การ​ประกอบ​เครื่องมือ​ที่​ทำให้​เรื่อง​นี้​เกิด​ขึ้น​อย่าง​เป็น​ระบบ

ก่อน​ไป​ต่อ​เรื่อง DI เรา​ต้อง​รู้จัก​เครื่องมือ​อีก​ชิ้น​ที่​โผล่​มา​ใน code domain บ่อย​มาก GenericsGenericsความ​สามารถ​เขียน type หรือ method ที่​ทำงาน​กับ type ใด​ก็ได้​โดย​ไม่​ต้อง​รู้​ล่วงหน้า ระบุ​ด้วย type parameter เช่น `List<T>`, `IReadOnlyList<OrderLine>` — ทำให้​เขียน code (เช่น Repository หรือ collection ห่อ​หุ้ม) ที่ type-safe และ​ใช้​ซ้ำ​ได้​กับ​หลาย​ชนิด​ข้อมูลArchitecture คือ​ความ​สามารถ​เขียน type หรือ method ที่​ทำงาน​กับ ชนิด​ข้อมูล​ใด​ก็ได้ โดย​ไม่​ต้อง​รู้​ล่วงหน้า​ว่า​เป็น​ชนิด​อะไร เรา​ระบุ​ช่องว่าง​นั้น​ด้วย type parameter — ตัว​อักษร​ใน​วงเล็บ​มุม <...> (นิยม​ใช้ T ย่อ​จาก Type)

ที่​จริง​เรา​ใช้ generics มา​ตั้งแต่​บท​ก่อน​แล้ว​โดย​ไม่รู้ตัว — List<OrderLine> คือ “list ที่​บรรจุ OrderLine” และ IReadOnlyCollection<OrderLine> คือ “collection อ่าน​อย่าง​เดียว​ของ OrderLine” ตัว List<T> เขียน​ครั้ง​เดียว​ใน .NET แล้ว​ใช้ได้​กับ​ทุก​ชนิด — List<int>, List<Money>, List<Order> — เพราะ T เป็น​ช่องว่าง​ที่​เรา​เติม​ชนิด​เข้าไป​ทีหลัง

ทีนี้​มา​เขียน generic ของ​เรา​เอง​บ้าง entity (entity) ทุก​ตัว​ใน domain มี​สิ่ง​หนึ่ง​เหมือน​กัน: มัน​มี identity คือ “เลข​ประจำ​ตัว” ที่​ทำให้​มัน​เป็น “ตัว​ไหน” ต่าง​จาก Money ที่​เป็น​แค่ “ค่า” เรา​จึง​ทำ base class กลาง​ชื่อ Entity<TId> ที่​เก็บ Id และ​เทียบ​ความ​เท่า​กัน​ด้วย Id ให้​ทุก entity ใช้​ร่วม​กัน:

namespace FoodOrdering.Domain;
public abstract class Entity<TId> where TId : notnull
{
public TId Id { get; }
protected Entity(TId id) => Id = id;
// entity สองตัวเท่ากันเมื่อ "Id เท่ากัน" ไม่ใช่ตอนที่ทุก field เท่ากัน
public override bool Equals(object? obj) =>
obj is Entity<TId> other && Id.Equals(other.Id);
public override int GetHashCode() => Id.GetHashCode();
}

มี​ของ​ใหม่​สอง​อย่าง:

  • Entity<TId>TId คือ type parameter สำหรับ “ชนิด​ของ Id” entity คนละ​ตัว​อาจ​ใช้ Id คนละ​ชนิด (OrderLine ใช้ OrderLineId, Order ใช้ OrderId) base class เดียว​นี้​จึง​รองรับ​ได้​หมด​โดย​ไม่​ต้อง​เขียน​ซ้ำ ส่วน abstract แปล​ว่า “สร้างตรงๆ ไม่​ได้ ต้อง​มี​คน​สืบทอด​ไป​ใช้”
  • where TId : notnull — ท่อน​นี้​คือ generic constraint (เงื่อนไข​ของ generic) มัน​จำกัด​ว่า “ชนิด​ที่​จะ​เอา​มา​เติม​ช่อง TId ต้อง​เป็น​ชนิด​ที่​เป็น null ไม่​ได้” ประโยชน์​คือ พอ compiler การันตี​ว่า Id ไม่มี​ทาง​เป็น null code ข้าง​ใน​อย่าง Id.Equals(other.Id) ก็​เรียก​ได้​อย่าง​มั่นใจ ไม่​ระเบิด​กลาง​ทาง — constraint คือ​การ​ซื้อ​ความ​มั่นใจ​ให้ code ข้าง​ใน generic

ตอน​นี้ OrderLine แค่​สืบทอด Entity<OrderLineId> ก็ได้ identity กับ​การ​เทียบ​ความ​เท่า​กัน​มาฟรีๆ สังเกต​ว่า OrderLine เป็น class ไม่ใช่ record — เพราะ​มัน​มี​ตัวตน (identity) ต่าง​จาก Money/OrderId ที่​เป็น record เพราะ​เป็น​แค่​ค่า:

namespace FoodOrdering.Domain.Orders;
public sealed class OrderLine : Entity<OrderLineId>
{
public ProductId ProductId { get; }
public Quantity Quantity { get; }
public Money UnitPrice { get; }
public OrderLine(OrderLineId id, ProductId productId, Quantity quantity, Money unitPrice)
: base(id) // ส่ง id ต่อขึ้นไปให้ base class Entity<OrderLineId> เก็บ
{
ProductId = productId;
Quantity = quantity;
UnitPrice = unitPrice;
}
}

ผล​ของ​การ​เทียบ​ด้วย identity เห็น​ชัด​ตอน​ใช้​จริง — สอง​บรรทัด​ที่ Id เดียวกัน​ถือว่า​เป็น “ออเดอร์​ไลน์​เดียวกัน” แม้​ของ​ข้าง​ใน​ต่าง​กัน:

var id = new OrderLineId(Guid.NewGuid());
var a = new OrderLine(id, padThai, new Quantity(1), Money.Thb(50));
var b = new OrderLine(id, greenCurry, new Quantity(2), Money.Thb(90));
bool same = a.Equals(b); // true! เพราะ Id เดียวกัน — entity เทียบด้วย identity ไม่ใช่ค่า

repository ก็​ใช้ generics ได้​เหมือน​กัน — port ส่วน​ใหญ่​หน้าตา​คล้าย​กัน (หา​ด้วย id, เพิ่ม​ของ​ใหม่) เรา​จึง​ยก​ส่วน​ที่​ซ้ำ​ขึ้น​ไป​ไว้​ใน IRepository<T> กลาง:

namespace FoodOrdering.Domain;
public interface IRepository<T> where T : class
{
Task<T?> FindAsync(Guid id, CancellationToken ct);
Task AddAsync(T entity, CancellationToken ct);
}

where T : class บอกว่า T ต้อง​เป็น reference type (class) — จึง​มี​ความหมาย​ที่​จะ​คืน T? (“เจอ หรือ ไม่​เจอ​เป็น null”) ได้ พอมี base สัญญา​นี้​แล้ว IOrderRepository ก็​แค่​ต่อยอด​ด้วย method เฉพาะ​ของออเดอร์:

namespace FoodOrdering.Domain.Orders;
public interface IOrderRepository : IRepository<Order>
{
// method เฉพาะที่ repository ทั่วไปไม่มี
Task<IReadOnlyList<Order>> FindByStatusAsync(OrderStatus status, CancellationToken ct);
}

พอ domain พึ่ง port แทน​ของ​จริง ทุกๆ ตัว​ที่​ต้อง​ใช้ port ก็​ต้อง รับ​มัน​เข้า​มา​ทาง constructor วิธี​ดั้งเดิม​คือ​ประกาศ field แล้ว​เขียน constructor คอย​รับ​ค่า​มา​ใส่ field — code ซ้ำๆ ที่​เรียก​ว่า boilerplate:

// วิธีดั้งเดิม: ประกาศ field + เขียน constructor รับค่าเอง
public class CheckoutHandler
{
private readonly IOrderRepository _orders;
private readonly IPaymentGateway _payments;
public CheckoutHandler(IOrderRepository orders, IPaymentGateway payments)
{
_orders = orders;
_payments = payments;
}
// ...method ที่ใช้ _orders กับ _payments...
}

Primary ConstructorPrimary Constructorไวยากรณ์​ย่อ (C# 12) ที่​ประกาศ parameter ของ constructor ไว้​บน​หัว class/struct ได้​เลย เช่น `class DeliveryFeeCalculator(IDistanceService distance)` — ลด code boilerplate ของ​การ​เขียน field + constructor แยก​กัน​เวลา​ที่​ต้องการ​แค่​เก็บ dependency หรือ​ค่าที่​รับ​เข้า​มาTactical Design (มาใน C# 12) ย่อ​ทั้งหมด​นั้น​ให้​เหลือ​การ​ประกาศ parameter ไว้​บน​หัว class เลย parameter ที่​ประกาศ​ตรง​นั้น​เรียก​ใช้ได้​ทั่ว​ทั้ง class โดย​ไม่​ต้อง​ทำ field เอง:

namespace FoodOrdering.Application.Orders;
// parameter บนหัว class = primary constructor
public class CheckoutHandler(IOrderRepository orders, IPaymentGateway payments)
{
public async Task CheckoutAsync(OrderId id, CancellationToken ct)
{
var order = await orders.FindAsync(id.Value, ct)
?? throw new InvalidOperationException("ไม่พบออเดอร์นี้");
await payments.ChargeAsync(order.Total, ct);
}
}

code 2 version ทำงาน​เหมือนกันเป๊ะ แต่ version primary constructor สั้น​กว่า​มาก และ​หัว class (IOrderRepository orders, IPaymentGateway payments) ประ​กาศชัดๆ ตั้งแต่​บรรทัด​แรกว่า “ตัว​นี้​ต้อง​พึ่ง2 port นี้” — ตรง​ข้าม​กับ version ดิบ​ก่อน​หน้าที่​ซ่อน dependency ไว้​ใน method (สังเกต​ว่า CheckoutHandler นี่​อยู่​ใน FoodOrdering.Application คือ​ชั้น “app” ที่​ประสาน​งาน ส่วน domain แท้ๆ ยัง​บริสุทธิ์​อยู่​ใน FoodOrdering.Domain)

เหลือ​คำถาม​สุดท้าย: ถ้า CheckoutHandler แค่ “รับ” IOrderRepository กับ IPaymentGateway เข้า​มา แล้ว ใคร​เป็น​คน​หา​ของ​จริง​มา​เสียบ​ให้? คำ​ตอบ​คือ Dependency InjectionDependency Injectionเทคนิค​ที่อ็อบเจ็กต์ 'รับ' dependency ของ​มัน​เข้า​มา​จาก​ภายนอก (ผ่าน constructor เป็น​หลัก) แทนที่​จะ​สร้าง​เอง​ข้าง​ใน — ทำให้​สลับ implementation ได้ (เช่น mock ตอน test) และ code พึ่งพา​แค่ interface ไม่​ผูก​กับ​รายละเอียด infrastructureArchitecture (DI) — เทคนิค​ที่อ็อบเจ็กต์รับ dependency ของ​มัน​เข้า​มา​จาก​ภายนอก แทนที่​จะ​สร้าง​เอง การ​ส่ง​ผ่าน constructor แบบ​ที่​เรา​เพิ่ง​ทำ​เรียก​ว่า constructor injection

แล้ว​ของ​จริง​มา​จาก​ไหน? มัน​ถูก​ผูก​ไว้​ที่ container — จุด​เดียว​ที่ “ขอบ” ของ​ระบบ (ใน .NET คือ file Program.cs) ที่​รู้จัก​ของ​จริง​ทั้งหมด และ​คอย​บอกว่า “พอ​มี​ใคร​ขอ interface นี้ ให้​หยิบ implementation ตัว​นี้​ไป​ให้”:

// Program.cs — "ขอบ" ของระบบ จุดเดียวที่รู้จักของจริง
var builder = WebApplication.CreateBuilder(args);
// เดินสาย: ผูก interface (port) เข้ากับ implementation จริง (adapter)
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IPaymentGateway, StripePaymentGateway>();
builder.Services.AddScoped<CheckoutHandler>();
var app = builder.Build();

บรรทัด AddScoped<IOrderRepository, SqlOrderRepository>() อ่าน​ว่า “เวลา​มี​ใคร​ขอ IOrderRepository ให้​สร้าง SqlOrderRepository ไป​ให้” พอ container สร้าง CheckoutHandler มัน​เห็น​ว่า constructor ขอ2 port ก็​หยิบ​ของ​จริง​ที่​ผูก​ไว้​มา​เสียบ​ให้​อัตโนมัติ — โดยที่ CheckoutHandler ไม่​เคย​เอ่ย​ชื่อ SqlOrderRepository หรือ StripePaymentGateway เลย​สัก​ครั้ง

ผลลัพธ์​ทั้งหมด​สรุป​เป็น​ภาพ​นี้: domain อยู่​ตรง​กลาง​พึ่ง​แค่ port, adapter ของ​จริง​อยู่​นอก​สุด, และ​มี container ที่​ขอบ​เป็น​คน​เดินสาย​เชื่อม​สอง​ฝั่ง

flowchart TB
  subgraph DOMAIN["code ของเรา (app + domain) — พึ่งแค่ port"]
    APP["CheckoutHandler"]
    PORT1["IPaymentGateway (port)"]
    PORT2["IOrderRepository (port)"]
    APP --> PORT1
    APP --> PORT2
  end
  subgraph INFRA["Infrastructure ที่ขอบ — ของจริง"]
    ADP1["StripePaymentGateway"]
    ADP2["SqlOrderRepository"]
  end
  ADP1 -. ทำตามสัญญา .-> PORT1
  ADP2 -. ทำตามสัญญา .-> PORT2
  DI["DI Container ที่ Program.cs<br/>ต่อสายที่ขอบ"]
  DI -->|ผูก IPaymentGateway กับ StripePaymentGateway| ADP1
  DI -->|ผูก IOrderRepository กับ SqlOrderRepository| ADP2

คำ​บรรยาย​ภาพ: ลูกศร​จาก CheckoutHandler ชี้​ไป​ที่ port (interface) เท่านั้น — code ฝั่ง​เรา​ไม่​เคย​ชี้​ไป​ที่ StripePaymentGateway หรือ SqlOrderRepository ตรงๆ เลย ส่วน adapter ของ​จริง​เป็น​ฝ่าย “ทำ​ตาม​สัญญา” (implements) ชี้​กลับ​เข้าหา port ทิศ​ของ​การ​พึ่งพา​จึง​ชี้​เข้าหา abstraction เสมอ และ​มี DI container ที่​ขอบ เป็น​ที่​เดียว​ที่​รู้จัก​ของ​จริง​และ​คอย​เดินสาย​ผูก port เข้า​กับ adapter ตอน app เริ่ม​ทำงาน นี่​คือ​รูปทรง​เดียว​กับ​ที่ version ดิบ​ทำ​ไม่​ได้ เพราะ​มัน​สร้าง​ของ​จริง​ปน​อยู่​ใน domain

บท​นี้​ทำให้ domain Order พึ่งพา​แค่ abstraction: interface เป็น port, generics ทำ base class และ repository ที่​ใช้​ซ้ำ​ได้, primary constructor รับ dependency สั้นๆ และ DI เดินสาย​ของ​จริง​เข้า​มา​ที่​ขอบ domain จึง test ได้​และ​สลับ infrastructure ได้​โดย​ไม่​ต้อง​แก้ตัว​มัน​เอง

เหลือ​อีก​ปม​หนึ่ง​ที่​เรา​เลื่อน​ไว้​ตลอด​บท: Task, async, await และ CancellationToken ที่​โผล่​มา​ใน​ทุก port

  • บท​ที่ 8 — async/await และ Task: ทำไม domain แท้ๆ ยัง​คง synchronous (ตัดสิน​ใจ​ล้วนๆ ไม่​รอ​อะไร) ส่วน async ไป​อยู่​ที่ ขอบ ที่​ต้อง​คุย​กับ I/O จริง และ​ปิด​คอร์ส​ด้วย​แผนที่​ว่า​แต่ละ feature ที่​เรียน​มา​ไป​ต่อยอด​ที่​คอร์ส​ไหน​ของ​สาย .NET/DDD

ส่วน​การ​ออกแบบ port กับ adapter ให้​ลึก​ใน​เชิง​สถาปัตยกรรม (ports and adapters, dependency inversion เต็ม​รูป) เก็บ​ไว้​ที่​คอร์ส Clean Architecture .NET และ​เรื่อง repository กับ aggregate ใน​เชิง DDD ที่​คอร์ส DDD in Code — บท​นี้​โฟกัส​ที่ “ไวยากรณ์ C# ที่​ทำให้ port เกิด​ขึ้น​ได้” ก่อน


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

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

  • Dependency Inversion Principle (DIP) — หลักการ​เบื้องหลัง​ทั้ง​บท: module ระดับ​สูง (domain) ไม่​ควร​พึ่ง module ระดับ​ล่าง (infrastructure) แต่​ทั้ง​คู่ควร​พึ่ง abstraction เดียวกัน
  • Dependency Injection — แนว​ปฏิบัติ​ของ​การ​ส่ง dependency เข้า​มา​จาก​ภายนอก​และ​เดินสาย​ที่ container ตรง​ขอบ
  • Repository Pattern — pattern เบื้องหลัง IOrderRepository: ห่อ​การ​เก็บ/ดึง​ข้อมูล​ไว้​หลัง interface ให้ domain ไม่รู้จัก​ฐาน​ข้อมูล​จริง

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

ข้อ 1 / 3

interface (port) อย่าง `IPaymentGateway` ให้อะไรกับ domain?