แอปพลิเคชันที่มีอายุการใช้งานยาวนานมักจะขยายตัวอย่างไม่มีทิศทาง ตอนแรกมันอาจเริ่มต้นด้วยสถาปัตยกรรมที่ออกแบบมาอย่างดี แต่เมื่อเวลาผ่านไป ภายใต้แรงกดดันด้านกำหนดส่งงาน มันอาจถึงจุดที่ไม่มีใครเข้าใจโครงสร้างโดยรวมอีกต่อไป ผู้คนสามารถทำงานในโปรเจกต์เป็นปีๆ โดยไม่รู้เลยว่าฟีเจอร์ใหม่ๆ ควรจะไปอยู่ที่ไหน พวกเขาแค่รู้จักจุดที่ใช้แฮก (hack) ในระบบช่วงหลังๆ พอเวลาจะเพิ่มฟีเจอร์ใหม่ พวกเขาก็ไปที่ "จุดแฮก" เพราะนั่นคือพื้นที่ที่พวกเขาคุ้นเคยที่สุด
ไม่มีวิธีแก้ไขเรื่องนี้ที่ง่ายดายนัก และความเร่งด่วนของสถานการณ์ก็แตกต่างกันไปอย่างมาก ในบางกรณี โปรแกรมเมอร์เจอทางตัน มันยากที่จะเพิ่มฟีเจอร์ใหม่ และนั่นทำให้ทั้งองค์กรเข้าสู่โหมดวิกฤต ผู้คนถูกมอบหมายภารกิจให้หาคำตอบว่าควรจะปรับสถาปัตยกรรมใหม่ (rearchitect) หรือเขียนระบบใหม่ (rewrite) ดี ในขณะที่บางองค์กร ระบบก็ยังคงทำงานต่อไปอย่างกระย่องกระแย่งเป็นปีๆ ใช่แล้ว มันใช้เวลานานกว่าที่ควรในการเพิ่มฟีเจอร์ใหม่ แต่นั่นกลับถูกมองว่าเป็นราคาที่ต้องจ่ายเพื่อทำธุรกิจ ไม่มีใครรู้ว่ามันจะดีกว่านี้ได้แค่ไหน หรือเงินจำนวนเท่าไหร่ที่สูญเสียไปเพราะโครงสร้างที่ไม่ดี
เมื่อทีมไม่ตระหนักถึงสถาปัตยกรรมของตนเอง มันก็มีแนวโน้มที่จะเสื่อมลง อะไรบ้างที่เป็นอุปสรรคต่อการตระหนักรู้นี้
• ระบบอาจซับซ้อนมากจนต้องใช้เวลานานในการเห็นภาพรวม
• ระบบอาจซับซ้อนมากจนไม่มีภาพรวมเลย
• ทีมอยู่ในโหมดตั้งรับ (reactive mode) ต้องจัดการกับเหตุฉุกเฉินซ้ำแล้วซ้ำเล่าจนมองไม่เห็นภาพรวม
ตามธรรมเนียมแล้ว หลายองค์กรใช้บทบาทของสถาปนิก (architect) เพื่อแก้ปัญหาเหล่านี้ โดยปกติสถาปนิกจะถูกมอบหมายให้ดูแลภาพรวมและตัดสินใจเพื่อรักษาภาพรวมไว้ให้กับทีม วิธีนั้นใช้ได้ผล แต่มีข้อควรระวังที่สำคัญอย่างหนึ่ง สถาปนิกต้องลงมาอยู่ในทีม ทำงานร่วมกับสมาชิกในทีมวันต่อวัน มิฉะนั้นโค้ดจะเบี่ยงเบนไปจากภาพรวม มีสองทางที่สิ่งนี้จะเกิดขึ้นได้: อาจมีคนกำลังทำสิ่งที่ไม่เหมาะสมในโค้ด หรือตัวภาพรวมเองอาจต้องได้รับการปรับเปลี่ยน ในสถานการณ์ที่แย่ที่สุดบางกรณีที่ผมเจอกับทีม สถาปนิกของกลุ่มมีมุมมองต่อระบบที่แตกต่างจากโปรแกรมเมอร์อย่างสิ้นเชิง บ่อยครั้งที่สิ่งนี้เกิดขึ้นเพราะสถาปนิกมีความรับผิดชอบอื่นๆ และไม่สามารถลงไปเขียนโค้ดหรือสื่อสารกับทีมที่เหลือได้บ่อยพอที่จะรู้ว่ามีอะไรอยู่ในระบบจริงๆ ผลลัพธ์คือการสื่อสารล้มเหลวทั่วทั้งองค์กร
ความจริงที่โหดร้ายคือ สถาปัตยกรรมมีความสำคัญเกินกว่าจะปล่อยให้คนไม่กี่คนดูแลแต่เพียงผู้เดียว การมีสถาปนิกนั้นดี แต่หนทางสำคัญที่จะรักษาสถาปัตยกรรมให้คงอยู่คือการทำให้แน่ใจว่าทุกคนในทีมรู้ว่ามันคืออะไรและมีส่วนร่วมในมัน ทุกคนที่แตะโค้ดควรรู้จักสถาปัตยกรรม และทุกคนที่แตะโค้ดควรได้รับประโยชน์จากสิ่งที่คนนั้นได้เรียนรู้ เมื่อทุกคนทำงานจากชุดความคิดเดียวกัน ความฉลาดของระบบโดยรวมของทีมก็จะถูกขยายออกไป ยกตัวอย่างเช่น ถ้าคุณมีทีม 20 คน และมีแค่ 3 คนที่รู้รายละเอียดสถาปัตยกรรม ไม่ว่าจะเป็นคน 3 คนนั้นต้องทำงานหนักเพื่อคอยนำทางคนอื่นๆ อีก 17 คน หรือคน 17 คนนั้นก็แค่ทำพลาดเพราะไม่คุ้นเคยกับภาพรวม
เราจะได้ภาพรวมของระบบขนาดใหญ่ได้อย่างไร? มีหลายวิธีในการทำสิ่งนี้ หนังสือ Object-Oriented Reengineering Patterns โดย Serge Demeyer, Stephane Ducasse, และ Oscar M. Nierstrasz (Morgan Kaufmann Publishers, 2002) มีแคตตาล็อกของเทคนิคที่เกี่ยวข้องกับปัญหานี้โดยเฉพาะ ที่นี่ผมจะอธิบายเทคนิคอื่นๆ อีกหลายอย่างที่ค่อนข้างทรงพลัง ถ้าคุณฝึกฝนเทคนิคเหล่านี้บ่อยๆ ในทีม มันจะช่วยให้ประเด็นด้านสถาปัตยกรรมยังคงมีชีวิตอยู่ในทีม—และนั่นอาจเป็นสิ่งที่สำคัญที่สุดที่คุณสามารถทำได้เพื่อรักษาสถาปัตยกรรม มันยากที่จะใส่ใจกับสิ่งที่คุณไม่ได้คิดถึงบ่อยๆ
Telling the Story of the System (การเล่าเรื่องราวของระบบ)
เวลาผมทำงานกับทีม ผมมักใช้เทคนิคที่เรียกว่า "การเล่าเรื่องราวของระบบ" (telling the story of the system) เพื่อทำมันให้ดี คุณต้องมีอย่างน้อยสองคน คนหนึ่งเริ่มต้นด้วยการถามอีกคนว่า "สถาปัตยกรรมของระบบคืออะไร?" แล้วอีกคนพยายามอธิบายสถาปัตยกรรมของระบบโดยใช้เพียงไม่กี่แนวคิด อาจจะแค่สองหรือสามแนวคิด ถ้าคุณเป็นคนอธิบาย คุณต้องทำเหมือนว่าอีกคนไม่รู้อะไรเกี่ยวกับระบบเลย คุณต้องอธิบายในไม่กี่ประโยคว่าชิ้นส่วนของการออกแบบคืออะไรและพวกมันโต้ตอบกันอย่างไร หลังจากที่คุณพูดสองสามประโยคนั้น คุณก็ได้สื่อถึงสิ่งที่คุณคิดว่าสำคัญที่สุดเกี่ยวกับระบบแล้ว จากนั้น คุณเลือกสิ่งที่สำคัญรองลงมาเกี่ยวกับระบบที่จะพูดต่อ คุณทำแบบนี้ไปเรื่อยๆ จนกว่าคุณจะได้พูดทุกอย่างที่สำคัญเกี่ยวกับแกนกลางของการออกแบบระบบ
เมื่อคุณเริ่มทำสิ่งนี้ คุณจะสังเกตเห็นความรู้สึกแปลกๆ การจะสื่อถึงสถาปัตยกรรมของระบบให้สั้นขนาดนั้น คุณต้องทำให้มันง่ายขึ้น คุณอาจจะพูดว่า "gateway ดึง rule sets จาก active database" แต่พอคุณพูดอย่างนั้น ส่วนหนึ่งในตัวคุณอาจจะกรีดร้องว่า "ไม่! gateway ดึง rule sets จาก active database แต่มันก็ดึงจาก current working set ด้วยเหมือนกัน" เวลาคุณพูดสิ่งที่ง่ายกว่า มันให้ความรู้สึกเหมือนคุณกำลังโกหก คุณแค่ไม่ได้เล่าเรื่องทั้งหมด แต่คุณกำลังเล่าเรื่องที่ง่ายกว่าซึ่งอธิบายสถาปัตยกรรมที่เข้าใจได้ง่ายกว่า ตัวอย่างเช่น ทำไม gateway ต้องดึง rule sets จากมากกว่าหนึ่งที่? มันไม่ง่ายกว่าหรือถ้ารวมเป็นที่เดียวกัน?
ข้อพิจารณาเชิงปฏิบัติมักขวางไม่ให้สิ่งต่างๆ กลายเป็นเรื่องง่าย แต่การพูดถึงมุมมองแบบง่ายๆ ก็มีคุณค่า อย่างน้อยที่สุด มันช่วยให้ทุกคนเข้าใจว่าอะไรคือสิ่งที่ดีที่สุดในอุดมคติ และอะไรที่อยู่ตรงนั้นเพราะความจำเป็นเร่งด่วน อีกสิ่งสำคัญเกี่ยวกับเทคนิคนี้คือมันบังคับให้คุณคิดจริงๆ ว่าอะไรคือสิ่งสำคัญในระบบ อะไรคือสิ่งที่สำคัญที่สุดที่จะต้องสื่อสาร?
ทีมจะไปได้ไกลแค่ไหนก็จำกัดเมื่อระบบที่พวกเขาทำงานด้วยเป็นปริศนาสำหรับพวกเขา ในแง่หนึ่ง การมีเรื่องเล่าง่ายๆ ว่าระบบทำงานอย่างไรก็ทำหน้าที่เป็นแผนที่นำทาง เป็นวิธีปรับทิศทางขณะที่คุณค้นหาจุดที่เหมาะสมในการเพิ่มฟีเจอร์ และมันยังช่วยให้ระบบดูน่ากลัวน้อยลงอีกด้วย
ในทีมของคุณ จงเล่าเรื่องราวของระบบบ่อยๆ เพื่อให้ทุกคนมีมุมมองร่วมกัน เล่าในรูปแบบที่แตกต่างกัน ถกเถียงกันว่าแนวคิดไหนสำคัญกว่าแนวคิดไหน เมื่อคุณพิจารณาการเปลี่ยนแปลงต่างๆ ในระบบ คุณจะสังเกตว่าบางการเปลี่ยนแปลงสอดคล้องกับเรื่องเล่ามากกว่า นั่นคือ มันทำให้เรื่องเล่าสั้นๆ รู้สึกเหมือนโกหกน้อยลง ถ้าคุณต้องเลือกระหว่างสองวิธีในการทำบางสิ่ง เรื่องเล่าสามารถเป็นวิธีที่ดีในการดูว่าวิธีไหนจะนำไปสู่ระบบที่เข้าใจได้ง่ายกว่า
นี่คือตัวอย่างของการเล่าเรื่องแบบนี้ในทางปฏิบัติ เป็นเซสชันที่พูดถึง JUnit มันสมมติว่าคุณรู้เกี่ยวกับสถาปัตยกรรมของ JUnit มาบ้าง ถ้าคุณไม่รู้ ให้ใช้เวลาสักครู่เพื่อดูซอร์สโค้ดของ JUnit คุณสามารถดาวน์โหลดได้จาก www.junit.org .
สถาปัตยกรรมของ JUnit คืออะไร?
JUnit มีคลาสหลักสองคลาส คลาสแรกชื่อ Test, และอีกคลาสชื่อ TestResult. ผู้ใช้สร้างเทสแล้วรันมัน โดยส่ง TestResult เข้าไป เมื่อเทสล้มเหลว มันจะบอก TestResult เกี่ยวกับข้อผิดพลาดนั้น จากนั้นผู้ใช้ก็สามารถถาม TestResult สำหรับข้อผิดพลาดทั้งหมดที่เกิดขึ้น
มาลองรายการสิ่งที่ถูกทำให้ง่ายขึ้นกัน:
1. ใน JUnit ยังมีคลาสอื่นๆ อีกมากมาย ที่ผมบอกว่า Test และ TestResult เป็นคลาสหลักก็เพราะผมคิดอย่างนั้นเท่านั้น สำหรับผม ปฏิสัมพันธ์ระหว่างพวกมันคือปฏิสัมพันธ์หลักในระบบ คนอื่นอาจมีมุมมองต่อสถาปัตยกรรมที่แตกต่างแต่ก็ถูกต้องเท่าเทียมกัน
2. ผู้ใช้ไม่ได้สร้าง test objects โดยตรง Test objects ถูกสร้างจาก test case classes ผ่านการทำ reflection
3. Test ไม่ใช่คลาส มันคืออินเทอร์เฟซ เทสที่รันใน JUnit มักจะถูกเขียนในซับคลาสของคลาสที่ชื่อ TestCase, ซึ่ง implement Test.
4. โดยทั่วไปแล้วผู้ใช้ไม่ได้ถาม TestResults สำหรับข้อผิดพลาด TestResults จะลงทะเบียน listeners ซึ่งจะถูกแจ้งเตือนเมื่อใดก็ตามที่ TestResult ได้รับข้อมูลจากเทส
5. เทสรายงานมากกว่าแค่ failures: พวกมันรายงานจำนวนเทสที่รันและจำนวน errors (Errors คือปัญหาที่เกิดขึ้นในเทสซึ่งไม่ได้ถูกตรวจสอบอย่างชัดเจน Failures คือการตรวจสอบที่ล้มเหลว)
การทำให้ง่ายขึ้นเหล่านี้ให้ความเข้าใจอะไรไหมว่า JUnit จะทำให้ง่ายขึ้นได้อย่างไร? เล็กน้อย เฟรมเวิร์ก xUnit testing ที่ง่ายกว่าบางตัวทำให้ Test เป็นคลาสและทิ้ง TestCase ไปทั้งหมด ส่วนเฟรมเวิร์กอื่นๆ รวม errors และ failures เข้าด้วยกันเพื่อให้รายงานในลักษณะเดียวกัน
กลับมาเรื่องของเรากันต่อ
แค่นั้นเหรอ?
ไม่ เทสสามารถถูกจัดกลุ่มเป็นออบเจกต์ที่เรียกว่า suites เราสามารถรัน suite ด้วย test result เหมือนกับเทสเดี่ยว เทสทั้งหมดภายในมันจะทำงานและบอก test result เมื่อพวกมันล้มเหลว
เรามีการทำให้ง่ายขึ้นอะไรบ้างตรงนี้?
1. TestSuites ทำมากกว่าแค่เก็บและรันชุดของเทส พวกมันยังสร้างอินสแตนซ์ของคลาสที่สืบทอดจาก TestCase ผ่านการทำ reflection
2. มีการทำให้ง่ายขึ้นอีกข้อ เป็นผลพวงจากข้อแรก เทสไม่ได้รันตัวเองจริงๆ พวกมันส่งตัวเองไปยังคลาส TestResult ซึ่งจะเรียกเมธอดการรันเทสกลับไปที่เทสอีกที การส่งไปกลับนี้เกิดขึ้นในระดับที่ค่อนข้างต่ำ การคิดถึงมันแบบง่ายๆ ก็สะดวกดี มันเป็นการโกหกเล็กน้อย แต่มันก็เป็นวิธีที่ JUnit เคยเป็นตอนที่มันยังง่ายกว่านี้
แค่นั้นเหรอ?
ไม่ จริงๆ แล้ว Test เป็นอินเทอร์เฟซ มีคลาสที่ชื่อ TestCase ที่ implement Test. ผู้ใช้สร้างซับคลาสของ TestCase แล้วเขียนเทสเป็นเมธอด public void ที่ขึ้นต้นด้วยคำว่า test ในซับคลาสของพวกเขา คลาส TestSuite ใช้ reflection เพื่อสร้างกลุ่มของเทสที่สามารถรันได้ในการเรียกใช้เมธอด TestSuite's run เพียงครั้งเดียว
เราสามารถไปต่อได้อีก แต่สิ่งที่ผมแสดงให้เห็นถึงตอนนี้ก็ให้ความรู้สึกถึงเทคนิคแล้ว เราเริ่มต้นด้วยการทำคำอธิบายสั้นๆ เมื่อเราทำให้ง่ายและดึงรายละเอียดออกไปเพื่ออธิบายระบบ เรากำลังทำ abstraction จริงๆ บ่อยครั้งเมื่อเราบังคับตัวเองให้สื่อสารมุมมองที่ง่ายมากของระบบ เราสามารถค้นพบ abstractions ใหม่ๆ ได้
ถ้าระบบไม่ได้เรียบง่ายเท่าเรื่องเล่าที่เรียบง่ายที่สุดที่เราเล่าเกี่ยวกับมัน นั่นหมายความว่ามันแย่หรือเปล่า? ไม่ใช่ โดยธรรมชาติแล้ว เมื่อระบบเติบโตขึ้น มันก็จะซับซ้อนมากขึ้น เรื่องเล่าช่วยชี้ทางให้เรา
สมมติว่าเราจะเพิ่มฟีเจอร์ใหม่ให้ JUnit เราต้องการสร้างรายงานของเทสทั้งหมดที่ไม่เรียก assertions ใดๆ เมื่อเรารันมัน เรามีตัวเลือกอะไรบ้างจากสิ่งที่อธิบายไว้ใน JUnit?
ทางเลือกหนึ่งคือการเพิ่มเมธอดในคลาส TestCase ชื่อ buildUsageReport ที่รันแต่ละเมธอดแล้วสร้างรายงานของเมธอดทั้งหมดที่ไม่เรียกเมธอด assert นั่นจะเป็นวิธีที่ดีในการเพิ่มฟีเจอร์นี้หรือเปล่า? มันจะทำอะไรกับเรื่องเล่าของเรา? มันจะเพิ่ม "การโกหกโดยการละเว้น" อีกเล็กน้อยจากคำอธิบายระบบที่สั้นที่สุดของเรา:
JUnit มีคลาสหลักสองคลาส คลาสแรกชื่อ Test, และอีกคลาสชื่อ TestResult. ผู้ใช้สร้างเทสแล้วรันมัน โดยส่ง TestResult เข้าไป เมื่อเทสล้มเหลว มันจะบอก TestResult เกี่ยวกับข้อผิดพลาดนั้น จากนั้นผู้ใช้ก็สามารถถาม TestResult สำหรับข้อผิดพลาดทั้งหมดที่เกิดขึ้น
ดูเหมือนว่า Tests จะมีความรับผิดชอบที่แตกต่างไปโดยสิ้นเชิง นั่นคือการสร้างรายงาน ซึ่งเราไม่เคยพูดถึงเลย
แล้วถ้าเราเพิ่มฟีเจอร์ด้วยวิธีที่แตกต่างออกไปล่ะ? เราสามารถเปลี่ยนแปลงปฏิสัมพันธ์ระหว่าง TestCase และ TestResult เพื่อให้ TestResult ได้รับจำนวน assertions ที่ถูกรันเมื่อใดก็ตามที่เทสทำงาน จากนั้นเราสามารถสร้างคลาสสำหรับสร้างรายงานและลงทะเบียนมันกับ TestResult เป็น listener แบบนี้ส่งผลต่อเรื่องเล่าของระบบอย่างไร? มันอาจเป็นเหตุผลที่ดีในการทำให้เรื่องเล่ากว้างขึ้นอีกหน่อย Tests ไม่ได้แค่บอก TestResults เกี่ยวกับจำนวน failures เท่านั้น พวกมันยังบอกเกี่ยวกับจำนวน errors, จำนวนเทสที่รัน, และจำนวน assertions ที่รันด้วย เราสามารถเปลี่ยนเรื่องเล่าสั้นๆ ของเราเป็นแบบนี้:
JUnit มีคลาสหลักสองคลาส คลาสแรกชื่อ Test, และอีกคลาสชื่อ TestResult. ผู้ใช้สร้างเทสแล้วรันมัน โดยส่ง TestResult เข้าไป เมื่อเทสทำงาน มันจะส่งข้อมูลเกี่ยวกับการรันเทสไปยัง TestResult. จากนั้นผู้ใช้ก็สามารถถาม TestResult สำหรับ ข้อมูล เกี่ยวกับการรันเทสทั้งหมด
แบบนั้นดีกว่าไหม? พูดตามตรง ผมชอบเวอร์ชันเดิมที่เป็นการบันทึก failures สำหรับผม มันคือหนึ่งในพฤติกรรมหลักของ JUnit ถ้าเราเปลี่ยนโค้ดเพื่อให้ TestResults บันทึกจำนวน assertions ที่รัน เราก็ยังโกหกอยู่เล็กน้อย แต่เราก็มองข้ามข้อมูลอื่นๆ ที่เราส่งจากเทสไปยัง test results อยู่แล้ว ทางเลือกอื่นคือการใส่ความรับผิดชอบในการรันเคสหลายๆ เคสและสร้างรายงานจากพวกมันไว้ที่ TestCase, ซึ่งจะเป็นการโกหกที่ใหญ่กว่า: เราไม่ได้พูดถึงความรับผิดชอบเพิ่มเติมของ TestCase นี้เลย เราจะดีกว่าถ้าให้เทสรายงานจำนวน assertions ที่รันขณะที่มันทำงาน เรื่องเล่าแรกของเราถูกทำให้กว้างขึ้นอีกนิด แต่อย่างน้อยมันก็ยังเป็นความจริงเป็นส่วนใหญ่ นั่นหมายความว่าการเปลี่ยนแปลงของเราสอดคล้องกับสถาปัตยกรรมของระบบมากขึ้น
Naked CRC (CRC แบบไม่เขียนการ์ด)
ในยุคแรกๆ ของ object orientation หลายคนประสบปัญหากับเรื่องการออกแบบ มันยากที่จะชินกับ object orientation เมื่อประสบการณ์การเขียนโปรแกรมส่วนใหญ่ของคุณมาจากภาษา procedural พูดง่ายๆ คือ วิธีที่คุณคิดเกี่ยวกับโค้ดของคุณนั้นแตกต่างออกไป ผมจำได้ว่าครั้งแรกที่มีคนพยายามให้ผมดูการออกแบบเชิงวัตถุบนกระดาษ ผมมองดูรูปร่างและเส้นต่างๆ และฟังคำอธิบาย แต่คำถามที่ผมพยายามถามอยู่เรื่อยคือ "แล้ว main() อยู่ไหน? จุดเริ่มต้นสำหรับสิ่งใหม่อย่าง object พวกนี้อยู่ที่ไหน?" ผมงงอยู่พักหนึ่ง แต่แล้วมันก็เริ่มปะติดปะต่อ ปัญหาไม่ได้มีแค่กับผมนะ ดูเหมือนว่าคนส่วนใหญ่ในอุตสาหกรรมก็ประสบปัญหาเดียวกันในช่วงเวลาเดียวกัน พูดตรงๆ ทุกวันนี้คนใหม่ๆ ในวงการก็ยังเจอปัญหาเหล่านี้เมื่อพวกเขาเจอโค้ดเชิงวัตถุเป็นครั้งแรก
ในช่วงทศวรรษ 1980 Ward Cunningham และ Kent Beck กำลังจัดการกับปัญหานี้ พวกเขาพยายามช่วยให้ผู้คนเริ่มคิดเกี่ยวกับการออกแบบในแง่ของ objects ในเวลานั้น Ward กำลังใช้เครื่องมือชื่อ Hypercard ซึ่งช่วยให้คุณสร้างการ์ดบนหน้าจอคอมพิวเตอร์และสร้างลิงก์ระหว่างพวกมันได้ ทันใดนั้น ความเข้าใจก็ปรากฏขึ้น ทำไมไม่ใช้บัตรดัชนีจริงๆ เพื่อแทนคลาสล่ะ? มันจะทำให้จับต้องได้และพูดคุยกันได้ง่าย ควรพูดถึงคลาส Transaction ไหม? ได้เลย นี่คือการ์ดของมัน—บนนั้นมีความรับผิดชอบ (responsibilities) และผู้ร่วมงาน (collaborators) ของมัน
CRC ย่อมาจาก Class, Responsibility, และ Collaborations คุณเขียนการ์ดแต่ละใบด้วยชื่อคลาส ความรับผิดชอบของมัน และรายชื่อ collaborators (คลาสอื่นๆ ที่คลาสนี้สื่อสารด้วย) ถ้าคุณคิดว่าความรับผิดชอบบางอย่างไม่ควรอยู่บนคลาสนั้น ให้ขีดฆ่ามันแล้วเขียนลงบนการ์ดคลาสอื่น หรือสร้างการ์ดคลาสใหม่ขึ้นมาเลย
ถึงแม้ว่า CRC จะได้รับความนิยมในช่วงหนึ่ง แต่ในที่สุดก็เกิดการผลักดันครั้งใหญ่ไปทางไดอะแกรม เกือบทุกคนที่สอน OO บนโลกนี้มีสัญกรณ์ (notation) สำหรับคลาสและความสัมพันธ์เป็นของตัวเอง ในที่สุดก็มีความพยายามครั้งใหญ่หลายปีเพื่อรวมสัญกรณ์ให้เป็นหนึ่งเดียว UML คือผลลัพธ์ และหลายคนคิดว่านั่นเป็นการยุติการพูดถึงวิธีการออกแบบระบบใดๆ ผู้คนเริ่มคิดว่าสัญกรณ์คือวิธีการ (method) ว่า UML คือหนทางในการพัฒนาระบบ: วาดไดอะแกรมเยอะๆ แล้วค่อยเขียนโค้ดทีหลัง มันต้องใช้เวลาพอสมควรกว่าผู้คนจะตระหนักว่าแม้ว่า UML จะเป็นสัญกรณ์ที่ดีสำหรับการจัดทำเอกสารระบบ แต่มันไม่ใช่วิธีเดียวในการทำงานกับแนวคิดที่เราใช้สร้างระบบ ณ จุดนี้ ผมรู้ว่ามีวิธีที่ดีกว่ามากในการสื่อสารเกี่ยวกับการออกแบบในทีม มันเป็นเทคนิคที่เพื่อนนักทดสอบของผมตั้งชื่อว่า Naked CRC เพราะมันเหมือนกับ CRC ทุกอย่าง ยกเว้นว่าคุณไม่ต้องเขียนบนการ์ด น่าเสียดายที่มันไม่ง่ายนักที่จะอธิบายในหนังสือ นี่คือความพยายามที่ดีที่สุดของผม
หลายปีก่อน ผมเจอ Ron Jeffries ในงาน conference เขาสัญญากับผมไว้ว่าจะแสดงให้ดูว่าเขาสามารถอธิบายสถาปัตยกรรมโดยใช้การ์ดในแบบที่ทำให้ปฏิสัมพันธ์ต่างๆ ดูมีชีวิตชีวาและน่าจดจำ และเขาก็ทำได้จริงๆ มันทำงานแบบนี้ คนที่อธิบายระบบจะใช้ชุดการ์ดดัชนีเปล่าๆ และวางมันลงบนโต๊ะทีละใบ เขาหรือเธอสามารถขยับการ์ด ชี้ไปที่มัน หรือทำอะไรก็ได้ที่จำเป็นเพื่อสื่อถึง objects ทั่วไปในระบบและวิธีการที่พวกมันโต้ตอบกัน
นี่คือตัวอย่าง คำอธิบายของระบบลงคะแนนออนไลน์:
"นี่คือวิธีการทำงานของระบบลงคะแนนแบบ real-time นี่คือ client session" (ชี้ไปที่การ์ด).
"แต่ละ session มีสอง connections, incoming connection และ outgoing connection" (วางการ์ดแต่ละใบบนการ์ดใบแรกและชี้ไปที่แต่ละใบตามลำดับ).
"เมื่อมันเริ่มทำงาน session จะถูกสร้างขึ้นบน server ตรงนี้" (วางการ์ดลงทางด้านขวา).
"Server sessions ก็มีสอง connections ต่ออันเหมือนกัน" (วางการ์ดสองใบที่แทน connections ลงบนการ์ดทางด้านขวา).
"เมื่อ server session เริ่มทำงาน มันจะลงทะเบียนกับ vote manager (วางการ์ดสำหรับ vote manager ไว้เหนือ server session).
"เราสามารถมีหลาย sessions ทางฝั่ง server" (วางการ์ดอีกชุดสำหรับ server session ใหม่และการเชื่อมต่อของมัน).
"เมื่อ client ลงคะแนน คะแนนจะถูกส่งไปยัง session ทางฝั่ง server" (ใช้มือชี้จาก connection บน client-side session ไปยัง connection บน server-side session).
"server session ตอบกลับด้วย acknowledgment แล้วบันทึกคะแนนกับ vote manager" (ชี้จาก server session กลับไปยัง client session จากนั้นชี้จาก server session นั้นไปยัง vote manager).
"หลังจากนั้น vote manager จะบอกให้แต่ละ server session แจ้ง client session ของมันว่ายอดคะแนนใหม่เป็นเท่าไหร่" (ชี้จากการ์ด vote manager ไปยังแต่ละ server session ตามลำดับ).
ผมมั่นใจว่าคำอธิบายนี้ขาดอะไรบางอย่างไปเพราะผมไม่สามารถขยับการ์ดไปมาบนโต๊ะหรือชี้ไปที่มันแบบที่ทำได้ถ้าเรานั่งอยู่ด้วยกัน ถึงกระนั้น เทคนิคนี้ก็ทรงพลังทีเดียว มันทำให้ชิ้นส่วนของระบบกลายเป็นสิ่งที่จับต้องได้ คุณไม่จำเป็นต้องใช้การ์ด; อะไรก็ได้ที่หยิบใช้สะดวกก็ใช้ได้ สิ่งสำคัญคือคุณสามารถใช้การเคลื่อนไหวและตำแหน่งเพื่อแสดงว่าส่วนต่างๆ ของระบบโต้ตอบกันอย่างไร บ่อยครั้งที่สองสิ่งนี้ทำให้สถานการณ์ที่ซับซ้อนเข้าใจได้ง่ายขึ้น ด้วยเหตุผลบางอย่าง เซสชันการใช้การ์ดเหล่านี้ยังทำให้การออกแบบน่าจดจำมากขึ้นด้วย
มีแนวทางปฏิบัติแค่สองข้อใน Naked CRC:
1. การ์ดแทน instances ไม่ใช่คลาส
2. ซ้อนทับการ์ดเพื่อแสดงถึงกลุ่มของการ์ดเหล่านั้น
Conversation Scrutiny (การพิจารณาบทสนทนา)
ใน legacy code มันเย้ายวนใจที่จะหลีกเลี่ยงการสร้าง abstractions เมื่อผมมองคลาสสี่ห้าคลาสที่มีโค้ดประมาณพันบรรทัดต่อคลาส ผมไม่ได้คิดถึงการเพิ่มคลาสใหม่มากเท่ากับการพยายามหาว่าอะไรต้องเปลี่ยนแปลง
เพราะเราถูกเบี่ยงเบนความสนใจมากเมื่อพยายามหาสิ่งเหล่านี้ เรามักพลาดสิ่งที่สามารถให้แนวคิดเพิ่มเติมได้ นี่คือตัวอย่าง ผมเคยทำงานกับสมาชิกหลายคนในทีมครั้งหนึ่ง และพวกเขากำลังทำแบบฝึกหัดในการทำให้โค้ดจำนวนมากทำงานได้จากหลายเธรด โค้ดค่อนข้างซับซ้อนและมีโอกาสเกิด deadlock หลายจุด เราตระหนักว่าถ้าเราสามารถรับประกันได้ว่าทรัพยากรถูกล็อกและปลดล็อกในลำดับที่แน่นอน เราก็จะหลีกเลี่ยง deadlock ในโค้ดได้ เราเริ่มมองหาวิธีปรับเปลี่ยนโค้ดเพื่อให้สิ่งนี้เป็นไปได้ ตลอดเวลา เราพูดถึงนโยบายการล็อกใหม่นี้และหาวิธีรักษาค่านับใน arrays เพื่อให้มันทำงานได้ เมื่อโปรแกรมเมอร์คนอื่นเริ่มเขียนโค้ดนโยบายแบบอินไลน์ ผมพูดว่า "เดี๋ยวก่อน เรากำลังพูดถึงนโยบายการล็อก (locking policy) ใช่ไหม? ทำไมเราไม่สร้างคลาสชื่อ LockingPolicy และเก็บค่านับไว้ในนั้นล่ะ? เราสามารถใช้ชื่อเมธอดที่อธิบายสิ่งที่เรากำลังพยายามทำจริงๆ และมันจะชัดเจนกว่าโค้ดที่เพิ่มค่าตัวเลขนับใน array"
เรื่องที่แย่คือทีมไม่ได้ไร้ประสบการณ์ มีส่วนอื่นๆ ของโค้ดเบสที่ดูดีมาก แต่มีบางอย่างที่น่าหลงใหลเกี่ยวกับโค้ด procedural จำนวนมาก: มันดูเหมือนจะขอร้องให้มีเพิ่ม
จงฟังบทสนทนาเกี่ยวกับการออกแบบของคุณ แนวคิดที่คุณใช้ในการสนทนาเป็นแนวคิดเดียวกันกับในโค้ดหรือเปล่า? ผมไม่คาดหวังว่ามันจะเหมือนกันทั้งหมด ซอฟต์แวร์ต้องตอบสนองข้อจำกัดที่แข็งแกร่งกว่าแค่การพูดถึงได้ง่าย แต่ถ้าไม่มีความทับซ้อนที่แข็งแกร่งระหว่างการสนทนาและโค้ด มันสำคัญที่จะถามว่าทำไม คำตอบมักจะเป็นส่วนผสมของสองสิ่ง: โค้ดไม่ได้รับอนุญาตให้ปรับตัวตามความเข้าใจของทีม หรือทีมจำเป็นต้องเข้าใจมันแตกต่างออกไป ไม่ว่าจะกรณีใด การปรับให้เข้ากับแนวคิดที่ผู้คนใช้ตามธรรมชาติเพื่ออธิบายการออกแบบนั้นทรงพลัง เมื่อผู้คนพูดถึงการออกแบบ พวกเขากำลังพยายามให้คนอื่นเข้าใจพวกเขา ใส่ความเข้าใจนั้นบางส่วนลงในโค้ด
ในบทนี้ ผมได้อธิบายเทคนิคสองสามอย่างสำหรับการค้นพบและสื่อสารสถาปัตยกรรมของระบบขนาดใหญ่ที่มีอยู่แล้ว เทคนิคหลายอย่างก็เป็นวิธีที่ดีในการออกแบบระบบใหม่ด้วย การออกแบบก็คือการออกแบบ ไม่ว่ามันจะเกิดขึ้นในช่วงไหนของวงจรการพัฒนา หนึ่งในความผิดพลาดที่แย่ที่สุดที่ทีมสามารถทำได้คือการรู้สึกว่าการออกแบบ "จบลงแล้ว" ณ จุดใดจุดหนึ่งในการพัฒนา ถ้าการออกแบบ "จบลง" และผู้คนยังคงทำการเปลี่ยนแปลงอยู่ โอกาสสูงที่โค้ดใหม่จะไปปรากฏในที่ที่ไม่เหมาะสม และคลาสจะพองตัวเพราะไม่มีใครรู้สึกสบายใจที่จะแนะนำ abstraction ใหม่ ไม่มีวิธีใดที่จะทำให้ legacy system แย่ลงไปได้แน่นอนกว่านี้อีกแล้ว