Governance and Fitness Functions (Governance และ Fitness Function)
เมื่อสถาปนิกกำหนดและจัดลำดับความสำคัญของ architecture characteristics แล้ว พวกเขาจะมั่นใจได้อย่างไรว่านักพัฒนาจะเคารพลำดับความสำคัญเหล่านั้นและ implement การออกแบบได้อย่างถูกต้องและปลอดภัย ไม่ว่าจะมีแรงกดดันด้านตารางเวลาแค่ไหน ในโปรเจกต์ซอฟต์แวร์หลายที่ ความเร่งด่วนมักครอบงำ แต่สถาปนิกก็ยังต้องมีเครื่องมือและเทคนิคเพื่อสร้าง governance เชิงสถาปัตยกรรม modularity เป็นตัวอย่างที่ดีของแง่มุม architecture ที่สำคัญแต่ไม่เร่งด่วน
Governing Architecture Characteristics (การกำกับดูแล Architecture Characteristics)
Governance มาจากคำภาษากรีก kubernan (แปลว่าบังคับเลี้ยว) เป็นความรับผิดชอบสำคัญของบทบาทสถาปนิก ตามชื่อบ่งบอก ขอบเขตของมันครอบคลุมทุกแง่มุมของกระบวนการพัฒนาซอฟต์แวร์ที่สถาปนิกต้องการมีอิทธิพลต่อ ตัวอย่างเช่น การรับประกันคุณภาพซอฟต์แวร์ก็อยู่ภายใต้ architectural governance เพราะการละเลยมันอาจนำไปสู่ปัญหาคุณภาพที่ร้ายแรง
โชคดีที่สถาปนิกมีโซลูชันที่ซับซ้อนขึ้นเรื่อย ๆ สำหรับปัญหานี้ — เป็นตัวอย่างที่ดีของการเติบโตแบบค่อยเป็นค่อยไปในความสามารถของระบบนิเวศการพัฒนาซอฟต์แวร์ แรงผลักดันสู่การทำงานอัตโนมัติที่เกิดจาก Extreme Programming ก่อให้เกิด continuous integration (CI) CI นำไปสู่การทำงานอัตโนมัติเพิ่มเติมในฝ่ายปฏิบัติการ ซึ่งตอนนี้เราเรียกว่า DevOps และห่วงโซ่นี้ก็ดำเนินต่อไปจนถึง architectural governance หนังสือ Building Evolutionary Architectures (O'Reilly, 2022) โดย Neal Ford และคณะ อธิบายกลุ่มเทคนิคที่เรียกว่า fitness functions ซึ่งใช้ทำ architecture governance หลาย ๆ ด้านให้เป็นอัตโนมัติ เราจะใช้เวลาที่เหลือของบทนี้พูดถึง fitness functions
Fitness Functions
คำว่า evolutionary ในชื่อหนังสือ Building Evolutionary Architectures มาจาก evolutionary computing มากกว่าชีววิทยา หนึ่งในผู้เขียน ดร. Rebecca Parsons ใช้เวลาส่วนหนึ่งอยู่ในวงการ evolutionary computing รวมถึงทำงานกับเครื่องมืออย่าง genetic algorithm เมื่อนักพัฒนาออกแบบ genetic algorithm เพื่อให้ได้ผลลัพธ์ที่เป็นประโยชน์บางอย่าง พวกเขามักต้องการชี้นำอัลกอริทึมด้วยการให้การวัดผลเชิงภววิสัยของคุณภาพของผลลัพธ์นั้น กลไกชี้นำนี้เรียกว่า fitness function คือฟังก์ชันเป้าหมายที่ใช้ประเมินว่าผลลัพธ์เข้าใกล้เป้าหมายแค่ไหน
ตัวอย่างเช่น สมมติคุณต้องแก้ traveling salesperson problem ปัญหาชื่อดังที่ใช้เป็นพื้นฐานสำหรับ machine learning เมื่อกำหนดพนักงานขาย รายชื่อเมืองที่ต้องไปเยือน และระยะทางระหว่างเมืองเหล่านั้น เส้นทางที่เหมาะสมที่สุดคืออะไร — ที่ลดระยะทาง เวลา และค่าใช้จ่ายให้น้อยที่สุด ถ้าคุณออกแบบ genetic algorithm เพื่อแก้ปัญหานี้ คุณอาจใช้ fitness function หนึ่งเพื่อประเมินความยาวเส้นทาง และอีกอันเพื่อประเมินต้นทุนรวมที่เกี่ยวข้องกับเส้นทางนั้น หรืออีกอันอาจประเมินเวลาที่พนักงานขายต้องอยู่ห่างบ้าน
แนวปฏิบัติใน evolutionary architecture ยืมแนวคิดนี้มาสร้าง architectural fitness function คือกลไกใด ๆ ก็ตามที่ให้การประเมินความสมบูรณ์เชิงภววิสัยของ architecture characteristic หนึ่งตัวหรือมากกว่านั้นรวมกัน
Fitness function ไม่ใช่ framework ใหม่ที่สถาปนิกต้องไปดาวน์โหลด แต่มันเป็นมุมมองใหม่ต่อเครื่องมือที่มีอยู่แล้วหลายตัว สังเกตวลี กลไกใด ๆ ก็ตาม ในนิยาม — เทคนิคการตรวจสอบสำหรับ architecture characteristics นั้นมีความหลากหลายพอ ๆ กับตัว characteristics เอง fitness function ทับซ้อนกับกลไกตรวจสอบที่มีอยู่หลายตัว ขึ้นอยู่กับวิธีที่มันถูกใช้ ไม่ว่าจะเป็นใน chaos engineering หรือเป็น metric, monitor หรือ unit testing library (ดู Figure 6-2 )
Figure 6-2. The mechanisms of fitness functions
เครื่องมือหลายตัวสามารถนำมาใช้ implement fitness function ได้ ขึ้นอยู่กับ architecture characteristics ลองมาดูตัวอย่าง fitness function สองสามตัวที่ทดสอบแง่มุมต่าง ๆ ของ modularity กัน
Cyclic dependencies (การพึ่งพากันแบบวนลูป)
Modularity เป็น implicit architecture characteristic ที่สถาปนิกส่วนใหญ่ให้ความสำคัญ เพราะ modularity ที่ดูแลไม่ดีจะทำร้ายโครงสร้างของโค้ดเบส สถาปนิกจึงมักให้ความสำคัญสูงกับการรักษา modularity ที่ดี แต่บนหลายแพลตฟอร์ม มีแรงบางอย่างที่ทำงานสวนทางกับความตั้งใจดีนั้น ตัวอย่างเช่น เมื่อเขียนโค้ดในสภาพแวดล้อมพัฒนา Java หรือ .NET ยอดนิยม ทันทีที่นักพัฒนาอ้างอิงคลาสที่ยังไม่ได้ import IDE ก็จะช่วยเหลือด้วยการเด้ง dialog ถามว่าต้องการ auto-import การอ้างอิงนั้นหรือไม่ สิ่งนี้เกิดขึ้นบ่อยจนโปรแกรมเมอร์ส่วนใหญ่จะปัด dialog auto-import ทิ้งแบบไม่ทันคิด แต่การ import คลาสข้าม component แบบไร้ระเบียบสามารถสร้างหายนะให้ modularity ได้ ตัวอย่างเช่น Figure 6-3 แสดง Cyclic Dependencies ซึ่งเป็น antipattern ที่สร้างความเสียหายอย่างมากที่สถาปนิกพยายามหลีกเลี่ยง
ใน Figure 6-3 แต่ละ component อ้างอิงบางอย่างใน component อื่น ๆ การมีเครือข่ายแบบนี้ทำลาย modularity เพราะเป็นไปไม่ได้ที่จะนำ component เดียวไปใช้ซ้ำโดยไม่ลาก component อื่น ๆ ตามไปด้วย แล้วถ้า component อื่น ๆ เหล่านั้นก็ผูกติดกับ component อื่นอีกล่ะ architecture จะยิ่งเอนเอียงไปทาง antipattern Big Ball of Mud มากขึ้นเรื่อย ๆ สถาปนิกจะกำกับดูแลพฤติกรรมนี้ได้อย่างไรโดยไม่ต้องคอยจับตาดูนักพัฒนาอยู่ตลอดเวลา code review ช่วยได้ แต่มักเกิดขึ้นช้าเกินไปในวงจรการพัฒนาจนไม่มีประสิทธิผลเต็มที่ ถ้าทีมพัฒนา import โค้ดข้าม codebase แบบไร้การควบคุมนานหนึ่งสัปดาห์จนกว่าจะถึง code review พวกเขาก็สร้างความเสียหายร้ายแรงให้โค้ดเบสไปแล้ว
Figure 6-3. Cyclic dependencies between components
ทางแก้ของปัญหานี้คือการเขียน fitness function เพื่อคอยดู cycle ดังตัวอย่างใน Example 6-2
Example 6-2. Fitness function to detect component cycles
public class CycleTest {
private JDepend jdepend;
@BeforeEach
void init() {
jdepend = new JDepend();
jdepend.addDirectory("/path/to/project/persistence/classes");
jdepend.addDirectory("/path/to/project/web/classes");
jdepend.addDirectory("/path/to/project/thirdpartyjars");
}
@Test
void testAllPackages() {
Collection packages = jdepend.analyze();
assertEquals("Cycles exist", false, jdepend.containsCycles());
}
}
โค้ดนี้ใช้เครื่องมือวัดผล JDepend เพื่อตรวจสอบการพึ่งพากันระหว่าง package เครื่องมือนี้เข้าใจโครงสร้างของ Java package และทำให้เทสต์ fail ถ้าพบ cycle ใด ๆ สถาปนิกสามารถเชื่อมเทสต์นี้เข้ากับ continuous build ของโปรเจกต์แล้วเลิกกังวลเรื่องนักพัฒนาที่ import มือไวจนเผลอสร้าง cycle ได้ นี่เป็นตัวอย่างที่ดีของ fitness function ที่คอยเฝ้าดูแนวปฏิบัติที่ สำคัญแต่ไม่เร่งด่วน ในการพัฒนาซอฟต์แวร์ มันเป็นเรื่องสำคัญสำหรับสถาปนิก แต่แทบไม่ส่งผลต่อการเขียนโค้ดในแต่ละวัน
Distance from the Main Sequence fitness function (Fitness Function สำหรับ Distance from the Main Sequence)
ใน "Coupling" เราได้แนะนำ metric ที่ค่อนข้างเฉพาะทางอย่าง Distance from the Main Sequence ซึ่งสามารถตรวจสอบได้ด้วย fitness function เช่นกัน ดังแสดงใน Example 6-3
Example 6-3. Distance from the Main Sequence fitness function
@Test
void AllPackages() {
double ideal = 0.0;
double tolerance = 0.5; // project-dependent
Collection packages = jdepend.analyze();
Iterator iter = packages.iterator();
while (iter.hasNext()) {
JavaPackage p = (JavaPackage)iter.next();
assertEquals("Distance exceeded: " + p.getName(),
ideal, p.distance(), tolerance);
}
}
โค้ดนี้ใช้ JDepend เพื่อกำหนด threshold ของค่าที่ยอมรับได้ และทำให้เทสต์ fail หากคลาสใดคลาสหนึ่งอยู่นอกช่วงนั้น (เครื่องมือ ArchUnit ที่เราจะพูดถึงในหัวข้อถัดไป ก็ช่วยให้สถาปนิกสร้าง fitness function แบบเดียวกันนี้ได้) ตัวอย่างการวัดผลเชิงภววิสัยสำหรับ architecture characteristic นี้แสดงให้เห็นความสำคัญของการทำงานร่วมกันระหว่างนักพัฒนาและสถาปนิกเมื่อออกแบบและ implement fitness function เจตนาไม่ใช่ให้กลุ่มสถาปนิกขึ้นไปอยู่บนหอคอยงาช้างแล้วสร้าง fitness function เฉพาะทางที่นักพัฒนาไม่เข้าใจ แต่เป็นการ implement กฎ governance อัตโนมัติที่รับประกันคุณภาพของโค้ดเบส
Tip (เคล็ดลับ)
สถาปนิกต้องมั่นใจว่านักพัฒนาเข้าใจจุดประสงค์ของ fitness function ก่อนจะบังคับใช้กับพวกเขา
ความซับซ้อนของเครื่องมือ fitness function เพิ่มขึ้นในช่วงไม่กี่ปีที่ผ่านมา โดยเฉพาะเมื่อเครื่องมือเฉพาะทางบางตัวถือกำเนิดขึ้น เครื่องมือหนึ่งคือ ArchUnit framework การทดสอบสำหรับ Java ที่ได้แรงบันดาลใจจาก — และใช้ — บางส่วนของระบบนิเวศ JUnit ArchUnit มีกฎ governance สำเร็จรูปหลากหลาย ที่เขียนขึ้นในรูปแบบ unit test และยังช่วยให้สถาปนิกเขียนเทสต์เฉพาะเจาะจงเพื่อจัดการเรื่อง modularity ได้ ลองพิจารณา layered architecture ที่แสดงใน Figure 6-4
Figure 6-4. Layered architecture
เมื่อออกแบบ layered monolith แบบใน Figure 6-4 สถาปนิกจะกำหนดเลเยอร์ต่าง ๆ ด้วยเหตุผลที่ดี (เราอธิบายแรงจูงใจ trade-off และแง่มุมอื่น ๆ ไว้ใน Chapter 10 ) แต่นักพัฒนาบางคนอาจไม่เข้าใจความสำคัญของ pattern เหล่านี้ ในขณะที่บางคนอาจยึดทัศนคติ "ขออภัยทีหลังง่ายกว่าขออนุญาตก่อน" เพราะข้อกังวลเฉพาะหน้าบางอย่าง เช่น performance แต่การปล่อยให้พวกเขากัดกร่อนเหตุผลเบื้องหลัง architecture จะทำร้ายสุขภาพระยะยาวของ architecture นั้น
ArchUnit ช่วยให้สถาปนิกจัดการปัญหานี้ผ่าน fitness function ดังแสดงใน Example 6-4
Example 6-4. ArchUnit fitness function to govern layers
layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")
ใน Example 6-4 สถาปนิกกำหนดความสัมพันธ์ที่พึงประสงค์ระหว่างเลเยอร์ต่าง ๆ และเขียน fitness function เพื่อตรวจสอบว่าเป็นไปตามนั้น
มีเครื่องมือคล้าย ๆ กันในโลก .NET คือ NetArchTest Example 6-5 แสดงการตรวจสอบเลเยอร์ในภาษา C#
Example 6-5. NetArchTest for layer dependencies
// Classes in the presentation should not directly reference repositories
var result = Types.InCurrentDomain()
.That()
.ResideInNamespace("NetArchTest.SampleLibrary.Presentation")
.ShouldNot()
.HaveDependencyOn("NetArchTest.SampleLibrary.Data")
.GetResult()
.IsSuccessful;
การพูดถึง testability ของเราชี้ให้เห็นปัญหาของ metric หรือการวัดผลใด ๆ นั่นคือความเป็นไปได้ที่นักพัฒนาจะพยายามเล่นเกมกับระบบ เมื่อพวกเขาเรียนรู้ว่าสถาปนิกวัด compliance อย่างไร พวกเขาก็อาจเขียนโค้ดให้ผ่าน metric แทนที่จะสร้างสิ่งที่ถูกต้องจริง ๆ ตัวอย่างเช่น ปัญหาที่พบบ่อยของ testability คือเมื่อนักพัฒนาที่เอาแต่ลัดขั้นตอนเขียน unit test โดยไม่มี assertion เลย การ "แตะ" โค้ดโดยไม่ตรวจสอบว่ามันทำงานถูกต้องก็คือการโกง metric code-coverage การเขียนโค้ด governance ด้วยเครื่องมืออย่าง ArchUnit ช่วยให้สถาปนิกป้องกันพฤติกรรมนี้ได้ โดยรับประกันว่า unit test ทุกตัวมี assertion อย่างน้อยหนึ่งตัว แน่นอนว่าคนที่ตั้งใจโกงจริง ๆ ย่อมหาทางได้เสมอ แต่ fitness function แบบนี้ช่วยป้องกันความผิดพลาดที่ไม่ตั้งใจได้
อีกตัวอย่างหนึ่งของ fitness function คือ "Chaos Monkey" ของ Netflix และ Simian Army ที่มาด้วยกัน เมื่อ Netflix ตัดสินใจย้ายการดำเนินงานไปยังคลาวด์ของ Amazon สถาปนิกก็ไม่มีอำนาจควบคุมฝ่ายปฏิบัติการโดยตรงอีกต่อไป ซึ่งทำให้พวกเขากังวลว่าจะเกิดอะไรขึ้นถ้ามีข้อบกพร่องเกิดขึ้นระหว่างการทำงานจริง เพื่อแก้ปัญหานี้ พวกเขาจึงสร้างสาขาวิชาที่เรียกว่า chaos engineering ขึ้นมา Chaos Monkey ทำหน้าที่เป็น production fitness function โดยจำลองความโกลาหลทั่วไปเพื่อดูว่าระบบทนทานได้ดีแค่ไหน latency เป็นปัญหากับ AWS instance บางตัว ดังนั้น Chaos Monkey จึงจำลอง latency สูง ๆ (จริง ๆ แล้วปัญหานี้ใหญ่มากจนพวกเขาสร้าง Latency Monkey เฉพาะทางขึ้นมาในที่สุด) สิ่งนี้นำไปสู่การพัฒนาเครื่องมือเพิ่มเติม เช่น Chaos Kong ซึ่งจำลองการล่มของ Amazon datacenter ทั้งศูนย์ ช่วยให้ Netflix หลีกเลี่ยง outage แบบนั้นได้เมื่อมันเกิดขึ้นจริง
โดยเฉพาะ "Monkey" (fitness function) ตระกูล Conformity , Security และ Janitor เป็นตัวอย่างชั้นดีของแนวทาง automated governance Conformity Monkey ช่วยให้สถาปนิกของ Netflix กำหนดกฎ governance ที่ถูกบังคับใช้โดยตัว monkey เองในสภาพแวดล้อม production ตัวอย่างเช่น ถ้าพวกเขาตัดสินใจว่าทุกบริการต้องตอบสนองโดยไม่มี error สำหรับทุก request พวกเขาก็สร้างการตรวจสอบนั้นเข้าไปใน Conformity Monkey Security Monkey จะตรวจสอบทุกบริการหาข้อบกพร่องด้านความปลอดภัยที่รู้จักกันดี เช่น พอร์ตที่ไม่ควรเปิดใช้งานและข้อผิดพลาดด้าน configuration
สุดท้าย Janitor Monkey จะมองหา instance ที่ไม่มีบริการอื่นใดกำหนดเส้นทางมาหาอีกต่อไป Netflix มี evolutionary architecture ดังนั้นนักพัฒนาจึงย้ายไปใช้บริการใหม่ ๆ อยู่เสมอ ทิ้งบริการเก่าให้ทำงานต่อไปโดยไม่มีตัวไหนใช้งานร่วมด้วยแล้ว เพราะการรันบริการบนคลาวด์มีค่าใช้จ่าย Janitor Monkey จึงมองหาบริการกำพร้าและปลดระวางมันออกจาก production
Chaos engineering เปิดมุมมองใหม่ที่น่าสนใจต่อ architecture นั่นคือมันไม่ใช่คำถามว่า ถ้า บางอย่างจะพังในที่สุดหรือไม่ แต่เป็นคำถามว่า เมื่อไร การคาดการณ์ความเสียหายเหล่านั้นล่วงหน้าและทดสอบเพื่อป้องกันมันทำให้ระบบแข็งแกร่งขึ้นมาก หนังสือโดยผู้บุกเบิกจาก Netflix บางคน คือ Casey Rosenthal และ Nora Jones ชื่อ Chaos Engineering (O'Reilly, 2020) เน้นย้ำแนวทางนี้
หนังสือทรงอิทธิพลของ Atul Gawande เรื่อง The Checklist Manifesto (Metropolitan, 2009) อธิบายว่ามืออาชีพอย่างนักบินสายการบินและศัลยแพทย์ใช้ checklist กันอย่างไร (บางครั้งถึงขั้นถูกกำหนดโดยกฎหมายให้ต้องทำ) ไม่ใช่เพราะมืออาชีพเหล่านั้นไม่รู้งานของตัวเองหรือขี้ลืม แต่เพราะเมื่อมืออาชีพทำงานที่ละเอียดซับซ้อนซ้ำ ๆ กันไปเรื่อย ๆ รายละเอียดต่าง ๆ ก็หลุดรอดไปได้ง่าย checklist สั้น ๆ จึงเป็นตัวช่วยเตือนความจำที่ได้ผล
นี่คือมุมมองที่ถูกต้องต่อ fitness function — มันไม่ใช่กลไก governance ที่หนักอึ้ง แต่เป็นกลไกให้สถาปนิกแสดงออกและตรวจสอบหลักการ architecture ที่สำคัญได้โดยอัตโนมัติ นักพัฒนารู้ดีว่าไม่ควรปล่อยโค้ดที่ไม่ปลอดภัยออกไป แต่นั่นต้องแข่งกับลำดับความสำคัญอื่น ๆ อีกหลายสิบหรือหลายร้อยข้อ เครื่องมืออย่าง Security Monkey และ fitness function โดยทั่วไป ช่วยให้สถาปนิกฝัง governance check ที่สำคัญเข้าไปในโครงสร้างพื้นฐานของ architecture ได้