Architecture and Implementation (สถาปัตยกรรมกับการ Implement)

The First Law of Software Architecture (กฎข้อแรกของสถาปัตยกรรมซอฟต์แวร์) ยังเป็นคำตอบที่สถาปนิกซอฟต์แวร์มักให้กับคำถามใดๆ ที่สุดด้วย นั่นคือ “It depends” (แล้วแต่กรณี) บางทีคำตอบที่พบบ่อยเป็นอันดับสองคือ “นั่นเป็นรายละเอียดของ implementation” เมื่อสถาปัตยกรรมซอฟต์แวร์ล้มเหลวในการบรรลุเป้าหมาย คำตอบที่สองนี้มักถูกใช้เป็นข้ออ้าง

เพื่อให้สถาปัตยกรรมทำงานได้อย่างถูกต้อง implementation ของมัน—นั่นคือ source code—ต้องสอดคล้องกับ design เพื่อจัดการสามเรื่อง ได้แก่ ประเด็น operational ของสถาปัตยกรรม (เช่น fault tolerance, responsiveness, scalability และอื่นๆ) โครงสร้างภายใน และข้อจำกัดต่างๆ ส่วนนี้จะกล่าวถึงทั้งสามเรื่องตามลำดับ

Operational Concerns (ประเด็นด้าน Operational)

Operational concerns คือ architectural characteristics ที่เราให้ความสำคัญใน Part I ของหนังสือเล่มนี้ ซึ่งเป็นรากฐานของสถาปัตยกรรมซอฟต์แวร์ทุกแบบ และเป็นสิ่งที่สถาปัตยกรรมต้องรองรับเพื่อแก้ปัญหาทางธุรกิจที่กำลังเผชิญอยู่ Architectural characteristics คือสิ่งที่ขับเคลื่อนการตัดสินใจด้านสถาปัตยกรรม (ตามที่กล่าวถึงใน Chapter 21 )

แล้วการที่สถาปัตยกรรมและ implementation ไม่สอดคล้องกันในการจัดการกับ operational concerns ของระบบหมายความว่าอย่างไร? สมมติว่าคุณเป็นสถาปนิกที่กำลังทำงานกับระบบ order-entry ใหม่ที่ต้องรองรับลูกค้าพร้อมกันตั้งแต่หลักพันไปจนถึงห้าแสนคน คุณเลือก microservices architecture style โดยอ้างอิงจาก star rating ที่พบใน “Style Characteristics” ใน Chapter 18 ซึ่งเหมาะสมกับความต้องการด้าน high scalability และ elasticity ของระบบนี้ อย่างไรก็ตาม ระหว่าง implementation ทีมพัฒนาสังเกตเห็นว่า เนื่องจาก bounded context ของ service ต่างๆ ถูกกำหนดไว้แน่นหนามาก (ดู “Bounded Context” ใน Chapter 18 ) service Order Placement จึงไม่สามารถเข้าถึง inventory database ได้โดยตรง แต่ต้องเรียก service Inventory แบบ synchronous เพื่อดึงข้อมูลสต็อกปัจจุบันของสินค้าที่ลูกค้าสนใจซื้อ การเรียกแบบ synchronous นี้ไม่เพียงทำให้ทั้งสอง service coupling กันแน่นเกินไป แต่ยังทำให้ responsiveness ของระบบช้าลงอย่างมากด้วย

ทีมพัฒนาจึงตัดสินใจใช้ in-memory replicated cache ระหว่าง service เหล่านี้ ดังแสดงใน Figure 26-1 โดยข้อมูลจะอยู่ใน internal memory ของแต่ละ service instance และถูก sync ให้ตรงกันอยู่เสมอผ่านการ cache ที่ทำงานอยู่เบื้องหลัง ผลิตภัณฑ์ caching สำหรับจุดประสงค์นี้ได้แก่ Apache Ignite และ Hazelcast ในกรณีนี้ service Inventory จะมี writable in-memory cache ที่เก็บ item ID และจำนวนสต็อกปัจจุบัน ส่วนแต่ละ instance ของ service Order Placement จะมี cache แบบ replicated ที่ read-only ของ cache นั้นอยู่ใน internal memory ของตัวเอง การใช้ in-memory replicated cache ช่วย decouple service เหล่านี้ออกจากกัน และปรับปรุง responsiveness ได้ อย่างมาก

In-memory replicated caching

Figure 26-1. ทีมพัฒนาตัดสินใจใช้ in-memory replicated caching ระหว่าง service ต่างๆ ซึ่งส่งผลให้เกิดสภาวะ out-of-memory เมื่อ scale service เหล่านั้น

หลัง production release เมื่อจำนวนผู้ใช้พร้อมกันเพิ่มขึ้น ก็จำเป็นต้องเพิ่ม instance ของแต่ละ service มากขึ้นเพื่อรองรับโหลด ระบบล่มเมื่อโหลดถึงประมาณ 80,000 ผู้ใช้พร้อมกัน เนื่องจากความต้องการหน่วยความจำของ internal cache สูงเกินไป จนทำให้เกิดสภาวะ out-of-memory ใน virtual machine ทั้งหมด

ในกรณีนี้ สถาปัตยกรรมและ implementation ของมันไม่สอดคล้องกัน ในขณะที่สถาปัตยกรรมมุ่งเน้นการรองรับ scalability และ elasticity ในระดับสูง implementation กลับมุ่งเน้นไปที่ responsiveness และ service decoupling ทั้งสองทีมต่างตัดสินใจได้ดีทั้งคู่ แต่เพื่อเป้าหมายที่ต่างกัน

Structural Integrity (ความสมบูรณ์เชิงโครงสร้าง)

คุณได้เรียนรู้ใน Chapter 8 ว่า logical component คือ building block ของระบบทุกระบบ และก่อตัวเป็น logical architecture ของระบบนั้น โดยทั่วไป component เหล่านี้จะถูกแทนด้วยโครงสร้าง directory ใน source-code repository (หรือ namespace ขึ้นอยู่กับภาษาโปรแกรม) เนื่องจาก logical architecture อธิบายว่าระบบทำงานอย่างไรและส่วนใดของระบบมีปฏิสัมพันธ์กับส่วนอื่น จึงสำคัญมากที่โครงสร้างของ source code ต้องตรงกับโครงสร้างของ logical architecture

หากขาดการชี้แนะ ความรู้ และ governance ที่เหมาะสม developer ก็มักจะละเลย logical architecture ของระบบ แล้วเริ่มสร้างโครงสร้าง directory และ namespace ตามใจชอบ โดยไม่คำนึงถึงผลกระทบต่อความสมบูรณ์ของระบบ ความไม่สอดคล้องนี้ทำให้เกิดสถาปัตยกรรมที่ดูแลรักษา ทดสอบ และ deploy ได้ยาก และส่งผลให้ความน่าเชื่อถือลดลงและยากต่อการวิวัฒนาการหรือปรับตัวรับ feature ใหม่ๆ ดังเช่น logical architecture ที่แสดงใน Figure 26-2

Bad logical architecture

Figure 26-2. ตัวอย่างของ internal logical architecture ที่ขาด governance และความสอดคล้อง

เพื่อให้แน่ใจว่าโครงสร้างของ source code ตรงกับ logical architecture เราแนะนำให้ใช้เครื่องมือ automated governance เช่น ArchUnit สำหรับแพลตฟอร์ม Java, ArchUnitNet และ NetArchTest สำหรับแพลตฟอร์ม .NET, PyTestArch สำหรับ Python หรือ TSArch สำหรับ TypeScript และ JavaScript เครื่องมือ automated เหล่านี้ ประกอบกับการสื่อสารและความร่วมมือที่ดีระหว่างสถาปนิกกับทีมพัฒนา จะสร้าง implementation ที่สอดคล้องกับสถาปัตยกรรมอย่างเหมาะสม ดังแสดงใน Figure 26-3

Good logical architecture

Figure 26-3. ตัวอย่างของ internal logical architecture ที่อิงจาก governance และความสอดคล้องที่เหมาะสม

ลองเปรียบเทียบ Figure 26-2 กับ Figure 26-3 จะสังเกตได้ว่าสถาปัตยกรรมใน Figure 26-3 นั้นดูแลรักษา ทดสอบ deploy เชื่อถือได้ ปรับตัว และขยายได้ดีกว่าสถาปัตยกรรมที่ไม่สอดคล้องกันซึ่งแสดงใน Figure 26-2 มาก การเปรียบเทียบนี้แสดงให้เห็นความสำคัญของความสอดคล้องด้าน implementation ประเภทนี้

Architectural Constraints (ข้อจำกัดทางสถาปัตยกรรม)

constraint คือกฎหรือหลักการที่ควบคุมและอธิบายข้อจำกัดบางอย่างภายในสถาปัตยกรรม (เช่น การจำกัดการสื่อสารให้ใช้เฉพาะ REST หรือการใช้ database ประเภทใดประเภทหนึ่ง) ที่จำเป็นเพื่อให้บรรลุเป้าหมายของสถาปัตยกรรม หาก implementation ของระบบไม่ปฏิบัติตาม constraint เหล่านี้ สถาปัตยกรรมก็จะล้มเหลว ดังนั้น ส่วนหนึ่งของงานสถาปนิกซอฟต์แวร์คือการระบุและสื่อสาร constraint ของสถาปัตยกรรม

เพื่อยกตัวอย่างจุดตัดนี้ ลองพิจารณาธุรกิจที่พยายามจะ release ระบบใหม่ภายในงบประมาณที่จำกัดมากและกำหนดเวลาที่กระชั้น ธุรกิจนี้คาดว่าจะมีการเปลี่ยนแปลงโครงสร้าง database จำนวนมาก และต้องทำการเปลี่ยนแปลงเหล่านั้นให้เร็วที่สุดเท่าที่จะทำได้ ในกรณีนี้ traditional layered architecture (ดู Chapter 10 ) จะเหมาะสมอย่างยิ่งด้วยความเรียบง่าย ความคุ้มค่า และการแบ่งส่วนตาม technical partitioning เนื่องจาก style นี้แยก layer ออกจากกัน การเปลี่ยนแปลง database จึงถูกจำกัดอยู่ใน layer เดียว ทำให้เปลี่ยนแปลงได้ง่ายและเร็วขึ้น

เพื่อให้ layered architecture ทำงานได้กับปัญหาทางธุรกิจนี้โดยเฉพาะ สถาปนิกจะต้องกำหนด constraint ต่อไปนี้

  • logic ทั้งหมดเกี่ยวกับ database ต้องอยู่ใน Persistence layer

  • Presentation layer ไม่สามารถเข้าถึง Persistence layer ได้โดยตรง แต่ต้องผ่านทุก layer แม้แต่สำหรับ query ง่ายๆ

constraint เหล่านี้จำเป็นเพื่อป้องกันไม่ให้ database logic กระจายไปทั่วทั้งสถาปัตยกรรม และเพื่อให้การเปลี่ยนแปลงโครงสร้าง database จริง (เช่น การลบ table หรือเปลี่ยนชื่อ column) ไม่ส่งผลกระทบต่อ code ใดๆ นอก Persistence layer

ทีนี้สมมติว่า UI developer ตัดสินใจว่าการเรียก database โดยตรงนั้นเร็วกว่า และเลือก implement สถาปัตยกรรมแบบนั้นแทน นอกจากนี้ backend developer ก็ตระหนักว่าการดูแลรักษาและทดสอบ code จะง่ายขึ้นมาก หากรวม Business logic กับ database logic ไว้ด้วยกัน จึงเลือกละเลย constraint เช่นกัน และรวมสิ่งเหล่านี้ไว้ใน business layer ของสถาปัตยกรรม implementation นี้ไม่สอดคล้องกับ constraint ของสถาปัตยกรรม ซึ่งหมายความว่าการเปลี่ยนแปลง database จะส่งผลกระทบต่อ code ในทุก layer ใช้เวลานานเกินไป และระบบจะไม่บรรลุเป้าหมายทางธุรกิจ

เครื่องมือทางสถาปัตยกรรมยังมีประโยชน์สำหรับการควบคุม constraint ทางสถาปัตยกรรมด้วย