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

ทำไม​ต้อง​มี​โครง — จาก​วงแหวน​สู่ 4 project

ใน​คอร์ส Event Storming เรา​ยืน​หน้า​ผนัง แปะ Post-it สี​ส้ม แล้ว​ค้น​เจอ domain ฟู้ด​เดลิ​เวอรี​ทั้ง​เส้น — ตั้งแต่ OrderPlaced ไปจน FoodDelivered และ​ใน​บท​สุดท้าย​เรา​ก็​ลาก​เส้น​แบ่ง bounded contextBounded Contextขอบเขต​ที่ ubiquitous language และ model ชุด​หนึ่ง​มี​ความหมาย​คง​เส้น​คง​วา เช่น Ordering, Payment, Dispatch ใน domain ฟู้ด​เดลิ​เวอรีStrategic Design ออก​มา​เป็น Ordering, Payment, Dispatch พร้อม​จะ​กลาย​เป็น code (ถ้า​ยัง​ไม่​ได้​อ่าน แนะนำ​ให้​แวะ​ไป​ที่ จาก Event Storming สู่ Bounded Context และ code ก่อน) ตอน​นี้​เรา​มี “แผนที่” แล้ว คำถาม​ต่อ​ไป​คือ จะ​ลงมือ​สร้าง​ของ​จริง​ยังไง​ให้​กฎ​ธุรกิจ​ที่​เพิ่ง​ค้น​เจอ​ไม่​ถูก​กลืน​หาย​ไป​ใน code ของ framework

คำ​ตอบ​ชื่อ Clean ArchitectureClean Architectureแนว​สถาปัตยกรรม​ของ Robert C. Martin ที่​จัด code เป็น​วงแหวน​ซ้อน​กัน (Entities → Use Cases → Interface Adapters → Frameworks) ภาย​ใต้ Dependency Rule เพื่อ​กัน business rules ออก​จาก UI ฐาน​ข้อมูล และ frameworkArchitecture — แนว​สถาปัตยกรรม​ของ Robert C. Martin (Uncle Bob) ที่​จัด code เป็น​วงแหวน​ซ้อน​กัน เพื่อ​กัน business rules ออก​จาก UI ฐาน​ข้อมูล และ framework ถ้า​คุณ​เคย​อ่าน​บทความ The Clean Architecture มา​แล้ว บทความ​นั้น​ตอบ​คำถาม​ว่า ทำไม ถึง​ต้อง​จัด​โครง​แบบ​นี้ ส่วน​คอร์ส​นี้​จะ​ตอบ​ว่า ยังไง — เรา​จะ​แปล​วงแหวน​บน​กระดาษ​ให้​กลาย​เป็น​โซลูชัน .NET ที่ dotnet build ผ่าน​ได้​จริง ที​ละ​บรรทัด ทีละ project

📦 code ตัวอย่าง

code เต็ม​ของ​คอร์ส​นี้​อยู่​ที่ repo kaen-food-ordering (กำลัง​จัด​ทำ) — แต่ละ​บท​จะ​ชี้ path ที่​ตรง​กัน

ภาพ​จำ​ของ Clean Architecture คือ​วงแหวน​สี่​วง​ซ้อน​กัน วงใน​สุด​คือ​กฎ​ที่​เป็น​แก่น​ที่สุด วงนอก​สุด​คือ​ของ​ที่ “เปลี่ยน​บ่อย​และ​ไม่​สำคัญ​ต่อ​ธุรกิจ” อย่าง framework หรือ​ยี่ห้อ​ฐาน​ข้อมูล บน​กระดาษ​มัน​สวย​ดี แต่​พอ​ต้อง​ลงมือ​จริง เรา​ต้อง​ตอบ​คำถาม​ที่​เป็น​รูปธรรม​กว่า​นั้น: วงแหวน​พวก​นี้ อยู่​ที่ไหน​ใน​โซลูชัน .NET

คำ​ตอบ​ที่​ตรง​ไป​ตรง​มา​ที่สุด​คือ​แปลง​หนึ่ง​วงแหวน​ให้​เป็น1 project (.csproj) โดย​ตั้ง​ชื่อ​ตาม domain ของ​เรา:

  • Entities (วงใน​สุด)FoodOrdering.Domain — ที่​อยู่​ของ entityEntityobject ที่​มี identity คงที่​ตลอด​อายุ แม้​ค่า​ข้าง​ใน​เปลี่ยน เช่น Order ที่​มี OrderIdTactical Design, value objectValue Objectobject ที่​นิยาม​ด้วย 'ค่า' ไม่ใช่ identity เปลี่ยนแปลง​ไม่​ได้ (immutable) และ​เทียบเท่า​กัน​เมื่อ​ค่า​เท่า​กัน เช่น Money, AddressTactical Design และ aggregateAggregateกลุ่ม​ของ object ที่​ถือ​เป็น​หนึ่ง​หน่วย​ความ​สอดคล้อง (consistency boundary) มี root เดียว​เป็น​ประตู​เข้า และ​คอย​รักษา invariant ของ​ทั้ง​กลุ่ม เช่น Order ที่​คุม OrderLineTactical Design อย่าง Order ที่​คุม OrderLine ไว้ พวก​นี้​คือ​กฎ​ธุรกิจ​ล้วนๆ ที่​จริง​แม้​ไม่มี app นี้​ก็ได้ (ร้าน​อาหาร​ก็​มี “ออเดอร์” ที่​มี invariant ของ​มัน​อยู่​แล้ว)
  • Use CasesFoodOrdering.Application — ที่​อยู่​ของ use caseUse Caseกฎ​ธุรกิจ​เฉพาะ application (application business rule) ที่​ประสาน​การ​ไหล​ของ​ข้อมูล​เข้า-ออก Entities เพื่อ​ทำงาน​หนึ่ง​อย่าง​ให้​จบ เช่น PlaceOrder — ใน code คือ handler ของ command/queryArchitecture อย่าง PlaceOrder ที่​ประสาน​การ​ไหล​ของ​ข้อมูล​เข้า-ออก Entities เพื่อ​ทำงาน​หนึ่ง​อย่าง​ให้​จบ รวม​ถึง portPortinterface ที่​วงใน​ประกาศ​ไว้​เพื่อ​คุย​กับ​โลก​ภายนอก (เช่น IPaymentGateway) โดย​ไม่รู้จัก implementation จริงArchitecture ที่​วงใน​ประกาศ​ไว้​เพื่อ​คุย​กับ​โลก​ภายนอก
  • Interface AdaptersFoodOrdering.Infrastructure — ที่​อยู่​ของ adapterAdaptercode วงนอก​ที่ implement port เพื่อ​เชื่อม​กับ​เทคโนโลยี​จริง เช่น EF Core repository หรือ HTTP payment clientArchitecture ที่​แปลง​ระหว่าง​โลก​ของ​เรา​กับ​เทคโนโลยี​จริง เช่น repositoryRepositoryabstraction ที่​ทำให้ domain เข้าถึง/บันทึก aggregate ได้​เหมือน​เป็น in-memory collection โดย​ไม่รู้จัก​เทคโนโลยี​จัด​เก็บ​จริง — interface อยู่​วงใน implementation อยู่ InfrastructureTactical Design ที่​ทำ​ด้วย EF Core หรือ HTTP client ที่​ยิง​ไป​หา payment gateway
  • Frameworks & Drivers (วงนอก​สุด)FoodOrdering.Web — ASP.NET Core, endpoint ที่​รับ HTTP, และ composition rootComposition Rootจุด​เดียว​ใกล้​ขอบ​สุด​ของ app (ใน .NET คือ Program.cs / DI container) ที่​ประกอบ interface เข้า​กับ implementation จริง เป็น​ที่​เดียว​ที่ 'รู้จัก​ทุก​อย่าง'Architecture ที่​ประกอบ​ทุก​อย่าง​เข้า​ด้วย​กัน​ตอน app ส​ตาร์ต
flowchart LR
  Web["FoodOrdering.Web<br/>(API, Composition Root)"] --> Infra["FoodOrdering.Infrastructure<br/>(EF Core, adapters)"]
  Infra --> App["FoodOrdering.Application<br/>(use cases, ports)"]
  App --> Dom["FoodOrdering.Domain<br/>(entities, VO, aggregate)"]
  classDef inner fill:#16a34a,stroke:#065f46,color:#f8fafc;
  class Dom inner;

คำ​บรรยาย​ภาพ: ลูกศร = ทิศทาง ProjectReference; ทุก​ลูกศร​ชี้​เข้าหา Domain ที่​อยู่​วงใน​สุด

สังเกต​ให้​ดี​ว่า​ลูกศร​ทุก​เส้น ชี้​ไป​ทาง​เดียวกัน คือ​จาก​วงนอก​เข้าหา​วงใน ไม่มี​เส้น​ไหน​วิ่ง​ย้อน​ออก นี่​ไม่ใช่​เรื่อง​บังเอิญ​หรือ​ความ​สวยงาม​ของ​ไดอะแกรม แต่​มัน​คือ​กฎ​ข้อ​เดียว​ที่​ค้ำ​สถาปัตยกรรม​นี้​ไว้​ทั้งหมด ซึ่ง​เรา​กำลัง​จะ​ไป​ดู​กัน​ใน​หัวข้อ​ถัด​ไป

Clean Architecture มี​กฎ​อยู่​ข้อ​เดียวจริงๆ Uncle Bob เรียก​มัน​ว่า Dependency RuleDependency Ruleกฎ​เดียว​ของ Clean Architecture: source-code dependency ต้อง​ชี้​เข้า​ด้าน​ใน​เสมอ วงใน​ห้าม​รู้จัก​วงนอก — ใน code .NET แปล​เป็น​ทิศทาง​ของ ProjectReference ระหว่าง projectArchitecture: source-code dependency ต้อง​ชี้​เข้า​ด้าน​ใน​เสมอ วงใน​ห้าม​รู้จัก​วงนอก พูด​อีก​แบบ​คือ code ใน​วงใน​ไม่​ควร​มีชื่อ​ของ​อะไร​ก็ตาม​ที่​อยู่​วงนอก​โผล่​มา​เลย — ไม่​ว่า​จะ​เป็น​ชื่อ class ชื่อ function ชื่อ​ตัวแปร หรือ​ชื่อ package

ใน​โลก​ของ​ทฤษฎี​นี่​ฟัง​ดู​นามธรรม แต่​ใน .NET มัน​กลาย​เป็น​สิ่ง​ที่​จับ​ต้อง​ได้​และ​บังคับ​ได้​จริง เพราะ “dependency ระหว่าง​วงแหวน” แปล​ตรง​ตัว​เป็น <ProjectReference> ระหว่าง project คือ:

ถ้า project A มี <ProjectReference> ชี้​ไป​หา B แปล​ว่า A พึ่งพา B — ลูกศร​ใน​ไดอะแกรม​ข้าง​บน​คือ​เส้น ProjectReference นี่แหละ

ผล​ที่​ตาม​มา​สำคัญ​มาก และ​เป็น​สิ่ง​ที่​คุณ​จะ​ยึด​เป็น​เข็มทิศ​ตลอด​คอร์ส: FoodOrdering.Domain ต้อง​ไม่มี <ProjectReference> ออก​ไป​หา​ใคร​เลย มัน​คือ​วงใน​สุด ไม่มี​อะไร​อยู่​ใน​กว่า​นี้​ให้​พึ่งพา file .csproj ของ​มัน​จึง​เปล่า​เปลือย​แบบ​นี้:

<!-- FoodOrdering.Domain.csproj — ไม่มี ProjectReference ออกนอกเลย -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net10.0</TargetFramework></PropertyGroup>
</Project>

ไม่มี EF Core ไม่มี ASP.NET ไม่มี​แม้แต่ project พี่น้อง​ของ​ตัวเอง Domain จึง “บริสุทธิ์” คือ​รู้จัก​แต่ .NET base class library กับ​กฎ​ธุรกิจ​ของ​มัน​เอง ผลพลอยได้​คือ​มัน​ทดสอบ​ง่าย​มาก เพราะ unit testUnit Testtest หน่วย​เล็ก​ที่สุด​แบบ​แยก​ตัว รัน​เร็ว ไม่​พึ่ง DB/เครือข่าย — ใน​สถาปัตยกรรม​นี้ domain ทดสอบ​ได้​โดย​ไม่​ต้อง mock เพราะ​มัน​บริสุทธิ์​อยู่​แล้วArchitecture ของ Domain ไม่​ต้อง​ต่อ​ฐาน​ข้อมูล​หรือ mock อะไร​เลย

ถัด​ออก​มา​หนึ่ง​วง​คือ FoodOrdering.Application มัน​ได้​รับ​อนุญาต​ให้​พึ่งพา Domain (เพราะ Domain อยู่​ใน​กว่า) แต่ ห้าม รู้จัก Infrastructure หรือ Web ดังนั้น .csproj ของ​มัน​จึง​มี ProjectReference ออก​ไป​แค่​เส้น​เดียว:

<!-- FoodOrdering.Application.csproj — reference เข้าหา Domain เท่านั้น -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net10.0</TargetFramework></PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\FoodOrdering.Domain\FoodOrdering.Domain.csproj" />
</ItemGroup>
</Project>

ไล่ออก​ไป​ต่อ​เนื่อง ตาม​หลัก​เดียวกัน: Infrastructure reference Application (เพื่อ implement port ที่ Application ประกาศ​ไว้), และ Web reference ทั้ง Application และ Infrastructure เพราะ​มัน​คือ​วงนอก​สุด​ที่​ต้อง​ประกอบ​ทุก​อย่าง​เข้า​ด้วย​กัน ความ​สวยงาม​ของ​วิธี​นี้​คือ compiler กลาย​เป็น​ยาม​เฝ้า​กฎ ให้​เรา​ฟรี — วัน​ไหน​มี​คน​เผลอ​ไป​พิมพ์ code ใน Domain ที่​เรียก EF Core เข้า project จะ build ไม่​ผ่าน​ทันที เพราะ Domain ไม่มี reference ไปหา Infrastructure ให้​เรียก กฎ​ที่​บังคับ​ได้​ด้วย compiler ย่อม​แข็งแรง​กว่า​กฎ​ที่​เขียน​ไว้​ใน wiki แล้ว​หวัง​ว่า​ทุก​คน​จะ​จำ

มา​ถึง​ตรง​นี้​อาจ​มี​คำถาม​ค้าง​คา​ใจ: ถ้า Domain กับ Application ห้าม​รู้จัก Infrastructure แล้ว use case อย่าง PlaceOrder จะบันทึก​ออเดอร์​ลง​ฐาน​ข้อมูล​ได้​ยังไง ใน​เมื่อ code EF Core อยู่​วง Infrastructure ที่​อยู่ “ข้าง​นอก” คำ​ตอบ​คือ Dependency Rule เป็น​เพียง​ญาติ​ผู้ใหญ่​ของ​หลัก​ที่​คุณ​อาจ​รู้จัก​อยู่​แล้ว​ใน​ระดับ class นั่น​คือ Dependency Inversion Principle (DIP) — วงใน​ประกาศ interface (port) ที่​มัน​ต้องการ เช่น IOrderRepository ไว้​ใน​ตัวเอง แล้ว​ให้​วงนอก​เป็น​ฝ่าย​มา implement interface นั้น dependency ใน code จึง​ยัง​ชี้​เข้า​ใน (Infrastructure รู้จัก Application) ทั้ง​ที่ ณ runtime ข้อมูล​ไหล​ออก​ไป​โดน​ฐาน​ข้อมูล​จริง Clean Architecture คือ​การ​เอา DIP ที่​เรา​เคย​ใช้​กลับ​ทิศ dependency ระหว่าง class มา “ขยาย​สเกล” ให้​กลับ​ทิศ dependency ระหว่าง​ชั้น​สถาปัตยกรรม​ทั้ง​ชั้น

ถ้า​ภาพ​วงแหวน​ซ้อน​กัน​ยัง​รู้สึก​นามธรรม ลอง​มอง​อีก​มุม​ผ่าน Hexagonal ArchitectureHexagonal ArchitecturePorts & Adapters ของ Alistair Cockburn — แกน app อยู่​กลาง คุย​กับ​ภายนอก​ผ่าน port/adapter เท่านั้น พูด​เรื่อง​เดียว​กับ Clean Architecture ด้วย​รูปทรง​ต่าง​กันArchitecture (Ports & Adapters) ของ Alistair Cockburn ที่​พูด​เรื่อง​เดียวกัน​ด้วย​รูปทรง​ต่าง​กัน — แกน app อยู่​ตรง​กลาง คุย​กับ​โลก​ภายนอก​ผ่าน port/adapter เท่านั้น อยาก​เข้าใจ​แง่​มุม​นี้​ให้​ลึก​ขึ้น อ่าน​ต่อ​ได้ที่​บทความ Hexagonal Architecture วงแหวน​ของ Uncle Bob กับ​หก​เหลี่ยม​ของ Cockburn เป็น​ภาพ​วาด​คนละ​แบบ​ของ​หลัก​คิด​เดียวกัน คือ กัน​แก่น​ธุรกิจ​ไว้​ตรง​กลาง แล้ว​ผลัก​รายละเอียด​ออก​ไป​อยู่​ขอบ

บท​นี้​เรา​วาง​เสาเข็ม​ไว้​สาม​ต้น: หนึ่ง — Clean Architecture คือ​วงแหวน​สี่​วง​ที่​เรา​แปลง​เป็น4 project FoodOrdering.Domain / .Application / .Infrastructure / .Web สอง — Dependency Rule มี​กฎ​เดียว​คือ dependency ชี้​เข้า​ใน ซึ่ง​ใน .NET คือ​ทิศทาง​ของ <ProjectReference> และ Domain ต้อง​ไม่ reference ออก​ไป​ไหน​เลย สาม — เมื่อ​วงใน​ต้อง​ใช้​ของ​วงนอก เรา​กลับ​ทิศ​ด้วย DIP ให้​วงใน​ประกาศ port แล้ว​ให้​วงนอก​มา implement

จาก​นี้​ไป​เรา​จะ​สร้าง​ทั้ง​สี่​ชั้น​ที​ละ​บท โดย​ลงมือ​กับ feature จริง​ของฟู้ด​เดลิ​เวอรี​ชุด F1–F5 ไล่​ตั้งแต่​วาง​โครง​โซลูชัน, ปั้น Domain model ของ Order, เขียน use case PlaceOrder, ต่อ EF Core ที่ Infrastructure, ไป​จน​เปิด API endpoint ที่ Web แล้ว​ค่อย​ยก​ระดับ​ด้วย เคส​ซับซ้อน C1–C3 (เช่น การ​ยกเลิก​ออเดอร์ การ​คืน​เงิน และ​การ​รับมือ​กับ payment ที่​ล้มเหลว) ใน​ช่วง​ท้าย​คอร์ส ทุก​บท​จะ​กลับ​มา​ที่​โซลูชัน​เดียวกัน​นี้​เสมอ และ​ทุก​บรรทัด​ที่​เรา​เขียน​จะ​เคารพ​ลูกศร​ที่​ชี้​เข้า​ใน​ของ Dependency Rule


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

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

  • Dependency Inversion Principle — หลัก​ที่ Dependency Rule ขยาย​จาก​ระดับ class สู่​ระดับ​สถาปัตยกรรม
  • Separation of Concerns — เหตุผล​ที่​แยก​เป็น​สี่​ชั้น

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

ข้อ 1 / 3

ใน Clean Architecture ทิศทางของ source-code dependency ต้องชี้ไปทางใด?