เขียนการทดสอบที่สำคัญ: จัดการกับโค้ดที่ซับซ้อนที่สุดก่อน
เผยแพร่แล้ว: 2022-03-11มีการอภิปราย บทความ และบล็อกมากมายเกี่ยวกับคุณภาพของโค้ด มีคนบอกว่า - ใช้เทคนิค Test Driven! การทดสอบเป็นสิ่งที่ "ต้องมี" เพื่อเริ่มการปรับโครงสร้างใหม่! เยี่ยมไปเลย แต่นี่มันปี 2016 แล้ว ยังมีผลิตภัณฑ์และฐานรหัสจำนวนมากที่ยังคงอยู่ในการผลิต ซึ่งสร้างขึ้นเมื่อสิบ สิบห้า หรือยี่สิบปีที่แล้ว ไม่ต้องสงสัยเลยว่าพวกเขาส่วนใหญ่มีรหัสเดิมที่มีความครอบคลุมในการทดสอบต่ำ
ในขณะที่ฉันต้องการเป็นผู้นำเสมอ หรือแม้แต่เลือดไหล ขอบของโลกเทคโนโลยี - มีส่วนร่วมกับโครงการและเทคโนโลยีเจ๋ง ๆ - น่าเสียดายที่มันไม่สามารถทำได้เสมอไปและบ่อยครั้งฉันต้องจัดการกับระบบเก่า ฉันชอบที่จะบอกว่าเมื่อคุณพัฒนาจากศูนย์ คุณทำหน้าที่เป็นผู้สร้าง เชี่ยวชาญเรื่องใหม่ แต่เมื่อคุณทำงานกับรหัสเดิม คุณเป็นเหมือนศัลยแพทย์มากกว่า คุณรู้ว่าระบบทำงานอย่างไรโดยทั่วไป แต่คุณไม่มีทางรู้แน่ชัดว่าผู้ป่วยจะรอดจาก "การผ่าตัด" ของคุณหรือไม่ และเนื่องจากเป็นรหัสดั้งเดิม คุณจึงไม่ต้องพึ่งพาการทดสอบล่าสุดมากนัก ซึ่งหมายความว่าบ่อยครั้งมากหนึ่งในขั้นตอนแรกสุดคือการทดสอบ แม่นยำยิ่งขึ้น ไม่ใช่แค่เพื่อให้ครอบคลุม แต่เพื่อพัฒนากลยุทธ์การทดสอบความครอบคลุม
โดยพื้นฐานแล้ว สิ่งที่ฉันต้องพิจารณาคือส่วนใด (คลาส / แพ็คเกจ) ของระบบที่เราจำเป็นต้องครอบคลุมด้วยการทดสอบตั้งแต่แรก ที่ซึ่งเราต้องการการทดสอบหน่วย ซึ่งการทดสอบการรวมจะมีประโยชน์มากกว่า ฯลฯ มีหลายวิธีที่ยอมรับได้ เข้าหาการวิเคราะห์ประเภทนี้และแบบที่ฉันใช้อาจไม่ใช่วิธีที่ดีที่สุด แต่เป็นวิธีอัตโนมัติ เมื่อนำแนวทางของฉันไปใช้แล้ว จะต้องใช้เวลาน้อยที่สุดในการวิเคราะห์ตัวเองจริงๆ และสิ่งที่สำคัญกว่านั้นก็คือ การนำความสนุกมาสู่การวิเคราะห์โค้ดแบบเดิม
แนวคิดหลักในที่นี้คือการวิเคราะห์เมตริกสองแบบ – การมีเพศสัมพันธ์ (เช่น การมีเพศสัมพันธ์ทางอวัยวะ หรือ CA) และความซับซ้อน (เช่น ความซับซ้อนของวงจร)
อันแรกวัดจำนวนคลาสที่ใช้คลาสของเรา ดังนั้นโดยพื้นฐานแล้วบอกเราว่าคลาสใดคลาสหนึ่งอยู่ใกล้หัวใจของระบบมากแค่ไหน ยิ่งมีชั้นเรียนที่ใช้ชั้นเรียนของเรามากเท่าใด การทดสอบก็ยิ่งมีความสำคัญมากขึ้นเท่านั้น
ในทางกลับกัน ถ้าคลาสนั้นง่ายมาก (เช่น มีเพียงค่าคงที่) แม้ว่าจะใช้โดยส่วนอื่น ๆ ของระบบ ก็ไม่สำคัญเท่ากับการสร้างการทดสอบ นี่คือจุดที่เมตริกที่สองสามารถช่วยได้ หากคลาสมีตรรกะมากมาย ความซับซ้อนของ Cyclomatic จะสูง
ตรรกะเดียวกันนี้สามารถนำไปใช้ย้อนกลับได้ กล่าวคือ แม้ว่าหลาย ๆ คลาสจะไม่ได้ใช้คลาสใด ๆ และเป็นเพียงกรณีการใช้งานหนึ่ง ๆ ก็ตาม ก็ยังสมเหตุสมผลที่จะครอบคลุมมันด้วยการทดสอบหากตรรกะภายในของมันซับซ้อน
มีข้อแม้ประการหนึ่ง: สมมติว่าเรามีสองคลาส – หนึ่งมี CA 100 และความซับซ้อน 2 และอีกอันหนึ่งมี CA 60 และความซับซ้อน 20 แม้ว่าผลรวมของตัวชี้วัดจะสูงกว่าสำหรับอันแรก เราควรครอบคลุมอย่างแน่นอน คนที่สองก่อน เนื่องจากชั้นหนึ่งถูกใช้โดยชั้นอื่นๆ มากมาย แต่ไม่ซับซ้อนมาก ในทางกลับกัน คลาสที่สองก็ถูกใช้โดยคลาสอื่นๆ มากมาย แต่ค่อนข้างซับซ้อนกว่าคลาสแรก
โดยสรุป: เราจำเป็นต้องระบุคลาสที่มี CA และ Cyclomatic ที่ซับซ้อนสูง ในแง่คณิตศาสตร์ ฟังก์ชันฟิตเนสจำเป็นที่สามารถใช้เป็นการให้คะแนน - f(CA,ความซับซ้อน) ซึ่งมีค่าเพิ่มขึ้นพร้อมกับ CA และความซับซ้อน
การค้นหาเครื่องมือในการคำนวณ CA และความซับซ้อนสำหรับฐานโค้ดทั้งหมด และให้วิธีง่ายๆ ในการดึงข้อมูลนี้ในรูปแบบ CSV ซึ่งพิสูจน์แล้วว่าเป็นความท้าทาย ระหว่างการค้นหาของฉัน ฉันพบเครื่องมือสองอย่างที่ใช้งานได้ฟรี จึงไม่ยุติธรรมที่จะไม่พูดถึงเครื่องมือเหล่านี้:
- เมตริกข้อต่อ: www.spinellis.gr/sw/ckjm/
- ความซับซ้อน: cyvis.sourceforge.net/
คณิตศาสตร์เล็กน้อย
ปัญหาหลักที่นี่คือ เรามีเกณฑ์สองข้อ – CA และความซับซ้อนแบบไซโคลมาติก – ดังนั้นเราจึงจำเป็นต้องรวมเกณฑ์เหล่านี้และแปลงเป็นค่าสเกลาร์หนึ่งค่า หากเรามีงานที่แตกต่างออกไปเล็กน้อย – เช่น เพื่อค้นหาชั้นเรียนที่มีเกณฑ์รวมกันที่แย่ที่สุด – เราจะมีปัญหาการปรับให้เหมาะสมแบบหลายวัตถุประสงค์แบบคลาสสิก:
คงต้องหาจุดที่เรียกว่าหน้าพาเรโต (สีแดงในรูปด้านบน) สิ่งที่น่าสนใจเกี่ยวกับชุด Pareto คือทุกจุดในชุดเป็นโซลูชันสำหรับงานเพิ่มประสิทธิภาพ เมื่อใดก็ตามที่เราเดินไปตามเส้นสีแดง เราจำเป็นต้องประนีประนอมระหว่างเกณฑ์ของเรา – หากสิ่งใดดีขึ้น เกณฑ์อื่นจะแย่ลง สิ่งนี้เรียกว่า Scalarization และผลลัพธ์สุดท้ายขึ้นอยู่กับวิธีที่เราทำ
มีเทคนิคมากมายที่เราสามารถใช้ได้ที่นี่ แต่ละคนมีข้อดีและข้อเสียของตัวเอง อย่างไรก็ตาม สิ่งที่ได้รับความนิยมมากที่สุดคือการสเกลาไรซ์เชิงเส้นและอันที่อิงตามจุดอ้างอิง เชิงเส้นเป็นวิธีที่ง่ายที่สุด ฟังก์ชันฟิตเนสของเราจะมีลักษณะเป็นเส้นตรงของ CA และความซับซ้อน:
f(CA, ความซับซ้อน) = A×CA + B×Complexity
โดยที่ A และ B เป็นสัมประสิทธิ์บางอย่าง
จุดที่แสดงถึงการแก้ปัญหาการเพิ่มประสิทธิภาพจะอยู่ในบรรทัด (สีน้ำเงินในภาพด้านล่าง) ให้แม่นยำยิ่งขึ้นก็จะอยู่ที่จุดตัดของเส้นสีน้ำเงินและด้านหน้า Pareto สีแดง ปัญหาเดิมของเราไม่ใช่ปัญหาการเพิ่มประสิทธิภาพอย่างแน่นอน แต่เราจำเป็นต้องสร้างฟังก์ชันการจัดอันดับ ลองพิจารณาค่าสองค่าของฟังก์ชันการจัดอันดับของเรา โดยพื้นฐานแล้วค่าสองค่าในคอลัมน์อันดับของเรา:
R1 = A∗CA + B∗ความซับซ้อน และ R2 = A∗CA + B∗ความซับซ้อน

สูตรทั้งสองที่เขียนไว้ข้างต้นเป็นสมการของเส้นตรง นอกจากนี้ เส้นเหล่านี้ยังขนานกัน การพิจารณาค่าอันดับที่มากขึ้น เราจะได้เส้นมากขึ้น ดังนั้นจึงได้คะแนนที่เส้น Pareto ตัดกับเส้นสีน้ำเงิน (จุด) มากขึ้น คะแนนเหล่านี้จะเป็นคลาสที่สอดคล้องกับค่าอันดับเฉพาะ
ขออภัย มีปัญหากับแนวทางนี้ สำหรับบรรทัดใด ๆ (ค่าอันดับ) เราจะมีคะแนนที่มี CA ขนาดเล็กมากและความซับซ้อนที่ใหญ่มาก (และในทางกลับกันด้วยวีซ่า) วางอยู่บนนั้น ซึ่งจะทำให้คะแนนมีความแตกต่างกันมากระหว่างค่าเมตริกที่ด้านบนของรายการ ซึ่งเป็นสิ่งที่เราต้องการหลีกเลี่ยง
อีกวิธีหนึ่งในการทำสเกลาไรซ์นั้นขึ้นอยู่กับจุดอ้างอิง จุดอ้างอิงคือจุดที่มีค่าสูงสุดของเกณฑ์ทั้งสอง:
(สูงสุด (CA), สูงสุด (ความซับซ้อน))
ฟังก์ชันฟิตเนสจะเป็นระยะห่างระหว่างจุดอ้างอิงและจุดข้อมูล:
f(CA,ความซับซ้อน) = √((CA−CA ) 2 + (ความซับซ้อน−ความซับซ้อน) 2 )
เราสามารถนึกถึงฟังก์ชันฟิตเนสนี้เป็นวงกลมที่มีจุดศูนย์กลางอยู่ที่จุดอ้างอิง รัศมีในกรณีนี้คือค่าของอันดับ วิธีแก้ไขปัญหาการปรับให้เหมาะสมคือจุดที่วงกลมสัมผัสกับด้านหน้า Pareto วิธีแก้ปัญหาเดิมจะเป็นชุดของจุดที่สอดคล้องกับรัศมีวงกลมต่างๆ ดังแสดงในรูปภาพต่อไปนี้ (ส่วนของวงกลมสำหรับอันดับต่างๆ จะแสดงเป็นเส้นโค้งเส้นประสีน้ำเงิน):
แนวทางนี้ใช้กับค่าสุดขั้วได้ดีกว่า แต่ยังมีประเด็นอยู่สองประเด็น: อย่างแรก ฉันต้องการให้มีคะแนนมากขึ้นใกล้กับจุดอ้างอิงเพื่อเอาชนะปัญหาที่เราพบด้วยชุดค่าผสมเชิงเส้นได้ดียิ่งขึ้น ประการที่สอง – ความซับซ้อนของ CA และ Cyclomatic นั้นแตกต่างกันโดยเนื้อแท้และมีการตั้งค่าที่แตกต่างกัน ดังนั้นเราจึงจำเป็นต้องทำให้เป็นมาตรฐาน (เช่น เพื่อให้ค่าทั้งหมดของตัวชี้วัดทั้งสองมีค่าตั้งแต่ 1 ถึง 100)
นี่เป็นเคล็ดลับเล็กๆ น้อยๆ ที่เราสามารถนำมาใช้แก้ปัญหาแรกได้ แทนที่จะดูที่ CA และ Cyclomatic Complexity เราสามารถดูค่าที่กลับกัน จุดอ้างอิงในกรณีนี้จะเป็น (0,0) ในการแก้ปัญหาที่สอง เราสามารถตั้งค่าเมตริกให้เป็นมาตรฐานโดยใช้ค่าต่ำสุดได้ นี่คือลักษณะ:
ความซับซ้อนแบบกลับด้านและทำให้เป็นมาตรฐาน – NormComplexity:
(1 + นาที(ความซับซ้อน)) / (1 + ความซับซ้อน)∗100
CA แบบกลับด้านและทำให้เป็นมาตรฐาน – NormCA:
(1 + นาที(CA)) / (1+CA)∗100
หมายเหตุ: ฉันเพิ่ม 1 เพื่อให้แน่ใจว่าไม่มีการหารด้วย 0
รูปภาพต่อไปนี้แสดงพล็อตที่มีค่ากลับด้าน:
อันดับสุดท้าย
เรากำลังเข้าสู่ขั้นตอนสุดท้าย - การคำนวณอันดับ ตามที่กล่าวไว้ ฉันกำลังใช้วิธีจุดอ้างอิง ดังนั้นสิ่งเดียวที่เราต้องทำคือการคำนวณความยาวของเวกเตอร์ ทำให้เป็นมาตรฐาน และทำให้ขึ้นด้วยความสำคัญของการสร้างการทดสอบหน่วยสำหรับชั้นเรียน นี่คือสูตรสุดท้าย:
อันดับ(NormComplexity , NormCA) = 100 − √(NormComplexity 2 + NormCA 2 ) / √2
สถิติเพิ่มเติม
มีอีกความคิดหนึ่งที่ผมอยากจะเพิ่ม แต่มาดูสถิติกันก่อน นี่คือฮิสโตแกรมของเมตริกการมีเพศสัมพันธ์:
สิ่งที่น่าสนใจเกี่ยวกับรูปภาพนี้คือจำนวนคลาสที่มี CA ต่ำ (0-2) คลาสที่มี CA 0 ไม่ได้ใช้เลยหรือเป็นบริการระดับบนสุด สิ่งเหล่านี้แสดงถึงตำแหน่งข้อมูล API ดังนั้นจึงเป็นเรื่องปกติที่เรามีจำนวนมาก แต่คลาสที่มี CA 1 เป็นคลาสที่อุปกรณ์ปลายทางใช้โดยตรง และเรามีคลาสเหล่านี้มากกว่าจุดปลาย สิ่งนี้หมายความว่าอย่างไรจากมุมมองของสถาปัตยกรรม / การออกแบบ?
โดยทั่วไป หมายความว่าเรามีแนวทางเชิงสคริปต์ประเภทหนึ่ง – เราเขียนสคริปต์แต่ละกรณีธุรกิจแยกกัน (เราไม่สามารถนำรหัสกลับมาใช้ใหม่ได้จริง ๆ เนื่องจากกรณีธุรกิจมีความหลากหลายเกินไป) หากเป็นกรณีนี้ แสดงว่ามีกลิ่นโค้ดและเราต้องทำการปรับโครงสร้างใหม่ มิฉะนั้น หมายความว่าการทำงานร่วมกันของระบบของเราอยู่ในระดับต่ำ ซึ่งในกรณีนี้ เราจำเป็นต้องมีการปรับโครงสร้างใหม่เช่นกัน แต่คราวนี้ต้องมีการปรับโครงสร้างสถาปัตยกรรมใหม่
ข้อมูลที่เป็นประโยชน์เพิ่มเติมที่เราได้จากฮิสโตแกรมด้านบนคือเราสามารถกรองคลาสที่มีคัปปลิ้งต่ำ (CA ใน {0,1}) ออกจากรายการคลาสที่มีสิทธิ์ครอบคลุมด้วยการทดสอบหน่วย แม้ว่าชั้นเรียนเดียวกันจะเป็นตัวเลือกที่ดีสำหรับการทดสอบการรวม / การทำงาน
คุณสามารถค้นหาสคริปต์และทรัพยากรทั้งหมดที่ฉันใช้ในที่เก็บ GitHub นี้: ashalitkin/code-base-stats
มันใช้งานได้เสมอหรือไม่?
ไม่จำเป็น. อย่างแรกเลย ทั้งหมดเกี่ยวกับการวิเคราะห์แบบสแตติก ไม่ใช่รันไทม์ หากคลาสนั้นเชื่อมโยงจากคลาสอื่น ๆ จำนวนมาก อาจเป็นสัญญาณว่ามีการใช้งานอย่างหนัก แต่ก็ไม่เป็นความจริงเสมอไป ตัวอย่างเช่น เราไม่ทราบว่าผู้ใช้ปลายทางใช้งานฟังก์ชันนี้มากจริงหรือไม่ ประการที่สอง ถ้าการออกแบบและคุณภาพของระบบดีเพียงพอ ส่วนใหญ่มักจะแยกส่วน/ชั้นของระบบออกผ่านอินเทอร์เฟซ ดังนั้นการวิเคราะห์แบบคงที่ของ CA จะไม่ให้ภาพที่แท้จริงแก่เรา ฉันเดาว่ามันเป็นหนึ่งในสาเหตุหลักที่ทำให้ CA ไม่เป็นที่นิยมในเครื่องมืออย่าง Sonar โชคดีที่ไม่เป็นไรสำหรับเรา เพราะถ้าคุณจำได้ เราสนใจที่จะใช้สิ่งนี้กับฐานโค้ดที่น่าเกลียดแบบเก่าโดยเฉพาะ
โดยทั่วไป ฉันคิดว่าการวิเคราะห์รันไทม์จะให้ผลลัพธ์ที่ดีกว่ามาก แต่น่าเสียดายที่มันมีค่าใช้จ่ายสูง ใช้เวลานาน และซับซ้อนกว่ามาก ดังนั้นแนวทางของเราจึงเป็นทางเลือกที่มีประโยชน์และมีค่าใช้จ่ายต่ำกว่า