Skip to content

Instantly share code, notes, and snippets.

@phanatagama
Last active July 3, 2025 08:59
Show Gist options
  • Select an option

  • Save phanatagama/f612030f4a1d415fc06f0a01e3ff7225 to your computer and use it in GitHub Desktop.

Select an option

Save phanatagama/f612030f4a1d415fc06f0a01e3ff7225 to your computer and use it in GitHub Desktop.
SOLID Principle

SOLID Principle

SOLID adalah singkatan dari lima prinsip dasar dalam pengembangan perangkat lunak berorientasi objek yang bertujuan untuk membuat desain yang lebih mudah dipahami, fleksibel, dan dapat dipelihara. Prinsip-prinsip ini diperkenalkan oleh Robert C. Martin (Uncle Bob).

Kelima prinsip tersebut adalah:

  1. Single Responsibility Principle (SRP)
  2. Open/Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

1. Single Responsibility Principle (SRP)

Penjelasan: Sebuah kelas seharusnya hanya memiliki satu alasan untuk berubah. Ini berarti setiap kelas atau modul harus memiliki satu tanggung jawab atau satu fungsionalitas inti. Jika sebuah kelas memiliki lebih dari satu tanggung jawab, ia akan menjadi "God Object" yang sulit dipertahankan dan diuji.

Manfaat:

  • Peningkatan Kohesi: Kelas-kelas menjadi lebih terfokus pada tugas tunggal.
  • Pengurangan Coupling: Perubahan pada satu tanggung jawab tidak mempengaruhi tanggung jawab lain dalam kelas yang sama.
  • Kemudahan Pengujian: Kelas menjadi lebih mudah untuk diuji secara unit. Contoh Kode Kotlin (Pelanggaran SRP vs. Penerapan SRP):

Pelanggaran SRP:

// Pelanggaran SRP: Kelas User memiliki banyak tanggung jawab
class User {
    fun registerUser(username: String, email: String) {
        println("Registering user: $username")
        // Logika untuk menyimpan user ke database
    }

    fun sendWelcomeEmail(email: String) {
        println("Sending welcome email to: $email")
        // Logika untuk mengirim email
    }

    fun generateReport(userId: String) {
        println("Generating report for user: $userId")
        // Logika untuk membuat laporan
    }
}

fun main() {
    val user = User()
    user.registerUser("john_doe", "[email protected]")
    user.sendWelcomeEmail("[email protected]")
    user.generateReport("john_doe")
}

Pada contoh di atas, kelas User bertanggung jawab untuk:

Mendaftarkan pengguna Mengirim email Membuat laporan Jika ada perubahan dalam cara mendaftar pengguna, atau cara mengirim email, atau cara membuat laporan, kelas User harus diubah. Ini melanggar SRP. Penerapan SRP:

// Penerapan SRP: Memecah tanggung jawab ke kelas-kelas terpisah

class UserRepository {
    fun saveUser(username: String, email: String) {
        println("Saving user $username to database.")
        // Logika untuk menyimpan user ke database
    }
}

class EmailService {
    fun sendEmail(to: String, subject: String, body: String) {
        println("Sending email to $to with subject: $subject")
        // Logika untuk mengirim email
    }
}

class ReportGenerator {
    fun generateUserReport(userId: String) {
        println("Generating report for user: $userId")
        // Logika untuk membuat laporan
    }
}

// Kelas orkestrasi yang menggunakan kelas-kelas dengan tanggung jawab tunggal
class UserManager(
    private val userRepository: UserRepository,
    private val emailService: EmailService
) {
    fun registerAndNotifyUser(username: String, email: String) {
        userRepository.saveUser(username, email)
        emailService.sendEmail(email, "Welcome!", "Welcome to our service, $username!")
    }
}

fun main() {
    val userRepository = UserRepository()
    val emailService = EmailService()
    val reportGenerator = ReportGenerator()

    val userManager = UserManager(userRepository, emailService)

    userManager.registerAndNotifyUser("jane_doe", "[email protected]")
    reportGenerator.generateUserReport("jane_doe")
}

Di sini, kita memecah tanggung jawab menjadi kelas-kelas yang lebih kecil dan terfokus: UserRepository untuk penyimpanan data, EmailService untuk pengiriman email, dan ReportGenerator untuk pembuatan laporan. UserManager kemudian mengorkestrasi operasi-operasi ini, tetapi ia sendiri tidak memiliki tanggung jawab untuk menyimpan data atau mengirim email.

2. Open/Closed Principle (OCP)

Penjelasan: Entitas perangkat lunak (kelas, modul, fungsi, dll.) harus terbuka untuk ekstensi, tetapi tertutup untuk modifikasi. Ini berarti Anda harus dapat menambahkan fungsionalitas baru tanpa mengubah kode yang sudah ada dan telah teruji. Biasanya dicapai dengan menggunakan abstraksi (interface atau abstract class) dan polimorfisme.

Manfaat:

  • Stabilitas Kode: Mengurangi risiko kerusakan pada kode yang sudah berfungsi.
  • Fleksibilitas: Mudah menambahkan fitur baru tanpa mengubah yang lama.
  • Kemudahan Pemeliharaan: Perubahan hanya perlu dilakukan pada bagian baru yang ditambahkan. Contoh Kode Kotlin (Pelanggaran OCP vs. Penerapan OCP):

Pelanggaran OCP:

// Pelanggaran OCP: Perlu memodifikasi kelas Calculator setiap kali ada operasi baru
class Calculator {
    fun calculate(operation: String, a: Int, b: Int): Int {
        return when (operation) {
            "add" -> a + b
            "subtract" -> a - b
            "multiply" -> a * b
            else -> throw IllegalArgumentException("Unknown operation")
        }
    }
}

fun main() {
    val calculator = Calculator()
    println("5 + 3 = ${calculator.calculate("add", 5, 3)}")
    println("5 - 3 = ${calculator.calculate("subtract", 5, 3)}")
    // Jika ingin menambahkan operasi "divide", kita harus memodifikasi kelas Calculator
}

Pada contoh di atas, jika kita ingin menambahkan operasi pembagian, kita harus memodifikasi metode calculate di kelas Calculator. Ini melanggar OCP.

Penerapan OCP:

// Penerapan OCP: Menggunakan interface untuk ekstensi

interface Operation {
    fun apply(a: Int, b: Int): Int
}

class Addition : Operation {
    override fun apply(a: Int, b: Int): Int = a + b
}

class Subtraction : Operation {
    override fun apply(a: Int, b: Int): Int = a - b
}

class Multiplication : Operation {
    override fun apply(a: Int, b: Int): Int = a * b
}

// Kelas baru untuk operasi pembagian, tanpa mengubah yang sudah ada
class Division : Operation {
    override fun apply(a: Int, b: Int): Int {
        require(b != 0) { "Cannot divide by zero" }
        return a / b
    }
}

class NewCalculator {
    fun calculate(operation: Operation, a: Int, b: Int): Int {
        return operation.apply(a, b)
    }
}

fun main() {
    val calculator = NewCalculator()

    val add = Addition()
    val subtract = Subtraction()
    val multiply = Multiplication()
    val divide = Division() // Menambahkan operasi baru tanpa mengubah kelas Calculator

    println("5 + 3 = ${calculator.calculate(add, 5, 3)}")
    println("5 - 3 = ${calculator.calculate(subtract, 5, 3)}")
    println("5 * 3 = ${calculator.calculate(multiply, 5, 3)}")
    println("6 / 3 = ${calculator.calculate(divide, 6, 3)}")
}

Dengan menggunakan interface Operation, kita dapat menambahkan operasi baru (seperti Division) hanya dengan membuat kelas baru yang mengimplementasikan Operation, tanpa perlu memodifikasi kelas NewCalculator. Ini adalah contoh yang baik dari OCP.

3. Liskov Substitution Principle (LSP)

Penjelasan: Objek-objek dalam sebuah program harus dapat digantikan dengan subtipe-nya tanpa mengubah kebenaran program. Ini berarti jika kelas B adalah subtipe dari kelas A, maka kita harus dapat menggunakan objek dari kelas B di mana pun objek dari kelas A diharapkan, tanpa merusak perilaku program. Jika memiliki beberapa instance dengan funsionalitas yang mirip, buatlah sebuah interface

Manfaat:

  • Peningkatan Reusability: Memastikan bahwa hierarki pewarisan berfungsi dengan benar.
  • Prediktabilitas Kode: Memastikan bahwa subtipe berperilaku seperti yang diharapkan dari tipe dasarnya.
  • Memfasilitasi Ekstensi: Memungkinkan penambahan subtipe baru tanpa memengaruhi kode yang ada. Contoh Kode Kotlin (Pelanggaran LSP vs. Penerapan LSP):

Pelanggaran LSP:

// Pelanggaran LSP: Square tidak selalu bisa menggantikan Rectangle dengan aman
open class Rectangle {
    open var width: Int = 0
    open var height: Int = 0

    open fun setDimensions(width: Int, height: Int) {
        this.width = width
        this.height = height
    }

    fun getArea(): Int = width * height
}

class Square : Rectangle() {
    // Override setter untuk memastikan width dan height selalu sama
    override var width: Int = 0
        set(value) {
            field = value
            super.height = value // Pelanggaran: Mengubah height saat width diatur
        }

    override var height: Int = 0
        set(value) {
            field = value
            super.width = value // Pelanggaran: Mengubah width saat height diatur
        }

    override fun setDimensions(width: Int, height: Int) {
        // Square mengharapkan width dan height sama
        require(width == height) { "Width and height must be equal for a square" }
        this.width = width
        this.height = height
    }
}

fun calculateArea(rectangle: Rectangle) {
    rectangle.setDimensions(4, 5) // Harapannya width=4, height=5
    println("Area: ${rectangle.getArea()}")
}

fun main() {
    val rect = Rectangle()
    calculateArea(rect) // Output: Area: 20

    val square = Square()
    // calculateArea(square) // Ini akan gagal karena setDimensions dari Square mengharapkan width == height
    // Jika kita paksa, perilakunya aneh:
    square.setDimensions(4,4) //OK
    println("Square area (4,4): ${square.getArea()}") // Output: 16

    // Ini menunjukkan masalah substitusi:
    val badSquare: Rectangle = Square()
    badSquare.width = 4 // Ini akan membuat height jadi 4 juga
    badSquare.height = 5 // Ini akan membuat width jadi 5 juga
    println("Area after bad substitution: ${badSquare.getArea()}") // Output: 25, padahal kita set width=4, height=5
}

Pada contoh di atas, Square mewarisi dari Rectangle. Namun, Square mengubah perilaku setDimensions dan width/height setter-nya sehingga tidak lagi dapat digantikan dengan aman di tempat Rectangle diharapkan. Misalnya, jika kita mencoba calculateArea dengan Square, itu akan gagal atau memberikan hasil yang tidak terduga.

Penerapan LSP: Untuk mematuhi LSP, kita perlu mendefinisikan abstraksi yang benar-benar bisa digantikan. Kita bisa mendefinisikan interface yang lebih umum untuk bentuk yang dapat menghitung luas, dan kemudian kelas-kelas yang berbeda mengimplementasikannya.

// Penerapan LSP: Menggunakan abstraksi yang tepat

interface Shape {
    fun getArea(): Int
}

class RectangleLSP(var width: Int, var height: Int) : Shape {
    override fun getArea(): Int = width * height
}

class SquareLSP(var side: Int) : Shape {
    override fun getArea(): Int = side * side
}

fun printArea(shape: Shape) {
    println("Area: ${shape.getArea()}")
}

fun main() {
    val rect = RectangleLSP(4, 5)
    printArea(rect) // Output: Area: 20

    val square = SquareLSP(4)
    printArea(square) // Output: Area: 16

    // Keduanya dapat digantikan oleh Shape tanpa masalah.
    val shapes: List<Shape> = listOf(RectangleLSP(2, 3), SquareLSP(5))
    shapes.forEach { printArea(it) }
}

Dalam contoh ini, baik RectangleLSP maupun SquareLSP mengimplementasikan Shape dan dapat menghitung luasnya sendiri. Karena keduanya mematuhi kontrak Shape (getArea()), mereka dapat digantikan satu sama lain dengan aman di mana pun Shape diharapkan.

4. Interface Segregation Principle (ISP)

Penjelasan: Klien tidak boleh dipaksa untuk mengimplementasikan antarmuka yang tidak mereka gunakan. Daripada satu antarmuka yang besar (fat interface), lebih baik memiliki banyak antarmuka kecil dan spesifik. Jika memiliki dua fungsi yang berbeda jangan dijadikan satu interface, buatlah interface terpisah

Manfaat:

  • Mengurangi Kode yang Tidak Perlu: Klien hanya perlu mengimplementasikan metode yang benar-benar relevan bagi mereka.
  • Peningkatan Reusability: Antarmuka yang lebih kecil lebih mudah digabungkan dan digunakan kembali.
  • Pengurangan Coupling: Perubahan pada satu bagian antarmuka yang tidak digunakan klien tidak akan memengaruhi klien tersebut. Contoh Kode Kotlin (Pelanggaran ISP vs. Penerapan ISP):

Pelanggaran ISP:

// Pelanggaran ISP: Interface besar yang tidak semua kelas butuhkan semua metodenya
interface Worker {
    fun work()
    fun eat()
    fun sleep()
    fun manageTeam() // Tidak semua worker memiliki tanggung jawab ini
    fun codeReview() // Tidak semua worker melakukan ini
}

class Developer : Worker {
    override fun work() { println("Developer is coding.") }
    override fun eat() { println("Developer is eating lunch.") }
    override fun sleep() { println("Developer is sleeping.") }
    override fun manageTeam() { /* Developer tidak melakukan ini, jadi metode kosong atau NotImplemented */ }
    override fun codeReview() { println("Developer is doing code review.") }
}

class Robot : Worker {
    override fun work() { println("Robot is assembling products.") }
    override fun eat() { /* Robot tidak makan, jadi metode kosong */ }
    override fun sleep() { /* Robot tidak tidur, jadi metode kosong */ }
    override fun manageTeam() { /* Robot tidak melakukan ini */ }
    override fun codeReview() { /* Robot tidak melakukan ini */ }
}

fun main() {
    val dev = Developer()
    dev.work()
    dev.eat()

    val robot = Robot()
    robot.work()
    // robot.eat() // Memanggil metode kosong yang tidak relevan
}

Pada contoh di atas, Worker adalah antarmuka yang terlalu besar. Developer tidak mengelola tim, dan Robot tidak makan atau tidur. Mereka dipaksa untuk mengimplementasikan metode yang tidak relevan bagi mereka.

Penerapan ISP:

// Penerapan ISP: Memecah interface besar menjadi interface yang lebih kecil

interface Workable {
    fun work()
}

interface Eatable {
    fun eat()
}

interface Sleepable {
    fun sleep()
}

interface Manageable {
    fun manageTeam()
}

interface Reviewable {
    fun codeReview()
}

class DeveloperISP : Workable, Eatable, Sleepable, Reviewable {
    override fun work() { println("Developer is coding.") }
    override fun eat() { println("Developer is eating lunch.") }
    override fun sleep() { println("Developer is sleeping.") }
    override fun codeReview() { println("Developer is doing code review.") }
}

class TeamLead : Workable, Eatable, Sleepable, Manageable, Reviewable {
    override fun work() { println("Team Lead is planning sprints.") }
    override fun eat() { println("Team Lead is eating lunch.") }
    override fun sleep() { println("Team Lead is sleeping.") }
    override fun manageTeam() { println("Team Lead is managing the team.") }
    override fun codeReview() { println("Team Lead is doing code review.") }
}

class RobotISP : Workable {
    override fun work() { println("Robot is assembling products.") }
}

fun main() {
    val dev = DeveloperISP()
    dev.work()
    dev.eat()
    dev.codeReview()

    val robot = RobotISP()
    robot.work()
    // robot.eat() // Tidak bisa memanggil, karena Robot tidak mengimplementasikan Eatable

    val teamLead = TeamLead()
    teamLead.manageTeam()
    teamLead.codeReview()
}

Sekarang, setiap kelas hanya mengimplementasikan antarmuka yang benar-benar relevan dengan tanggung jawabnya. RobotISP hanya mengimplementasikan Workable, DeveloperISP mengimplementasikan Workable, Eatable, Sleepable, dan Reviewable, dan TeamLead mengimplementasikan semua yang relevan. Ini jauh lebih bersih dan sesuai dengan ISP.

5. Dependency Inversion Principle (DIP)

Penjelasan: Modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah. Keduanya harus bergantung pada abstraksi. Abstraksi tidak boleh bergantung pada detail. Detail harus bergantung pada abstraksi. Ini berarti kita harus bergantung pada antarmuka atau kelas abstrak daripada implementasi konkret. Hal ini sering dicapai dengan Dependency Injection. Jika sebuah kelas Repository membutuhkan suatu instance database, jangan langsung diberikan instancenya. Tapi buatlah sebuah interface dan tambahkan tipe interface tersebut pada constructor kelas Repository. Jadi kita bisa mengganti database lebih mudah dari Mysql, PostgreSql atau pun mongoDB karena memiliki tipe interface database yang sama

Manfaat:

  • Pengurangan Coupling: Memisahkan modul-modul sehingga perubahan pada satu modul tidak memengaruhi modul lainnya secara langsung.
  • Peningkatan Fleksibilitas: Memungkinkan penggantian implementasi dengan mudah.
  • Kemudahan Pengujian: Memungkinkan pengujian unit yang lebih mudah dengan menggunakan mock atau stub untuk dependensi. Contoh Kode Kotlin (Pelanggaran DIP vs. Penerapan DIP):

Pelanggaran DIP:

// Pelanggaran DIP: Modul tingkat tinggi (ReportGenerator) bergantung pada modul tingkat rendah (MySQLDatabase)
class MySQLDatabase {
    fun getData(): List<String> {
        println("Getting data from MySQL database.")
        return listOf("Data 1", "Data 2", "Data 3")
    }
}

class ReportGenerator {
    private val database = MySQLDatabase() // Ketergantungan langsung pada implementasi konkret

    fun generateReport() {
        val data = database.getData()
        println("Generating report with data: $data")
    }
}

fun main() {
    val reportGenerator = ReportGenerator()
    reportGenerator.generateReport()
    // Jika ingin mengganti database (misalnya ke PostgreSQL), kita harus memodifikasi kelas ReportGenerator.
}

Pada contoh di atas, ReportGenerator (modul tingkat tinggi) secara langsung bergantung pada MySQLDatabase (modul tingkat rendah). Jika kita ingin mengganti jenis database, kita harus memodifikasi ReportGenerator.

Penerapan DIP:

// Penerapan DIP: Ketergantungan pada abstraksi (interface)

interface Database {
    fun fetchData(): List<String>
}

class MySQLDatabaseDIP : Database {
    override fun fetchData(): List<String> {
        println("Fetching data from MySQL database.")
        return listOf("MySQL Data 1", "MySQL Data 2", "MySQL Data 3")
    }
}

class PostgreSQLDatabaseDIP : Database {
    override fun fetchData(): List<String> {
        println("Fetching data from PostgreSQL database.")
        return listOf("PostgreSQL Data A", "PostgreSQL Data B", "PostgreSQL Data C")
    }
}

class ReportGeneratorDIP(private val database: Database) { // Injeksi dependensi melalui konstruktor
    fun generateReport() {
        val data = database.fetchData()
        println("Generating report with data: $data")
    }
}

fun main() {
    // Menggunakan MySQL
    val mySQLDatabase = MySQLDatabaseDIP()
    val mySQLReportGenerator = ReportGeneratorDIP(mySQLDatabase)
    mySQLReportGenerator.generateReport()

    println("\n--- Switching database ---\n")

    // Menggunakan PostgreSQL tanpa mengubah ReportGeneratorDIP
    val postgreSQLDatabase = PostgreSQLDatabaseDIP()
    val postgreSQLReportGenerator = ReportGeneratorDIP(postgreSQLDatabase)
    postgreSQLReportGenerator.generateReport()
}

Dengan penerapan DIP, ReportGeneratorDIP sekarang bergantung pada antarmuka Database daripada implementasi konkret. Implementasi Database yang sebenarnya (misalnya MySQLDatabaseDIP atau PostgreSQLDatabaseDIP) diinjeksikan ke ReportGeneratorDIP melalui konstruktor. Ini memungkinkan kita untuk dengan mudah mengganti database tanpa memodifikasi kode ReportGeneratorDIP, yang sangat meningkatkan fleksibilitas dan kemampuan pengujian.

Kesimpulan Menerapkan prinsip-prinsip SOLID dalam pengembangan perangkat lunak membantu menciptakan kode yang:

  • Mudah Dipelihara: Perubahan pada satu bagian kode cenderung tidak memengaruhi bagian lain.
  • Fleksibel: Lebih mudah untuk menambahkan fitur baru atau mengubah perilaku tanpa merusak yang sudah ada.
  • Dapat Diuji: Komponen-komponen menjadi lebih terisolasi dan mudah diuji secara unit.
  • Dapat Dipahami: Kode menjadi lebih terstruktur dan logis. Meskipun membutuhkan usaha lebih di awal, manfaat jangka panjangnya dalam proyek yang besar dan kompleks sangat signifikan.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment