Catatan Pribadi Mengenai Penulisan Kode yang Maintainable — Bagian 2
Hello, world!
Waktu itu saya sudah menulis artikel mengenai penulisan kode yang maintainable (bisa dilihat di sini). Tetapi, saya rasa akan lebih baik untuk membahas lebih lanjut mengenai hal tersebut di artikel ini, dan kali ini akan saya bahas lebih lanjut ke arah refactoring.
Pendahuluan
Ketika kita menulis kode, kita bisa saja membuat kode yang bekerja, namun tidak mengikuti panduan best practice. Kode yang kita buat akan jadi terkesan “ajaib” dan kita akan kesulitan untuk membacanya kembali di kemudian hari. Bahkan, ketika project sudah diserahkan ke pengembang berikutnya, pengembang berikutnya akan kebingungan dan lebih memilih untuk menuliskan kode baru dibanding menggunakan kode tersebut.
Untuk mencegah terjadinya hal tersebut, kita harus melakukan yang namanya refactoring. Refactoring adalah proses dalam pengembangan perangkat lunak untuk mengubah kode yang ada menjadi lebih bersih, tanpa mengubah fungsionalitas dari kode.
Menurut mata kuliah pemrograman lanjut, setidaknya ada 5 hal yang menjadi tanda untuk melakukan refactoring. Tanda-tanda ini kita sebut sebagai code smell, dan code smell inilah yang perlu untuk dibersihkan, di antaranya:
- Bloaters: kode sulit dibaca karena terlalu besar.
- OO Abusers: kode yang melanggar Object-Oriented Principles (bisa terjadi dengan model pemrograman berorientasi objek).
- Change Preventers: menyulitkan perubahan atau ekstensi kode.
- Dispensables: ada bagian kode yang tidak berguna.
- Couplers: antar komponen saling terikat erat.
Ketika kita sudah menemui hal-hal tersebut, alangkah baiknya untuk segera melakukan refactoring.
Dalam proyek perangkat lunak kami, terdapat beberapa hal yang kami lakukan untuk menjaga maintainability melalui refactoring:
- Parameter fungsi yang besar diganti dengan struct.
Saat setiap fungsi memanggil fungsi lainnya, tentunya akan ada parameter yang dibutuhkan. Sebagai contoh, terdapat fungsi berikut untuk menyimpan data yang kami sebut materi pada aplikasi kami, pada awalnya dibuat seperti ini:
CreateMaterial(id uint, sectionId uint, name string, url string, contentType string) (models.Material, error)
Namun, jika dilihat kembali, ini akan mengakibatkan perubahan besar apabila kita butuh untuk menambahkan parameter ke dalam fungsi tersebut. Ditambah, kita menjadi tidak fleksibel untuk mengembalikan nilai-nilai lain yang mungkin akan butuh untuk ditambahkan nanti. Untuk mengatasi hal tersebut, kita bisa mengubah fungsi untuk menerima DTO, kemudian mengembalikan DTO juga. DTO atau data transfer object adalah objek yang ditujukan khusus untuk transfer data saja. Sehingga tidak ada method yang diletakkan pada DTO. Representasi DTO pada golang dapat dibuat dengan struct. Sehingga fungsinya menjadi seperti berikut:
CreateMaterial(dto.CourseContentCreateMaterialReqDTO) (dto.CourseContentMaterialRespDTO, error)
Dengan definisi struct yang kami pakai saat ini adalah sebagai berikut:
type MaterialDTO struct {
Name string `form:"name" json:"name" binding:"required"`
URL string `form:"url" json:"url" binding:"required"`
}type CourseContentCreateMaterialReqDTO struct {
SectionID uint `form:"sectionId" json:"sectionId" binding:"required"`
MaterialDTO
Requirements []uint `form:"requirements" json:"requirements"`
}type CourseContentMaterialRespDTO struct {
ID uint `json:"id"`
SectionID uint `json:"sectionId"`
Seq int `json:"seq"`
ContentType string `json:"contentType"`
Requirements []interface{} `json:"requirements"`
IsCompleted bool `json:"isCompleted"`
MaterialWithIDAndTypeDTO
UncompletedRequirements []string `json:"uncompleted"`
}type MaterialWithIDAndTypeDTO struct {
ContentID uint `form:"contentId" json:"contentId" binding:"required"`
Name string `form:"name" json:"name" binding:"required"`
URL string `form:"url" json:"url" binding:"required"`
Type string `form:"type" json:"type" binding:"required"`
}
- Optimasi kode
Menurut Donald Knuth, optimisasi prematur adalah akar dari segala kejahatan. Oleh karena itu, pada kelompok PPL kami, kami memutuskan untuk membuat kode yang bisa kami buat terlebih dahulu, dan berusaha melakukan optimasi kemudian. Sebagai contoh, berikut adalah kode yang dibuat untuk melakukan insertion.
for i := range requirements {
requirements[i].CourseContentId = courseContent.ID
if err := tx.Create(&requirements[i]).Error; err != nil {
return err
}
}
Ini akan menjadi masalah karena satu kali insertion akan lebih berat, terlebih jika ORM yang kami gunakan ternyata memiliki optimasi untuk bulk insert, bukankah lebih baik untuk menggunakannya? Sehingga, kode di atas dapat diubah sebagai berikut:
for i := range requirements {
requirements[i].CourseContentId = courseContent.ID
}
if err := tx.Create(&requirements).Error; err != nil {
return err
}
Sehingga, insertion kami serahkan pada bulk insert milik gorm.
Sedikit Refleksi
Kami sendiri mengalami beberapa kendala dalam mengimplementasikan best practice tersebut. Bahkan, kami memiliki file test yang sudah mencapai 2000 baris dan berisi banyak sekali repetisi. Karena beberapa hal sudah terlanjur terbentuk seperti itu, kami merasa kesulitan untuk mengubahnya kembali, dan untuk saat ini kami merasa cukup saja untuk file test tersebut seperti itu. Salah satu hal yang kami juga lupa untuk atur dari awal adalah linter. Linter sebetulnya akan membantu mengurangi hal tersebut. Terutama, sudah ada tool seperti golangci-lint.
Yah, setidaknya kami mempelajari sesuatu dan kami akan berusaha lebih baik untuk proyek lain yang kami temui di masa depan 😅.
Penutup
Sekian yang dapat saya sampaikan. Semoga bermanfaat bagi kita semua. Selalu berusaha untuk terapkan best practice, karena yang kita kerjakan bukan hanya sekadar membuat kode, namun juga membuat kode yang bisa diubah dengan mudah di kemudian hari oleh orang lain. Sekian dan terima kasih 😄.
Referensi
- Slide asistensi mata kuliah Pemrograman Lanjut 2020/2021 Genap mengenai “Refactoring”.
- https://dtunkelang.medium.com/premature-optimization-is-still-the-root-of-all-evil-a3502c2ea262
- https://www.altexsoft.com/blog/engineering/code-refactoring-best-practices-when-and-when-not-to-do-it/