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

Common Architectural Vision

ซอฟต์แวร์​ที่​ออกแบบ​ดี​ต้อง​มี​สถาปัตยกรรม​เดียว​ที่​ชัดเจน

Common Architectural Vision คือ​แนว​ปฏิบัติ​ที่​ทำให้ ทุก​คนใน​ทีม​มอง​เห็น​และ​ยึดถือ​สถาปัตยกรรม​เดียวกัน เมื่อ​พัฒนา​และ​วิวัฒน์​ระบบ ไม่ใช่​แค่​หลักการ​นามธรรม แต่​เป็น​กิจวัตร​จริง เช่น การ​ประชุม​สื่อสาร​วิสัยทัศน์ การ​เขียน​เอกสาร​ตัดสิน​ใจ และ​การ​ตรวจ code ให้​สอดคล้อง​กับ​ทิศทาง​ที่​ตกลง​กัน​ไว้

แนวคิด​นี้​มี​ราก​มา​จาก Fred Brooks ใน The Mythical Man-Month ที่​เสนอ​คำ​ว่า conceptual integrity (บูรณภาพ​เชิง​แนวคิด) — ระบบ​ที่​ดี​ต้อง​มี​ความ​สอดคล้อง​กัน​ของ​การ​ออกแบบ​ทั้ง​ระบบ ราวกับ​ออกแบบ​มา​จาก​ความคิด​เดียว หรือ​กลุ่ม​ความคิด​เล็ก ๆ ที่​เห็น​พ้อง​กัน Brooks ถึง​กับ​ยก​ตัวอย่าง​มหา​วิหาร Reims ที่​แม้​สร้าง​ต่อ​เนื่อง​กัน​หลาย​ชั่ว​อายุ​คน​แต่​ยัง​คง​เอกภาพ​ทาง​สถาปัตยกรรม​ไว้​ได้ Martin Fowler ก็​ให้​มุมมอง​ใกล้​เคียง​กัน​ว่า​สถาปัตยกรรม​ซอฟต์แวร์​ที่แท้​จริง​คือ ความ​เข้าใจ​ร่วม​กัน (shared understanding) ที่​นัก​พัฒนา​อาวุโส​ใน​ทีม​มี​ต่อ​โครงสร้าง​ของ​ระบบ มากกว่า​ตัวเอกสาร​ที่​ผลิต​ออก​มา

ซอฟต์แวร์​แทบ​ทุก​ตัว​เริ่มต้น​ด้วย​สถาปัตยกรรม​เดียว แต่​หลาย app วิวัฒน์​ไป​ตาม​กาล​เวลา บาง​ครั้ง​ผ่าน​การ re-architect บาง​ส่วน​ที่​ไม่​เคย​เสร็จ​สมบูรณ์ เมื่อ​เป็น​เช่น​นั้น ซอฟต์แวร์​จะ​กลาย​เป็น “อสุรกาย Frankenstein” ที่​บาง​ส่วน​สร้าง​บน​สถาปัตยกรรม​หนึ่ง​และ​บาง​ส่วน​บน​อีก​แบบ — ซึ่ง​เป็น​อาการ​เดียว​กับ​ที่ Brian Foote และ Joseph Yoder เรียก​ว่า Big Ball of Mud คือ​ระบบ​ที่​ไร้​โครงสร้าง​ชัดเจน เต็ม​ไป​ด้วย​การ​ซ่อมแซม​เฉพาะ​หน้า​ซ้ำ​แล้ว​ซ้ำ​เล่า​จน​สถาปัตยกรรม​เดิม​สึก​กร่อน​ไป​เรื่อย ๆ

การ​เปลี่ยน​สถาปัตยกรรม​มัก​มี​เหตุผล​ที่​ดี ทางออก​จึง​ไม่ใช่​การ​ยึด​ติด​กับ​ของ​เดิม​เสมอ​ไป แต่​สำคัญ​ที่​ต้อง​สร้าง​ซอฟต์แวร์​ให้การ​เปลี่ยน​สถาปัตยกรรม​ใน​อนาคต​ทำได้​โดย​ไม่​ต้อง​ใช้​ความ​พยายาม​มาก​เกิน​จะ​รับ​ไหว และ​ใน​อุดมคติ​ควร​ทำให้ เสร็จ ไม่ใช่​ทิ้ง​ไว้​กลางคัน

ยิ่ง app เป็น modular มาก​เท่าไร ก็​ยิ่ง​ปรับ​รับ​สถาปัตยกรรม​ใหม่​ได้​ง่าย​ขึ้น app แบบ monolith ที่​ไม่มี modularity เลย ต้อง​เปลี่ยน​สถาปัตยกรรม​แบบ “ทั้งหมด​หรือ​ไม่​ทำ​เลย” หรือ​แย่​กว่า​นั้น​คือ​เปลี่ยน​ขณะ​ยัง​รัน​อยู่​จน​กลาย​เป็น Frankenstein แต่ app ที่ modular มากกว่า​สามารถ​ปรับ​สถาปัตยกรรม​ให้​เหมาะ​กับ​แต่ละ module หรือ​อัปเกรด​ที​ละ module ได้ สถาปัตยกรรม​แบบ microservices เป็น​ตัวอย่าง​ที่ component เล็ก ๆ จำนวน​มาก​สื่อสาร​กัน​ผ่าน HTTP และ​วิวัฒน์​แยก​กัน​ได้ อย่างไร​ก็ตาม microservices ไม่ใช่​ทาง​เดียว — app ใด ๆ ที่​ยึด Separation of Concerns และ​หลักการ​ออกแบบ​อื่น ๆ ก็​อัปเดต​และ​ดูแล​ง่าย​กว่า​ระบบ​ที่​เต็ม​ไป​ด้วย spaghetti code

  1. บันทึก​วิสัยทัศน์​ให้​เป็น​ลายลักษณ์​อักษร — ใช้ Architecture Decision Record (ADR) แบบ​สั้น ๆ ตาม​แนวทาง​ที่ Michael Nygard เสนอ บันทึก​บริบท ทาง​เลือก และ​เหตุผล​ของ​การ​ตัดสิน​ใจ​แต่ละ​ครั้ง เพื่อ​ให้​คน​ที่​เข้า​ทีม​ทีหลัง​เข้าใจ​ว่า​ทำไม​ระบบ​ถึง​ถูก​สร้าง​มา​แบบ​นี้
  2. สื่อสาร​วิสัยทัศน์​กับ​ทั้งทีมอย่าง​สม่ำเสมอ — จัด​ประชุม​ทบทวน​สถาปัตยกรรม​เป็น​ระยะ ไม่ใช่​แค่​ตอน​เริ่ม project Fowler แนะนำ​ให้​มี​ตัวอย่าง code ที่​ดี (reference implementation) ใน codebase จริง เพราะ​นัก​พัฒนา​ส่วน​ใหญ่​เรียนรู้​จาก code ที่​มี​อยู่​มากกว่า​จาก​เอกสาร
  3. บังคับ​ใช้​ด้วย​เครื่องมือ​อัตโนมัติ — เขียน architecture test (fitness function) ให้ CI ตรวจ​จับ​การ​ละเมิด​กฎ​การ​พึ่งพา​ระหว่าง layer หรือ module โดย​อัตโนมัติ แทนที่​จะ​พึ่ง​การ​ตรวจ code ด้วย​ตา​เพียง​อย่าง​เดียว
  4. เมื่อ​จะ​เปลี่ยน​สถาปัตยกรรม ให้​ตัดสิน​ใจ​ร่วม​กัน​และ​ทำให้​จบ — หลีก​เลี่ยง​การ​ปล่อย​ให้​ครึ่ง​ระบบ​อยู่​บน​ของ​เก่า​และ​อีก​ครึ่ง​อยู่​บน​ของ​ใหม่​ค้าง​คา​ไว้​ตลอด​กาล การ​ย้าย​แบบ Strangler Fig ช่วย​ให้​ทยอย​แทนที่​ระบบ​เก่า​ได้ที​ละ​ส่วน​โดย​ยัง​คง​มี​เส้นชัย​ที่​ชัดเจน​ว่า​เมื่อไร​จะ​ย้าย​เสร็จ
  5. เพิ่ม​ความ​เป็น modular ให้​ระบบ — แบ่ง​ขอบเขต module/บริการ​ให้​ชัด (เช่น​ตาม bounded context) เพื่อ​ให้​ต้นทุน​ของ​การ​เปลี่ยน​สถาปัตยกรรม​ใน​อนาคต​กระจุก​อยู่​ใน module เดียว ไม่​ลาม​ไป​ทั้ง​ระบบ
  6. ระวัง Conway’s Law — โครงสร้าง​องค์กร​มัก​สะท้อน​ออก​มา​เป็น​โครงสร้าง​ของ​ระบบ หาก​ทีม​กระจัดกระจาย​กัน​โดย​ไม่มี​เจ้าของ​สถาปัตยกรรม​ร่วม วิสัยทัศน์​เดียวกัน​จะ​รักษา​ไว้​ยาก จึง​ควร​มี​คน​หรือ​กลุ่ม​เล็ก ๆ ที่​รับผิดชอบ​ทิศทาง​สถาปัตยกรรม​โดย​รวม ตาม​ที่ Conway’s Law ชี้​ให้​เห็น
flowchart TD
    A[เกิดความต้องการเปลี่ยนสถาปัตยกรรม] --> B{ทั้งทีมเห็นพ้องกับวิสัยทัศน์ใหม่หรือยัง}
    B -- ยังไม่เห็นพ้อง --> C[จัดประชุมและเขียน ADR ร่วมกัน]
    C --> B
    B -- เห็นพ้องแล้ว --> D[บันทึกวิสัยทัศน์และเกณฑ์ลง ADR]
    D --> E[ทยอยย้ายทีละ module ด้วย Strangler Fig]
    E --> F{ยังมี code สถาปัตยกรรมเก่าคงค้างอยู่ไหม}
    F -- มี --> E
    F -- ไม่มีแล้ว --> G[ระบบทั้งหมดอยู่บนสถาปัตยกรรมเดียว]

สมมติ​ทีม​ตัดสิน​ใจ​ย้าย​จาก layered monolith ไป​เป็น modular monolith ที่​แบ่ง​ตาม bounded context ขั้น​แรก​คือ​บันทึก​การ​ตัดสิน​ใจ​ไว้​ใน ADR สั้น ๆ

# ADR 007: ย้ายจาก Layered Architecture เป็น Modular Monolith
สถานะ: ยอมรับแล้ว
บริบท: layer Business Logic ปัจจุบันพึ่งพากันข้าม feature แบบสะเปะสะปะ
ทำให้แก้ไข feature หนึ่งแล้ว feature อื่นพังโดยไม่คาดคิด
การตัดสินใจ: แบ่ง codebase เป็น module ตาม bounded context แต่ละ module
ห้าม reference internal type ของ module อื่นโดยตรง ต้องคุยผ่าน public
interface หรือ event เท่านั้น
ผลที่ตามมา: ต้องรื้อ namespace เดิมทั้งหมด และเพิ่ม architecture
test ใน CI เพื่อป้องกันการละเมิดกฎนี้ในอนาคต

จาก​นั้น​เขียน fitness function เป็น architecture test เพื่อ​บังคับ​กฎ​ที่​ตกลง​กัน​ไว้​โดย​อัตโนมัติ ไม่​ให้​ใคร​แอบ reference ข้าม module จน​วิสัยทัศน์​ค่อย ๆ เพี้ยน​ไป​ที​ละ​น้อย

// Fitness function: บังคับว่า module Domain ต้องไม่พึ่งพา Infrastructure
// โดยตรง เพื่อรักษาสถาปัตยกรรมเดียวไว้ทั้งระบบ
using NetArchTest.Rules;
using Xunit;
public class ArchitectureVisionTests
{
[Fact]
public void Domain_should_not_depend_on_infrastructure()
{
var result = Types.InAssembly(typeof(Domain.Order).Assembly)
.That()
.ResideInNamespace("MyApp.Domain")
.ShouldNot()
.HaveDependencyOn("MyApp.Infrastructure")
.GetResult();
Assert.True(
result.IsSuccessful,
"พบการละเมิดวิสัยทัศน์สถาปัตยกรรม: " +
string.Join(", ", result.FailingTypeNames ?? Array.Empty<string>()));
}
[Fact]
public void Ordering_module_should_not_reference_Billing_internals()
{
var result = Types.InAssembly(typeof(Ordering.OrderService).Assembly)
.That()
.ResideInNamespace("MyApp.Ordering")
.ShouldNot()
.HaveDependencyOn("MyApp.Billing.Internal")
.GetResult();
Assert.True(result.IsSuccessful);
}
}

test สอง​ตัว​นี้​ทำ​หน้าที่​เป็น “ยาม” ที่​คอย​ตรวจสอบ​ว่า code ใหม่​ทุก​ชิ้น​ที่ merge เข้า​มายัง​คง​สอดคล้อง​กับ​วิสัยทัศน์​ที่​ทีม​ตกลง​กัน​ไว้ หาก​ใคร​เผลอ import ข้าม​ขอบเขต​ที่​ไม่​ควร CI จะ​แดง​ทันที​แทนที่​จะ​รอ​ให้ code review จับ​ได้ทีหลัง (หรือ​จับ​ไม่​ได้​เลย)

ประโยชน์

  • ลด​ความ​สับสน​ของ​นัก​พัฒนา​ใหม่ เพราะ​มี​ความ​เข้าใจ​ร่วม​กัน​เดียว​ว่า​ระบบ​ทำงาน​อย่างไร ไม่​ต้อง​เดา​ว่า module ไหน​ใช้​แพทเทิร์น​ไหน
  • ลด​ต้นทุน​ของ Feature ใน​ระยะ​ยาว เพราะ​ไม่​ต้อง​เขียน code รองรับ​สอง​สถาปัตยกรรม​พร้อม​กัน
  • ทำให้การ re-architect ใน​อนาคต​วางแผน​ได้​ง่าย​ขึ้น เพราะ​ขอบเขต module ชัดเจน​อยู่​แล้ว
  • เปิด​ทาง​ให้ automated architecture test ทำ​หน้าที่​เฝ้า​ระวัง​แทน​คน​ตลอด​เวลา

ข้อ​ควร​ระวัง

  • อย่า​ให้​วิสัยทัศน์​กลาย​เป็น​ข้อ​อ้าง​ของ Big Design Up Front — วิสัยทัศน์​ควร​มี​รายละเอียด​พอที่​จะ​สื่อสาร​ได้ ไม่ใช่ spec ที่​ตายตัว​จน​ขยับ​ไม่​ได้​เมื่อ​บริบท​เปลี่ยน
  • วิสัยทัศน์​ที่​ไม่มี​ใคร​เป็น​เจ้าของ​หรือ​ไม่มี​การ​ตรวจสอบ​ซ้ำ​เป็น​ระยะ​จะ​กลาย​เป็น​เอกสาร​ที่​ล้าสมัย​และ​ถูก​ละเลย
  • การ​เปลี่ยน​สถาปัตยกรรม​ที่​ทำ​ครึ่ง ๆ กลาง ๆ อันตราย​กว่า​ไม่​เปลี่ยน​เลย เพราะ​ทำให้​ทีม​ต้อง​เข้าใจ​สอง​ระบบ​พร้อม​กัน
  • ต้อง​ถ่วง​ดุล​กับ YAGNI — ไม่ใช่​ทุก​การ​เปลี่ยนแปลง​ต้อง​รอ “วิสัยทัศน์​ใหญ่” เสมอ​ไป บาง​ครั้ง​การ​ปรับ​เล็ก ๆ ตรง​หน้า​ตาม Boy Scout Rule ก็​เพียงพอ