The Clean Architecture: วงแหวนที่ปกป้อง business rules จากกรอบงานและรายละเอียด
บทความนี้เป็น บทแปลภาษาไทยอย่างไม่เป็นทางการ ของ “The Clean Architecture” โดย Robert C. Martin (หรือที่รู้จักกันในชื่อ Uncle Bob) เผยแพร่ครั้งแรกบน The Clean Code Blog เมื่อ 13 สิงหาคม 2012
แปลและเรียบเรียงเพื่อการศึกษา — ไดอะแกรมวงแหวนซ้อนและไดอะแกรมการข้ามผ่านขอบเขตถูกวาดใหม่เป็น Mermaid ภาษาไทย เพื่อให้อ่านและค้นหาได้สะดวกในเว็บ ศัพท์เทคนิคคงไว้เป็นภาษาอังกฤษตามต้นฉบับ ลิขสิทธิ์ของเนื้อหาต้นฉบับเป็นของผู้เขียน
ตลอดสิบกว่าปีที่ผ่านมา วงการซอฟต์แวร์เสนอแนวทางการจัดสถาปัตยกรรมออกมาหลายชื่อ ทั้ง Hexagonal Architecture, Onion Architecture, Screaming Architecture, DCI และ BCE แต่ละชื่อมาจากคนละคน คนละบริบท คนละคำศัพท์
แต่ Uncle Bob ชี้ให้เห็นว่าเมื่อมองลึกลงไปในโครงสร้างของแต่ละแนวทาง พวกมันล้วนพยายามแก้ปัญหาเดียวกัน และมาบรรจบกันที่ข้อสรุปเดียวกันอย่างน่าทึ่ง นั่นคือการแบ่งซอฟต์แวร์ออกเป็นชั้น ๆ (layer) เพื่อแยก business rules ออกจากรายละเอียดที่ไม่เกี่ยวข้องกับธุรกิจ
จุดร่วมของสถาปัตยกรรมที่ดูต่างกัน
หัวข้อที่มีชื่อว่า “จุดร่วมของสถาปัตยกรรมที่ดูต่างกัน”- Hexagonal Architecture (หรือ Ports and Adapters) ของ Alistair Cockburn วางแกน application ไว้ตรงกลาง แล้วให้มันคุยกับโลกภายนอกผ่าน port และ adapter เท่านั้น
- DCI (Data, Context, Interaction) ของ James Coplien และ Trygve Reenskaug เน้นแยกบทบาท (role) ที่ object แสดงออกในแต่ละ use case ออกจากตัวข้อมูลถาวรของ object นั้น
- BCE (Boundary, Control, Entity) ของ Ivar Jacobson ซึ่งเป็นส่วนหนึ่งของ Objectory/ICONIX แบ่งออบเจ็กต์ออกเป็นสามบทบาท คือ boundary object ที่คุยกับภายนอก, control object ที่คุมตรรกะของ use case และ entity object ที่แทนความรู้ของ domain
- Onion Architecture ของ Jeffrey Palermo ก็วาดวงกลมซ้อนกันเช่นเดียวกัน โดยให้ domain model อยู่ที่แกนกลาง ล้อมด้วยชั้น application service แล้วจึงเป็นชั้นโครงสร้างพื้นฐานที่อยู่นอกสุด
แม้จะต่างกันในรายละเอียดของการแบ่งชั้นและคำศัพท์ที่ใช้ แต่ทั้งหมดนี้มีเป้าหมายร่วมเดียวกันคือ แยกเรื่องของธุรกิจ (business) ออกจากเรื่องของกลไก (mechanism) Uncle Bob จึงนำทั้งหมดมาสังเคราะห์รวมเป็นภาพเดียวที่เขาเรียกว่า The Clean Architecture
ระบบที่ปฏิบัติตามแนวทางนี้จะมีคุณสมบัติสำคัญห้าข้อ:
- ไม่ผูกติดกับ framework (Framework-independent) — สถาปัตยกรรมไม่พึ่งพาการมีอยู่ของ library หรือ framework ใด ๆ คุณใช้ framework เป็น “เครื่องมือ” ได้ ไม่ใช่ต้องยัดระบบทั้งหมดลงไปอยู่ใน constraint ที่ framework กำหนด
- ทดสอบได้ (Testable) — business rules ทดสอบได้โดยไม่ต้องมี UI, ฐานข้อมูล, web server หรือองค์ประกอบภายนอกอื่นใดเลย
- ไม่ผูกติดกับ UI — UI เปลี่ยนได้ง่ายโดยไม่กระทบส่วนที่เหลือของระบบ เช่น สลับจากเว็บมาเป็น console UI ได้โดยไม่ต้องแตะ business rules เลย
- ไม่ผูกติดกับฐานข้อมูล — สลับ Oracle หรือ SQL Server ไปเป็น MongoDB, BigTable, CouchDB หรืออะไรก็ได้ตามใจ เพราะ business rules ไม่ได้ผูกติดกับฐานข้อมูลใดฐานข้อมูลหนึ่ง
- ไม่ผูกติดกับสิ่งใด ๆ ในโลกภายนอก — จริง ๆ แล้ว business rules ไม่รู้อะไรเลยเกี่ยวกับโลกภายนอก
วงแหวนซ้อนกันสี่ชั้น
หัวข้อที่มีชื่อว่า “วงแหวนซ้อนกันสี่ชั้น”ภาพต้นฉบับของ Uncle Bob วาดเป็นวงกลมสี่วงซ้อนกัน โดยยิ่งอยู่วงในมากเท่าไร ก็ยิ่งเป็นนโยบาย (policy) ที่เป็นนามธรรมและกว้างระดับองค์กรมากขึ้นเท่านั้น ยิ่งอยู่วงนอกมากเท่าไร ก็ยิ่งเป็นกลไกที่เป็นรูปธรรมและใกล้ตัวเครื่อง (mechanism) มากขึ้นเท่านั้น
เมื่อวาดใหม่เป็นผังลำดับชั้นแบบ Mermaid (แทนวงกลมจริง) จะได้ดังนี้:
flowchart TB
subgraph FD["Frameworks and Drivers"]
subgraph IA["Interface Adapters"]
subgraph UCASE["Use Cases"]
subgraph ENT["Entities"]
E1[Enterprise business rules]
end
U1[Application business rules]
end
C1[Controllers]
C2[Presenters]
C3[Gateways]
end
W1[Web and UI frameworks]
W2[Database]
W3[Devices and external interfaces]
end
W1 --> C1
C1 --> U1
U1 --> E1
ภาพ 1 — วงแหวนซ้อนกันสี่ชั้นของ Clean Architecture จากในสุดไปนอกสุด: Entities, Use Cases, Interface Adapters, Frameworks and Drivers ลูกศรแสดงทิศทางของ source-code dependency ที่ชี้เข้าด้านในเสมอ ตั้งแต่ framework ไปยัง controller ไปยัง use case ไปจนถึง entity · อ่านต้นฉบับ ↗
Entities — กฎธุรกิจระดับองค์กร
หัวข้อที่มีชื่อว่า “Entities — กฎธุรกิจระดับองค์กร”วงในสุดคือ Entities ซึ่งห่อหุ้ม enterprise-wide business rules เอาไว้ นั่นคือกฎเกณฑ์และตรรกะทางธุรกิจที่สำคัญที่สุดของกิจการ ไม่ว่าจะรันด้วย application ตัวไหนก็ตาม
Entity อาจเป็น object ที่มีทั้งข้อมูลและ method หรือจะเป็นชุดของโครงสร้างข้อมูลกับ function แยกกันก็ได้ ไม่สำคัญ — สิ่งที่สำคัญคือ Entity ต้องเป็นสิ่งที่ เปลี่ยนแปลงน้อยที่สุด เมื่อโลกภายนอกเปลี่ยน
ถ้าเราเปลี่ยนวิธีนำทางในหน้าจอ หรือเปลี่ยนนโยบายด้านความปลอดภัย นั่นไม่ควรมีผลกับ Entity เลยแม้แต่น้อย เพราะ Entity อยู่ในระดับที่เป็นนามธรรมและกว้างที่สุดในระบบ
Use Cases — กฎธุรกิจเฉพาะ application
หัวข้อที่มีชื่อว่า “Use Cases — กฎธุรกิจเฉพาะ application”ถัดออกมาคือชั้น Use Cases ซึ่งบรรจุ application-specific business rules เอาไว้ ชั้นนี้ประสาน (orchestrate) การไหลของข้อมูลเข้าและออกจาก Entities และสั่งให้ Entities ใช้กฎธุรกิจระดับองค์กรของมันเพื่อบรรลุเป้าหมายของ use case นั้น ๆ โดยเฉพาะ
เราไม่คาดหวังว่าการเปลี่ยนแปลงในชั้นนี้จะกระทบ Entities และก็ไม่คาดหวังว่าชั้นนี้จะได้รับผลกระทบจากการเปลี่ยนแปลงภายนอก เช่น ฐานข้อมูล, UI หรือ framework ที่ใช้
ในทางกลับกัน ถ้าพฤติกรรมของ application เปลี่ยน — เช่น เปลี่ยนขั้นตอนของ use case — ชั้นนี้ก็คือชั้นที่ควรได้รับผลกระทบนั้น
Interface Adapters — ตัวแปลงข้อมูลระหว่างโลกใน-นอก
หัวข้อที่มีชื่อว่า “Interface Adapters — ตัวแปลงข้อมูลระหว่างโลกใน-นอก”ชั้นถัดมาคือ Interface Adapters ซึ่งเป็นชุดของตัวแปลง (adapter) ที่แปลงข้อมูลจากรูปแบบที่สะดวกต่อ use cases และ Entities ไปเป็นรูปแบบที่สะดวกต่อสิ่งภายนอก เช่น ฐานข้อมูลหรือเว็บ และแปลงกลับในทิศทางตรงข้าม
ชั้นนี้คือที่อยู่ของสถาปัตยกรรม MVC ทั้งชุดสำหรับ GUI — Controllers, Presenters และ Views — Model ในที่นี้ก็เป็นเพียงโครงสร้างข้อมูลที่ Controller ส่งเข้าไปยัง use case แล้วส่งกลับออกมาจาก use case ไปยัง Presenter หรือ View เท่านั้นเอง
ในชั้นเดียวกันนี้ยังมี Gateways ซึ่งทำหน้าที่แปลงข้อมูลไปมากับฐานข้อมูล
กฎสำคัญคือ SQL ทั้งหมดควรจำกัดอยู่ในชั้นนี้เท่านั้น — โดยเฉพาะในส่วนของ Interface Adapters ที่เกี่ยวข้องกับฐานข้อมูล — ไม่มีรหัสในวงในกว่านี้ (Use Cases หรือ Entities) ที่ควรรู้จักรายละเอียดของฐานข้อมูลเลยแม้แต่น้อย
Frameworks and Drivers — รายละเอียดที่อยู่วงนอกสุด
หัวข้อที่มีชื่อว่า “Frameworks and Drivers — รายละเอียดที่อยู่วงนอกสุด”วงนอกสุดคือ Frameworks and Drivers ซึ่งประกอบด้วย framework และเครื่องมือทั้งหลาย เช่น ฐานข้อมูล, เว็บ framework ฯลฯ โดยทั่วไปแล้วรหัสในวงนี้ควรมีน้อย ส่วนใหญ่เป็นเพียง glue code ที่คอยสื่อสารเข้าไปยังวงถัดเข้าไป
Uncle Bob ย้ำเสมอว่า “the Web is a detail. The database is a detail.” — เว็บก็คือรายละเอียด ฐานข้อมูลก็คือรายละเอียด — เพราะสิ่งเหล่านี้ล้วนเป็นรายละเอียดที่อยู่วงนอกสุด การเปลี่ยนแปลงในวงนี้จึงควรสร้างความเสียหายน้อยที่สุดต่อระบบ
กฎที่ทำให้ทุกอย่างในภาพนี้ทำงานได้คือกฎเดียว เรียบง่าย แต่เข้มงวด:
source-code dependency ต้องชี้เข้าด้านในเท่านั้น
ไม่มีสิ่งใดในวงในที่จะรู้จักอะไรเกี่ยวกับสิ่งที่อยู่ในวงนอกได้เลย รวมถึงชื่อ function ชื่อ class ตัวแปร หรือ entity ใด ๆ ที่ถูกประกาศไว้ในวงนอก
และข้อมูลรูปแบบใดก็ตามที่ถูกสร้างขึ้นในวงนอก (เช่น รูปแบบที่ framework กำหนด) ก็ไม่ควรถูกส่งเข้าไปปนเปื้อนวงใน เพราะจะทำให้วงในต้องรู้จักรายละเอียดของวงนอกทางอ้อม ซึ่งขัดกับหลักการทั้งหมด
ผลลัพธ์คือยิ่งอยู่ใกล้ศูนย์กลางมากเท่าไร code ก็ยิ่งเป็นนโยบายระดับสูง เป็นนามธรรม และเปลี่ยนแปลงน้อยเท่านั้น ส่วนยิ่งอยู่วงนอกมากเท่าไร code ก็ยิ่งเป็นกลไกที่เป็นรูปธรรม ผูกกับเทคโนโลยี และคาดว่าจะเปลี่ยนบ่อยกว่า
หลักการนี้คือรูปแบบเฉพาะของ Dependency Inversion Principle (DIP) ที่ขยายจากระดับ class ไปสู่ระดับสถาปัตยกรรมทั้งระบบ — module ระดับสูง (policy) ไม่ควรพึ่งพา module ระดับต่ำ (detail) ทั้งสองฝ่ายควรพึ่งพา abstraction ร่วมกันแทน
แค่สี่วงเท่านั้นหรือ?
หัวข้อที่มีชื่อว่า “แค่สี่วงเท่านั้นหรือ?”Uncle Bob ย้ำว่าสี่วงในภาพนี้เป็นเพียง แผนผังเชิงตัวอย่าง (schematic) ไม่ใช่กฎตายตัวว่าต้องมีสี่ชั้นเป๊ะ ๆ เสมอไป บางระบบอาจต้องการวงเพิ่มเติมมากกว่านี้ ไม่มีกฎว่าคุณต้องมีแค่สี่วงเท่านั้น
แต่ กฎการพึ่งพายังคงต้องใช้เสมอ ไม่ว่าจะมีกี่วงก็ตาม source code ต้องชี้เข้าด้านในเสมอ ยิ่งเข้าใกล้ศูนย์กลาง ระดับของนโยบายและความเป็นนามธรรมก็ยิ่งสูงขึ้น และวงในสุดคือที่ที่บรรจุนโยบายทั่วไปที่สุดของระบบเอาไว้
ข้ามผ่านขอบเขต: control flow กับ Dependency Inversion Principle
หัวข้อที่มีชื่อว่า “ข้ามผ่านขอบเขต: control flow กับ Dependency Inversion Principle”จุดที่ดูขัดแย้งในตัวเองที่สุดของภาพนี้คือ ในการทำงานจริง control flow มักวิ่งข้ามขอบเขตจากวงในไปวงนอก เช่น Use Case ต้องส่งผลลัพธ์ออกไปให้ Presenter แสดงผล แต่กฎการพึ่งพากลับห้ามไม่ให้ source code ของ Use Case (วงใน) รู้จักหรืออ้างอิงถึง Presenter (วงนอก) โดยตรง
ทางออกคือใช้ polymorphism ผ่าน Dependency Inversion Principle เพื่อ “พลิก” ทิศทางของ dependency ให้สวนทางกับทิศทางของ control flow ตัวอย่างคลาสสิกคือคู่ระหว่าง Use Case Interactor กับ Presenter:
flowchart LR Controller -->|control flow calls| Interactor Interactor -->|control flow calls| OutputPort[Use Case Output Port] Presenter -.->|implements - dependency points inward| OutputPort OutputPort -.->|runtime dispatch| Presenter Presenter -->|control flow calls| ViewModel
ภาพ 2 — การข้ามผ่านขอบเขตระหว่าง Use Case กับ Interface Adapters Interactor เรียก interface ที่ชื่อ Use Case Output Port ซึ่งถูกประกาศไว้ในวงของ Use Case เอง (เส้นทึบ = control flow) ส่วน Presenter ซึ่งอยู่วงนอกเป็นผู้ implement interface นั้น (เส้นประ = source-code dependency) ผลคือ control flow วิ่งจาก Interactor ออกไปหา Presenter ได้ตามปกติ แต่ source-code dependency ของ Presenter กลับชี้ เข้าด้านใน หา interface ที่อยู่ในวง Use Case ไม่ใช่ Interactor ที่ชี้ออกไปหา Presenter · อ่านต้นฉบับ ↗
พูดให้ชัดขึ้น: Interactor เรียกใช้ OutputPort ซึ่งเป็น interface ที่มันเป็นเจ้าของ (ประกาศอยู่ในวง Use Cases เดียวกัน) ส่วน Presenter ที่อยู่ในวง Interface Adapters เป็นผู้ implement interface นั้น
เวลารันจริง Interactor เรียก method บน OutputPort แล้วมันก็ไปเรียกใช้ implementation จริงที่อยู่ใน Presenter — control flow ข้ามจากในไปนอกได้ตามที่ต้องการ
แต่ dependency ทาง compile ของ Presenter (ต้อง import/รู้จัก interface OutputPort) กลับชี้ เข้าด้านใน ไม่ใช่ Interactor ที่ต้องรู้จัก Presenter แบบเดิม
เทคนิคนี้คือกลไกเดียวที่ถูกใช้ซ้ำ ๆ ทุกครั้งที่ต้อง ข้ามขอบเขต (crossing boundaries) ในภาพนี้ ไม่ว่าจะเป็นขอบเขตระหว่าง Controller กับ Use Case Interactor (ผ่าน Use Case Input Port) หรือระหว่าง Use Case กับ Gateway ที่คุยกับฐานข้อมูล
ข้อมูลที่ข้ามผ่านขอบเขตต้องมีรูปแบบอย่างไร
หัวข้อที่มีชื่อว่า “ข้อมูลที่ข้ามผ่านขอบเขตต้องมีรูปแบบอย่างไร”เมื่อข้อมูลต้องข้ามขอบเขตจากวงหนึ่งไปอีกวงหนึ่ง มันต้องอยู่ในรูปแบบที่ เรียบง่ายและแยกตัวเป็นอิสระ (isolated) ที่สุด อาจเป็น struct ธรรมดา, Data Transfer Object (DTO), parameter ของ function หรือแม้แต่แพ็กเข้า hash map ก็ได้
สิ่งที่สำคัญที่สุดคือ ห้ามส่ง Entity object หรือแถวข้อมูลจากฐานข้อมูล (database row) ข้ามขอบเขตเข้าไปในทิศทางเข้าด้านในเด็ดขาด เพราะนั่นจะทำให้วงในต้องรู้จักรูปแบบข้อมูลของวงนอกทางอ้อม ซึ่งขัดกับกฎการพึ่งพาโดยตรง
รูปแบบของข้อมูลที่ข้ามขอบเขตควรถูกออกแบบให้สะดวกที่สุดสำหรับ วงในที่เป็นผู้รับ เสมอ ไม่ใช่สะดวกสำหรับวงนอกที่เป็นผู้ส่ง — นี่คือรายละเอียดเล็ก ๆ ที่มักถูกมองข้าม แต่เป็นตัวตัดสินว่า code วงในจะยังคง “ไม่รู้จัก” วงนอกได้จริงหรือไม่
ภาพรวมของการไหลข้อมูลในระบบทั่วไป
หัวข้อที่มีชื่อว่า “ภาพรวมของการไหลข้อมูลในระบบทั่วไป”ลองนึกภาพเว็บ application ทั่วไปที่คุยกับฐานข้อมูล: เว็บ server รวบรวมข้อมูล input แล้วส่งให้ Controller ซึ่งบรรจุข้อมูลนั้นลงใน Request Model ที่เป็นโครงสร้างข้อมูลธรรมดา แล้วส่งต่อให้ Use Case Interactor
ตัว Interactor ตีความข้อมูลนั้นและใช้มันควบคุมการทำงานของ Entities มันอาจต้องเรียก Gateway (ผ่าน output port ของฝั่งข้อมูล) เพื่อดึงข้อมูลเพิ่มเติมจากฐานข้อมูล จัดข้อมูลให้อยู่ในรูปแบบที่ Entities ใช้งานสะดวก แล้วสั่งให้ Entities ทำงานตามกฎธุรกิจของมัน
จากนั้น Interactor รวบรวมผลลัพธ์จาก Entities มาสร้างเป็น Response Model แล้วส่งผ่าน output port ไปยัง Presenter ซึ่งมีหน้าที่แปลง Response Model ให้กลายเป็น View Model ที่พร้อมแสดงผล ก่อนจะส่งต่อไปยัง View เพื่อ render จริง
สังเกตว่า dependency ทั้งหมดในเรื่องนี้ชี้เข้าหา Use Cases เสมอ ไม่ว่าจะมาจากฝั่ง Controller/Presenter หรือฝั่ง Gateway ก็ตาม — นี่คือภาพรวมที่ยืนยันว่ากฎการพึ่งพาถูกยึดถืออย่างสม่ำเสมอตลอดทั้งระบบ
ทำไมเรื่องทั้งหมดนี้ถึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมเรื่องทั้งหมดนี้ถึงสำคัญ”เป้าหมายสุดท้ายของ Clean Architecture ไม่ใช่ความสวยงามของไดอะแกรม แต่คือการทำให้ระบบ ทดสอบได้โดยเนื้อแท้ (intrinsically testable)
เมื่อ business rules ถูกแยกออกจากรายละเอียดภายนอกอย่างเคร่งครัดตามกฎการพึ่งพา เราก็ทดสอบ business rules ได้โดยไม่ต้องพึ่ง UI, ฐานข้อมูล, web server หรือส่วนประกอบภายนอกอื่นใดเลย
และเมื่อวัน1 framework หรือฐานข้อมูลที่ใช้อยู่ล้าสมัยหรือต้องเปลี่ยน ก็สามารถแทนที่ได้โดยกระทบส่วนอื่นของระบบน้อยที่สุด เพราะ business logic ไม่เคยผูกติดกับรายละเอียดการ implement เหล่านั้นตั้งแต่แรก
แนวคิดนี้เชื่อมโยงโดยตรงกับงานออกแบบสถาปัตยกรรมใน Domain-Driven Design เช่นกัน — วงแหวน Entities/Use Cases ในภาพของ Uncle Bob สอดคล้องกับแนวคิดของ domain model กับ application architecture ที่ประสาน use case ต่าง ๆ ในทาง DDD
ส่วน Gateway ที่คุยกับฐานข้อมูลในชั้น Interface Adapters ก็คือรูปแบบเดียวกับ Repository pattern ที่ทำให้ domain ไม่ต้องรู้จักเทคโนโลยีจัดเก็บข้อมูลจริง
และถ้าเทียบภาพวงแหวนซ้อนกันของ Clean Architecture กับภาพ hexagon ของ Hexagonal Architecture จะเห็นว่าทั้งสองพูดเรื่องเดียวกันด้วยรูปทรงต่างกัน คือแกนกลางที่ไม่รู้จักโลกภายนอก ล้อมรอบด้วยชั้นที่คอยแปลงข้อมูลเข้า-ออก
Clean Architecture ไม่ใช่สถาปัตยกรรมใหม่ที่ Uncle Bob คิดค้นขึ้นมาเอง แต่เป็นการมองเห็นรูปแบบร่วมที่ซ่อนอยู่เบื้องหลัง Hexagonal Architecture, Onion Architecture, DCI และ BCE แล้วกลั่นออกมาเป็นกฎเดียวที่เรียบง่ายที่สุดเท่าที่จะทำได้ นั่นคือ กฎการพึ่งพา — source-code dependency ต้องชี้เข้าด้านในเสมอ
เมื่อยึดกฎนี้ไว้ ไม่ว่าคุณจะมีกี่วง จะเรียกชื่อชั้นแต่ละชั้นว่าอะไร หรือจะใช้เทคนิคใดในการข้ามขอบเขต (โดยเฉพาะ Dependency Inversion Principle) สิ่งที่ได้กลับมาเสมอคือระบบที่ business rules อยู่ตรงกลาง ปลอดภัยจากความผันผวนของ UI, ฐานข้อมูล และ framework ที่อยู่รอบตัวมัน
📄 อ่านต้นฉบับภาษาอังกฤษ: The Clean Architecture — Robert C. Martin (Uncle Bob), The Clean Code Blog (สิงหาคม 2012)