JavaRush /Java Blog /Random-ID /Prinsip SOLID yang akan membuat kode Anda lebih bersih
Paul Soia
Level 26
Kiyv

Prinsip SOLID yang akan membuat kode Anda lebih bersih

Dipublikasikan di grup Random-ID
Apa itu SOLID? Berikut singkatan dari SOLID: - S: Prinsip Tanggung Jawab Tunggal  . - O: Prinsip Terbuka-Tertutup  . - L: Prinsip Substitusi Liskov  . - I: Prinsip Pemisahan Antarmuka  . - D: Prinsip Pembalikan Ketergantungan  . Prinsip SOLID menyarankan cara merancang modul, yaitu batu bata dari mana aplikasi dibangun. Tujuan dari prinsip ini adalah untuk merancang modul yang: - mendorong perubahan - mudah dipahami - dapat digunakan kembali. Jadi, mari kita lihat apa yang dimaksud dengan modul tersebut beserta contohnya.
class MainRepository(
    private val auth: FirebaseAuth
) {
    suspend fun loginUser(email: String, password: String) {
        try {
            auth.signInWithEmailAndPassword(email, password)
        } catch (e: Exception) {
            val file = File("errors.txt")
            file.appendText(text = e.message.toString())
        }
    }
}
Apa yang kita lihat dalam contoh ini? FirebaseAuth diteruskan di konstruktor, dan kami menggunakannya untuk mencoba masuk. Jika kami menerima kesalahan sebagai respons, kami menulis pesan ke file. Sekarang mari kita lihat apa yang salah dengan kode ini. 1. S: Prinsip Tanggung Jawab Tunggal  : Sebuah kelas (atau fungsi/metode) harus bertanggung jawab hanya pada satu hal. Jika suatu kelas bertanggung jawab untuk menyelesaikan beberapa masalah, subsistemnya yang mengimplementasikan solusi untuk masalah tersebut akan terhubung satu sama lain. Perubahan pada salah satu subsistem akan menyebabkan perubahan pada subsistem lainnya. Dalam contoh kita, fungsi loginUser bertanggung jawab atas dua hal - login dan penulisan kesalahan ke file. Agar ada satu tanggung jawab, logika pencatatan kesalahan harus ditempatkan di kelas tersendiri.
class FileLogger{
    fun logError(error: String) {
        val file = File("errors.txt")
        file.appendText(text = error)
    }
}
class MainRepository(
    private val auth: FirebaseAuth,
    private val fileLogger: FileLogger,
) {
    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        } catch (e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }
}
Logika pencatatan kesalahan telah dipindahkan ke kelas terpisah dan sekarang metode loginUser hanya memiliki satu tanggung jawab - otorisasi. Jika logika error logging perlu diubah, maka kita akan melakukannya di kelas FileLogger, dan bukan di fungsi loginUser 2. O: Prinsip Terbuka-Tertutup  : Entitas perangkat lunak (kelas, modul, fungsi) harus terbuka untuk ekstensi. tapi tidak untuk modifikasi. Dalam konteks ini, keterbukaan terhadap ekstensi adalah kemampuan untuk menambahkan perilaku baru ke kelas, modul, atau fungsi jika diperlukan, dan ketertutupan terhadap perubahan adalah larangan mengubah kode sumber entitas perangkat lunak. Sekilas, hal ini terdengar rumit dan kontradiktif. Namun jika dicermati, prinsipnya cukup logis. Mengikuti prinsip OCP adalah perangkat lunak diubah bukan dengan mengubah kode yang sudah ada, tetapi dengan menambahkan kode baru. Artinya, kode yang awalnya dibuat tetap “utuh” dan stabil, dan fungsionalitas baru diperkenalkan baik melalui pewarisan implementasi atau melalui penggunaan antarmuka abstrak dan polimorfisme. Jadi mari kita lihat kelas FileLogger. Jika kita perlu menulis log ke file lain, kita dapat mengubah namanya:
class FileLogger{
    fun logError(error: String) {
        val file = File("errors2.txt")
        file.appendText(text = error)
    }
}
Tapi kemudian semua log akan ditulis ke file baru, yang kemungkinan besar tidak kita perlukan. Tapi bagaimana kita bisa mengubah logger kita tanpa mengubah kelas itu sendiri?
open class FileLogger{

    open fun logError(error: String) {
        val file = File("error.txt")
        file.appendText(text = error)
    }

}

class CustomErrorFileLogger : FileLogger() {

    override fun logError(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
Kami menandai kelas FileLogger dan fungsi logError sebagai terbuka, membuat kelas CustomErrorFileLogger baru dan menulis implementasi logging baru. Akibatnya, kelas kami tersedia untuk memperluas fungsionalitas, namun ditutup untuk modifikasi. 3. L: Prinsip Substitusi Liskov  . Subkelas harus dapat berfungsi sebagai pengganti superkelasnya. Tujuan dari prinsip ini adalah agar kelas turunan dapat digunakan sebagai pengganti kelas induk dari mana kelas tersebut diturunkan tanpa merusak program. Jika ternyata kode tersebut memeriksa tipe suatu kelas, maka prinsip substitusi dilanggar. Jika kita menulis kelas penerusnya seperti ini:
class CustomErrorFileLogger : FileLogger() {

    fun customErrorLog(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
Dan sekarang mari kita ganti kelas FileLogger dengan CustomErrorFileLogger di repositori
class MainRepository(
    private val auth: FirebaseAuth,
    private val fileLogger: CustomErrorFileLogger,
) {

    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        }catch(e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }

}
Dalam hal ini, logError akan dipanggil dari kelas induk, atau Anda harus mengubah panggilan ke fileLogger.logError(e.message.toString()) menjadi fileLogger.customErrorLog(e.message.toString()) 4. I : Prinsip Pemisahan Antarmuka. Buat antarmuka yang sangat terspesialisasi yang dirancang untuk klien tertentu. Klien tidak boleh bergantung pada antarmuka yang tidak mereka gunakan. Kedengarannya rumit, tapi sebenarnya sangat sederhana. Sebagai contoh, mari kita jadikan kelas FileLogger sebagai antarmuka, tetapi tambahkan satu fungsi lagi ke dalamnya:
interface FileLogger{

    fun printLogs()

    fun logError(error: String) {
        val file = File("errors.txt")
        file.appendText(text = error)
    }

}
Sekarang semua turunan akan diminta untuk mengimplementasikan fungsi printLogs, meskipun kita tidak memerlukannya di semua kelas turunan.
class CustomErrorFileLogger : FileLogger{

    override fun printLog() {

    }

    override fun logError(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
dan sekarang kita akan memiliki fungsi kosong, yang berdampak buruk bagi kebersihan kode. Sebagai gantinya, kita dapat membuat nilai default di antarmuka dan kemudian mengganti fungsi hanya di kelas-kelas yang memerlukannya:
interface FileLogger{

    fun printLogs() {
        //Howая-то дефолтная реализация
    }

    fun logError(error: String) {
        val file = File("errors.txt")
        file.appendText(text = error)
    }

}
class CustomErrorFileLogger : FileLogger{

    override fun logError(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
Sekarang kelas yang akan mengimplementasikan antarmuka FileLogger akan lebih bersih. 5. D: Prinsip Pembalikan Ketergantungan  . Objek ketergantungan harus berupa abstraksi, bukan sesuatu yang konkrit. Mari kita kembali ke kelas utama kita:
class MainRepository(
    private val auth: FirebaseAuth,
    private val fileLogger: FileLogger,
) {

    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        } catch (e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }

}
Tampaknya kami telah mengonfigurasi dan memperbaiki semuanya di sini. Namun masih ada satu hal lagi yang perlu diubah. Ini menggunakan kelas FirebaseAuth. Apa yang akan terjadi jika suatu saat kita perlu mengubah otorisasi dan masuk bukan melalui Firebase, tetapi, misalnya, menggunakan semacam permintaan API? Lalu kami harus mengubah banyak hal, dan kami tidak menginginkan hal itu. Untuk melakukan ini, buat antarmuka dengan fungsi signInWithEmailAndPassword(email: String, kata sandi: String):
interface Authenticator{
    fun signInWithEmailAndPassword(email: String, password: String)
}
Antarmuka ini adalah abstraksi kami. Dan sekarang kami membuat implementasi spesifik dari login
class FirebaseAuthenticator : Authenticator{

    override fun signInWithEmailAndPassword(email: String, password: String) {
        FirebaseAuth.getInstance().signInWithEmailAndPassword(email, password)
    }

}
class CustomApiAuthenticator : Authenticator{

    override fun signInWithEmailAndPassword(email: String, password: String) {
        //другой способ логина
    }

}
Dan di kelas `MainRepository` sekarang tidak ada ketergantungan pada implementasi spesifik, tetapi hanya pada abstraksi
class MainRepository (
    private val auth: Authenticator,
    private val fileLogger: FileLogger,
) {

    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        }catch(e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }

}
Dan sekarang, untuk mengubah metode otorisasi, kita hanya perlu mengubah satu baris di kelas modul.
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION