ทำไมต้องมีโครง — จากวงแหวนสู่ 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 เต็มของคอร์สนี้อยู่ที่ repo kaen-food-ordering (กำลังจัดทำ) — แต่ละบทจะชี้ path ที่ตรงกัน
จากวงแหวนสู่ 4 project
หัวข้อที่มีชื่อว่า “จากวงแหวนสู่ 4 project”ภาพจำของ 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 Cases →
FoodOrdering.Application— ที่อยู่ของ use caseUse Caseกฎธุรกิจเฉพาะ application (application business rule) ที่ประสานการไหลของข้อมูลเข้า-ออก Entities เพื่อทำงานหนึ่งอย่างให้จบ เช่น PlaceOrder — ใน code คือ handler ของ command/queryArchitecture อย่างPlaceOrderที่ประสานการไหลของข้อมูลเข้า-ออก Entities เพื่อทำงานหนึ่งอย่างให้จบ รวมถึง portPortinterface ที่วงในประกาศไว้เพื่อคุยกับโลกภายนอก (เช่น IPaymentGateway) โดยไม่รู้จัก implementation จริงArchitecture ที่วงในประกาศไว้เพื่อคุยกับโลกภายนอก - Interface Adapters →
FoodOrdering.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 ที่อยู่วงในสุด
สังเกตให้ดีว่าลูกศรทุกเส้น ชี้ไปทางเดียวกัน คือจากวงนอกเข้าหาวงใน ไม่มีเส้นไหนวิ่งย้อนออก นี่ไม่ใช่เรื่องบังเอิญหรือความสวยงามของไดอะแกรม แต่มันคือกฎข้อเดียวที่ค้ำสถาปัตยกรรมนี้ไว้ทั้งหมด ซึ่งเรากำลังจะไปดูกันในหัวข้อถัดไป
Dependency Rule = ทิศของ ProjectReference
หัวข้อที่มีชื่อว่า “Dependency Rule = ทิศของ ProjectReference”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 แล้วหวังว่าทุกคนจะจำ
เชื่อมกับ DIP
หัวข้อที่มีชื่อว่า “เชื่อมกับ DIP”มาถึงตรงนี้อาจมีคำถามค้างคาใจ: ถ้า 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:
- Dependency Inversion Principle — หลักที่ Dependency Rule ขยายจากระดับ class สู่ระดับสถาปัตยกรรม
- Separation of Concerns — เหตุผลที่แยกเป็นสี่ชั้น
เช็กความเข้าใจ — บทที่ 1
ข้อ 1 / 3ใน Clean Architecture ทิศทางของ source-code dependency ต้องชี้ไปทางใด?