Tulis Tes yang Penting: Selesaikan Kode Paling Rumit Terlebih Dahulu
Diterbitkan: 2022-03-11Ada banyak diskusi, artikel, dan blog seputar topik kualitas kode. Kata orang - gunakan teknik Test Driven! Tes adalah "harus dimiliki" untuk memulai refactoring apa pun! Itu semua keren, tapi ini tahun 2016 dan ada sejumlah besar produk dan basis kode yang masih diproduksi yang dibuat sepuluh, lima belas, atau bahkan dua puluh tahun yang lalu. Bukan rahasia lagi bahwa banyak dari mereka memiliki kode lama dengan cakupan pengujian yang rendah.
Meskipun saya ingin selalu menjadi yang terdepan, atau bahkan berdarah-darah, di ujung dunia teknologi - terlibat dengan proyek dan teknologi baru yang keren - sayangnya itu tidak selalu memungkinkan dan sering kali saya harus berurusan dengan sistem lama. Saya suka mengatakan bahwa ketika Anda berkembang dari awal, Anda bertindak sebagai pencipta, menguasai materi baru. Tetapi ketika Anda mengerjakan kode warisan, Anda lebih seperti ahli bedah – Anda tahu cara kerja sistem secara umum, tetapi Anda tidak pernah tahu pasti apakah pasien akan selamat dari “operasi” Anda. Dan karena ini adalah kode lama, tidak banyak pengujian terkini yang dapat Anda andalkan. Ini berarti bahwa sangat sering salah satu langkah pertama adalah menutupinya dengan tes. Lebih tepatnya, tidak hanya untuk memberikan cakupan, tetapi untuk mengembangkan strategi cakupan tes.
Pada dasarnya, yang perlu saya tentukan adalah bagian (kelas/paket) apa dari sistem yang pertama-tama perlu kami bahas dengan pengujian, di mana kami membutuhkan pengujian unit, di mana pengujian integrasi akan lebih membantu, dll. Memang ada banyak cara untuk pendekatan jenis analisis ini dan yang saya gunakan mungkin bukan yang terbaik, tetapi ini semacam pendekatan otomatis. Setelah pendekatan saya diterapkan, dibutuhkan waktu minimal untuk benar-benar melakukan analisis itu sendiri dan, yang lebih penting, ini membawa kesenangan ke dalam analisis kode lawas.
Ide utama di sini adalah untuk menganalisis dua metrik – kopling (yaitu, kopling aferen, atau CA) dan kompleksitas (yaitu kompleksitas siklomatik).
Yang pertama mengukur berapa banyak kelas yang menggunakan kelas kita, jadi pada dasarnya memberitahu kita seberapa dekat kelas tertentu dengan jantung sistem; semakin banyak kelas yang menggunakan kelas kami, semakin penting untuk menutupinya dengan tes.
Di sisi lain, jika sebuah kelas sangat sederhana (misalnya hanya berisi konstanta), bahkan jika kelas itu digunakan oleh banyak bagian lain dari sistem, membuat pengujian tidak sepenting itu. Di sinilah metrik kedua dapat membantu. Jika sebuah kelas mengandung banyak logika, kompleksitas Cyclomatic akan tinggi.
Logika yang sama juga dapat diterapkan secara terbalik; yaitu, bahkan jika sebuah kelas tidak digunakan oleh banyak kelas dan hanya mewakili satu kasus penggunaan tertentu, masih masuk akal untuk menutupinya dengan tes jika logika internalnya kompleks.
Namun ada satu peringatan: katakanlah kita memiliki dua kelas – satu dengan CA 100 dan kompleksitas 2 dan yang lainnya dengan CA 60 dan kompleksitas 20. Meskipun jumlah metrik lebih tinggi untuk yang pertama, kita pasti harus membahasnya yang kedua dulu. Ini karena kelas pertama sedang digunakan oleh banyak kelas lain, tetapi tidak terlalu kompleks. Di sisi lain, kelas kedua juga digunakan oleh banyak kelas lain tetapi relatif lebih kompleks daripada kelas pertama.
Untuk meringkas: kita perlu mengidentifikasi kelas dengan CA tinggi dan kompleksitas Cyclomatic. Dalam istilah matematika, diperlukan fungsi fitness yang dapat digunakan sebagai rating - f(CA,Complexity) - yang nilainya meningkat seiring dengan CA dan Complexity.
Menemukan alat untuk menghitung CA dan Kompleksitas untuk seluruh basis kode, dan menyediakan cara sederhana untuk mengekstrak informasi ini dalam format CSV, terbukti menjadi suatu tantangan. Selama pencarian saya, saya menemukan dua alat yang gratis sehingga tidak adil untuk tidak menyebutkannya:
- Metrik kopling: www.spinellis.gr/sw/ckjm/
- Kompleksitas: cyvis.sourceforge.net/
Sedikit Matematika
Masalah utama di sini adalah kita memiliki dua kriteria – CA dan kompleksitas Cyclomatic – jadi kita perlu menggabungkannya dan mengubahnya menjadi satu nilai skalar. Jika kami memiliki tugas yang sedikit berbeda – misalnya, untuk menemukan kelas dengan kombinasi terburuk dari kriteria kami – kami akan memiliki masalah optimasi multi-tujuan klasik:
Kita perlu menemukan titik di bagian depan Pareto (merah pada gambar di atas). Yang menarik dari himpunan Pareto adalah bahwa setiap titik dalam himpunan tersebut merupakan solusi dari tugas optimasi. Setiap kali kita bergerak di sepanjang garis merah, kita perlu membuat kompromi antara kriteria kita – jika yang satu menjadi lebih baik yang lain menjadi lebih buruk. Ini disebut Skalarisasi dan hasil akhirnya tergantung pada bagaimana kita melakukannya.
Ada banyak teknik yang bisa kita gunakan di sini. Masing-masing memiliki pro dan kontra sendiri. Namun, yang paling populer adalah skalarisasi linier dan yang didasarkan pada titik referensi. Linear adalah yang paling mudah. Fungsi kebugaran kami akan terlihat seperti kombinasi linier CA dan Kompleksitas:
f(CA, Kompleksitas) = A×CA + B×Kompleksitas
di mana A dan B adalah beberapa koefisien.
Titik yang mewakili solusi untuk masalah optimasi kami akan terletak pada garis (biru pada gambar di bawah). Lebih tepatnya, akan berada di persimpangan garis biru dan merah depan Pareto. Masalah awal kita bukanlah masalah optimasi. Sebaliknya, kita perlu membuat fungsi peringkat. Mari kita pertimbangkan dua nilai fungsi peringkat kami, pada dasarnya dua nilai di kolom Peringkat kami:
R1 = A∗CA + B∗Kompleksitas dan R2 = A∗CA + B∗Kompleksitas

Kedua rumus yang tertulis di atas merupakan persamaan garis, apalagi garis-garis tersebut sejajar. Dengan mempertimbangkan lebih banyak nilai peringkat, kita akan mendapatkan lebih banyak garis dan oleh karena itu lebih banyak titik di mana garis Pareto berpotongan dengan garis biru (bertitik). Poin-poin ini akan menjadi kelas yang sesuai dengan nilai peringkat tertentu.
Sayangnya, ada masalah dengan pendekatan ini. Untuk setiap baris (Nilai peringkat), kami akan memiliki poin dengan CA yang sangat kecil dan Kompleksitas yang sangat besar (dan sebaliknya) terletak di atasnya. Ini segera menempatkan poin dengan perbedaan besar antara nilai metrik di bagian atas daftar yang persisnya ingin kami hindari.
Cara lain untuk melakukan penskalaan didasarkan pada titik referensi. Titik acuan adalah titik dengan nilai maksimum dari kedua kriteria:
(maks(CA), maks(Kompleksitas))
Fungsi kebugaran akan menjadi jarak antara titik Referensi dan titik data:
f(CA,Kompleksitas) = ((CA−CA ) 2 + (Kompleksitas−Kompleksitas) 2 )
Kita dapat menganggap fungsi fitness ini sebagai sebuah lingkaran dengan pusat pada titik acuan. Jari-jari dalam hal ini adalah nilai dari Rank. Solusi untuk masalah optimasi akan menjadi titik di mana lingkaran menyentuh bagian depan Pareto. Solusi untuk masalah awal akan berupa kumpulan titik yang sesuai dengan jari-jari lingkaran yang berbeda seperti yang ditunjukkan pada gambar berikut (bagian lingkaran untuk peringkat yang berbeda ditunjukkan sebagai kurva biru bertitik):
Pendekatan ini lebih baik menangani nilai ekstrim tetapi masih ada dua masalah: Pertama – Saya ingin memiliki lebih banyak titik di dekat titik referensi untuk mengatasi masalah yang kita hadapi dengan kombinasi linier dengan lebih baik. Kedua – Kompleksitas CA dan Cyclomatic secara inheren berbeda dan memiliki nilai yang berbeda, jadi kita perlu menormalkannya (misalnya agar semua nilai dari kedua metrik adalah dari 1 hingga 100).
Berikut adalah trik kecil yang dapat kita terapkan untuk menyelesaikan masalah pertama – alih-alih melihat CA dan Kompleksitas Cyclomatic, kita dapat melihat nilai terbaliknya. Titik referensi dalam hal ini adalah (0,0). Untuk mengatasi masalah kedua, kita bisa menormalkan metrik menggunakan nilai minimum. Berikut adalah tampilannya:
Kompleksitas terbalik dan dinormalisasi – NormComplexity:
(1 + mnt(Kompleksitas)) / (1 + Kompleksitas)∗100
CA terbalik dan dinormalisasi – NormCA:
(1 + mnt(CA)) / (1+CA)∗100
Catatan: Saya menambahkan 1 untuk memastikan tidak ada pembagian dengan 0.
Gambar berikut menunjukkan plot dengan nilai terbalik:
Peringkat Akhir
Kita sekarang sampai pada langkah terakhir - menghitung peringkat. Seperti yang disebutkan, saya menggunakan metode titik referensi, jadi satu-satunya hal yang perlu kita lakukan adalah menghitung panjang vektor, menormalkannya, dan membuatnya naik dengan pentingnya pembuatan unit test untuk sebuah kelas. Berikut adalah rumus akhirnya:
Peringkat(NormComplexity , NormCA) = 100 (NormComplexity 2 + NormCA 2 ) / 2
Statistik Lainnya
Ada satu pemikiran lagi yang ingin saya tambahkan, tetapi pertama-tama mari kita lihat beberapa statistik. Berikut adalah histogram metrik Coupling:
Yang menarik dari gambar ini adalah banyaknya kelas dengan CA rendah (0-2). Kelas dengan CA 0 tidak digunakan sama sekali atau merupakan layanan tingkat atas. Ini mewakili titik akhir API, jadi tidak masalah jika kita memiliki banyak dari mereka. Tetapi kelas dengan CA 1 adalah kelas yang langsung digunakan oleh titik akhir dan kami memiliki lebih banyak kelas ini daripada titik akhir. Apa artinya ini dari perspektif arsitektur / desain?
Secara umum, ini berarti bahwa kami memiliki semacam pendekatan berorientasi skrip – kami membuat skrip setiap kasus bisnis secara terpisah (kami tidak dapat benar-benar menggunakan kembali kode karena kasus bisnis terlalu beragam). Jika itu masalahnya, maka itu pasti bau kode dan kita perlu melakukan refactoring. Jika tidak, itu berarti kohesi sistem kami rendah, dalam hal ini kami juga membutuhkan refactoring, tetapi refactoring arsitektural kali ini.
Informasi tambahan yang berguna yang dapat kita peroleh dari histogram di atas adalah bahwa kita dapat sepenuhnya menyaring kelas dengan kopling rendah (CA dalam {0,1}) dari daftar kelas yang memenuhi syarat untuk cakupan dengan unit test. Kelas yang sama, bagaimanapun, adalah kandidat yang baik untuk tes integrasi/fungsional.
Anda dapat menemukan semua skrip dan sumber daya yang saya gunakan di repositori GitHub ini: ashalitkin/code-base-stats.
Apakah Itu Selalu Bekerja?
Belum tentu. Pertama-tama ini semua tentang analisis statis, bukan runtime. Jika sebuah kelas ditautkan dari banyak kelas lain, itu bisa menjadi tanda bahwa itu banyak digunakan, tetapi itu tidak selalu benar. Misalnya, kami tidak tahu apakah fungsi tersebut benar-benar banyak digunakan oleh pengguna akhir. Kedua, jika desain dan kualitas sistem cukup baik, maka kemungkinan besar bagian / lapisan yang berbeda dipisahkan melalui antarmuka sehingga analisis statis CA tidak akan memberi kita gambaran yang benar. Saya kira itu salah satu alasan utama mengapa CA tidak begitu populer di alat seperti Sonar. Untungnya, ini baik-baik saja bagi kami karena, jika Anda ingat, kami tertarik untuk menerapkan ini secara khusus ke basis kode lama yang jelek.
Secara umum, saya akan mengatakan bahwa analisis runtime akan memberikan hasil yang jauh lebih baik, tetapi sayangnya itu jauh lebih mahal, memakan waktu, dan kompleks, jadi pendekatan kami adalah alternatif yang berpotensi berguna dan berbiaya lebih rendah.