Dalam membuat sebuah aplikasi menggunakan paradigma pemrograman berbasis objek, sering kali kita menerapkan kode seadanya, yang penting bisa jalan dan sesuai kaidah.

Tapi pernahkah kalian berfikir untuk membuat kode yang mudah dimengerti, dikelola, dan dikembangkan. Didalam pemrograman berbasis objek terdapat prinsip bernama SOLID, yang bertujuan untuk membantu kalian dalam membuat program yang mudah pahami dan dikelola.

Penasaran tentang prinsip SOLID ini? mari kita bahas lebih dalam.


Apa itu S.O.L.I.D ?


SOLID adalah sebuah akronim dari lima prinsip object-oriented design (OOD) oleh Robert C. Martin. Prinsip ini biasa diterapkan pada saat berkecimpung dalam pemrograman berorientasi objek.

Kelima prinsip ini adalah praktek dalam mengembangkan sebuah program dengan mempertimbangkan pemeliharaan serta pengembangan lebih lanjut agar kode mudah dirawat, mudah dimengerti serta fleksibel.

Mengadopsi prinsip ini dapat membantu kalian dalam menghindari bad code, membantu dalam refactoring code serta pengembangan aplikasi secara Agile atau Adaptive.

Singkatan dari SOLID sendiri adalah :

  • S - Solid-Responsibility Principle
  • O - Open-Closed Principle
  • L - Liskov Substitution Principle
  • I - Interface Segregation Principle
  • D - Dependency Inversion Principle

Nah, sekarang mari kita bahas satu setiap prinsip pada SOLID dengan contoh penerapannya menggunakan bahasa Python.

Walaupun anbi pakai bahasa Python, tapi prinsip ini bisa dipakai untuk bahasa mana saja.


1. Single-Responsibility Principle


Pada prinsip yang pertama, menyatakan bahwa :

Setiap class hanya memiliki satu tugas, sehingga alasan untuk merubah class tersebut hanya satu, yaitu merubah tugas yang diberikan kepadanya.

Contoh, kita buat aplikasi untuk mencari luas dari bentuk bangun datar seperti Lingkaran atau Persegi.

Lalu, kita buat class sesuai masing - masing bangunan dan sebuah fungsi constructor untuk menyiapkan parameter yang diperlukan.

Nah, untuk class Lingkaran kode nya seperti ini:

class Lingkaran:

    def __init__(self, radius):
        self.radius = radius

Sedangkan untuk class Persegi kodenya kita buat seperti ini:

class Persegi:

    def __init__(self, panjang):
        self.panjang = panjang

Selanjutnya kita akan membuat class Kalkulasi yang memiliki tugas untuk mengkalkulasi luas dari bangun datar yang tersedia.

class Kalkulasi:

    def __init__(self, *args):
        self.bangunDatar = args

    def calculate(self):
        listLuas = []
        for objek in self.bangunDatar:
            luas = 0
            if type(objek).__name__ == 'Persegi':
                luas = math.pow(objek.panjang, 2)
            elif type(objek).__name__ == 'Lingkaran':
                luas = math.pi * math.pow(objek.radius, 2)
            listLuas.append(luas)

        return sum(listLuas)

    def output(self):
        return self.calculate()

Untuk menggunakan class Kalkulasi, kamu perlu menginisialisasi kelas tersebut dan berikan beberapa array dari object lingkaran atau persegi.

Contohnya seperti ini :

  • Sebuah Lingkaran yang memiliki radius 2
  • Sebuah Persegi yang memiliki panjang 5
  • Sebuah Persegi lain yang memiliki panjang 6
if __name__ == "__main__":

    result = Kalkulasi(
        Lingkaran(2),
        Persegi(5),
        Persegi(6)
    )

    print("Jumlah luas dari bangunan ", result.output())

Output :

Jumlah luas dari bangunan  73.56637061435917

Masalah dengan metode ini adalah class Kalkulasi hanya menangani logika untuk satu jenis output dari hasil tersebut.

Contoh, jika kita ingin outputnya dalam bentuk JSON, atau kita ingin outputnya dalam bentuk yang lain.

Jika kita menempatkan logika untuk mengubah bentuk output pada class Kalkulasi, ini akan menyalahi prinsip Single-Responsibility.

Karena class Kalkulasi hanya dikhususkan menangani penjumlahan dari kumpulan bangun datar, ia tidak peduli outputnya akan seperti apa, mau itu JSON atau HTML.

Untuk mengatasi hal ini, kita bisa membuat class terpisah bernama OutputKalkulasi yang menangani logika untuk bentuk output yang diinginkan.

class OutputKalkulasi:

    def __init__(self, kalkulasi):
        self.kalkulasi = kalkulasi

    def toJSON(self):
        result = {
            "result" : self.kalkulasi.output()
        }
        return json.dumps(result)

    def toText(self):
        with open('result.txt', 'w') as f:
            f.write(self.kalkulasi.output())

    def toBase64(self):
        result = base64.encode(self.kalkulasi.output())
        return result

Mari kita implementasikan

if __name__ == "__main__":

    result = Kalkulasi(
        Lingkaran(2),
        Persegi(5),
        Persegi(6)
    )

    print("Jumlah luas dari bangunan ", result.output())

    calc_output = OutputKalkulasi(result)
    print("Dalam Bentuk JSON : ", calc_output.toJSON())

Dan ini adalah outputnya

Jumlah luas dari bangunan  73.56637061435917
Dalam Bentuk JSON :  {"result": 73.56637061435917}

Sekarang, logic untuk menangani bentuk dari output data sudah ditangani oleh class OutputKalkulasi, Dengan tetap menggunakan prinsip Single-Responsibility.


2. Open-Closed Principle


Open-Close Principle adalah prinsip yang menyatakan bahwa Objek atau entitas itu terbuka untuk ekstensi tetapi tertutup untuk modifikasi.

Singkatnya, sebuah class harus bisa dikembangkan tanpa memodifikasi kelas itu sendiri.

Mari kita kembali ke kode kelas Kalkulasi, sekarang kita fokus pada fungsi calculate.

class Kalkulasi:

    def __init__(self, *args):
        self.bangunDatar = args

    def calculate(self):
        listLuas = []
        for objek in self.bangunDatar:
            luas = 0
            if type(objek).__name__ == 'Persegi':
                luas = math.pow(objek.panjang, 2)
            elif type(objek).__name__ == 'Lingkaran':
                luas = math.pi * math.pow(objek.radius, 2)
            listLuas.append(luas)

        return sum(listLuas)

    def output(self):
        return self.calculate()

Misal kita ingin menambahkan tipe bangun datar yang lain, seperti hexagonal, segitiga atau jajar genjang.

Otomatis kita akan terus mengedit kelas Kalkulasi dan menambahkan if/else untuk setiap tipe bangun datar. Ini menyalahi prinsip Open-Closed Principle.

Maka, untuk membuat method calculate ini lebih baik adalah dengan menghapus logic if/else dan untuk setiap kelas bangun datar, kita buat satu fungsi khusus untuk menghitung luas.

Contoh, fungsi luas untuk menghitung kelas Persegi:

class Persegi:

    def __init__(self, panjang):
        self.panjang = panjang

    def luas(self):
        return pow(self.panjang, 2)

Lalu untuk kelas Lingkaran kita tambahkan fungsi luas seperti ini :

class Lingkaran:

    def __init__(self, radius):
        self.radius = radius

    def luas(self):
        return math.pi * pow(self.radius, 2)

Sehingga fungsi calculate bisa kita tulis ulang seperti ini :

class Kalkulasi:
    # ...

    def calculate(self):
        listLuas = []
        for objek in self.bangunDatar:
            luas = objek.luas()
            listLuas.append(luas)

        return sum(listLuas)

    # ...

Dengan begini kita bisa membuat class tipe bangun datar yang lain tanpa harus mengubah kode pada class Kalkulasi

Tetapi ada permasalahan yang lain. Bagaimana cara kita mengetahui bahwa objek yang diberikan ke kelas Kalkulasi adalah objek tipe bangun datar serta memiliki fungsi luas()?.

Solusinya adalah dengan membuat interface, karena ini adalah bagian dari SOLID.

Buat sebuah kelas interface dengan nama BangunDatar :

class BangunDatar:

    def luas():
        pass

Mari kita modifikasi kelas yang lain untuk mengimplementasikan interface :

Untuk yang kelas Persegi kita update seperti ini :

class Persegi(BangunDatar):
    # ...

Dan juga untuk yang kelas Lingkaran kita update seperti ini :

class Persegi(Interface):
    # ...

Lalu untuk fungsi calculate di kelas Kalkulasi kita tambahkan validasi apakah objek yang diberikan ini adalah implementasi dari interface kelas BangunDatar atau tidak. Jika tidak maka kita throw error.

class Kalkulasi:
    # ...

    def calculate(self):
        listLuas = []
        for objek in self.bangunDatar:
            if isinstance(objek, BangunDatar):
                luas = objek.luas()
                listLuas.append(luas)
                continue

            raise Exception("This Object (",object,") Not Implementation of BangunDatar")

    # ...

Dengan begitu prinsip Open-Closed kita terapkan.


3.Liskov Substiution Principle


Liskov-Substitution adalah prinsip yang menyatakan bahwa ketika terdapat kelas X yang diturunin dari class Y, maka objek yang berasal dari kelas Y harus bisa menggantikan dengan objek dari kelas X.

Singkatnya, setiap kelas turunan harus bisa menjadi pengganti untuk base class atau kelas parent nya.

Contoh paling sederhananya seperti ini :

Bad Example

class Burung:

    def __init__(self):
        pass

    def terbang(self):
        ''' Logic untuk terbang '''
        return True


class Elang(Burung):

    def __init__(self):
        pass

Berdasarkan skenario diatas sudah benar jika Elang bisa terbang karena turunan dari Burung. Tapi bagaimana dengan skenario ini :

class Penguin(Burung):

    def __init__(self):
        pass

Oke, memang benar Penguin itu Burung, tetapi apakah penguin bisa terbang? Tidak. Maka dari itu, penguin tidak bisa menggunakan method terbang(), yang berarti kita merusak prinsip LSP.

Good Example

Sehingga untuk memperbaikinya, kita ubah sedikit skenario dalam kode tersebut. Dengan membedakan jenis burung setiap class.

class Burung:

    def __init__(self):
        pass

class BurungTerbang(Burung):

    def __init__(self):
        pass

    def terbang(self):
        ''' Logic untuk terbang '''
        return True

Nah, lalu implementasinya untuk setiap tipe burung, bisa kita bedakan seperti ini :

class Elang(BurungTerbang):
    def __init__(self):
        pass

class Penguin(Burung):
    def __init__(self):
        pass

Kira - kira contoh implementasi prinsip Liskov Substitution seperti itu.


4. Interface Segregation Principle


Interface Segragation adalah prinsip yang menyatakan bahwa sebuah objek client tidak boleh dipaksa untuk mengimplementasikan sebuah interface yang tidak ia gunakan. Atau objek client tidak boleh bergantung pada metode yang tidak ia gunakan.

Sebuah interface tidak boleh menyediakan semua service untuk client objek. Satu interface hanya memiliki satu tugas spesifik untuk tiap clientnya.

Mari kita kembali ke kode kelas interface BangunDatar. Sekarang kebutuhan kita semakin berkembang, dan ingin menambahkan tipe bangun yang lain seperti Kubus dan Tabung dimana yang dicari adalah volume.

Coba kita lihat apa yang terjadi jika kita memodifikasi interface BangunDatar sesuai dengan skenario diatas.

class BangunDatar:

    def luas():
        pass

    def volume():
        pass

Nah, yang terjadi adalah setiap class tipe bangunan harus mengimplementasikan method volume(). Padahal, bangun datar itu tidak memiliki volume. Sehingga kelas Persegi akan dipaksa untuk mengimplementasikan method volume(), kan lucu, gaada gunanya.

Secara koding, itu benar, tetapi secara prinsip dan konsep itu melanggar Interface Segregation. Maka lebih baiknya kita buat interface lain, yang memiliki sifat berbeda dengan ini.

class BangunDatar:

    def luas():
        pass

class BangunRuang:

    def volume():
        pass

class Persegi(BangunDatar):

    def __init__(self, panjang):
        self.panjang = panjang

    def luas(self):
        return pow(self.panjang, 2)

class Kubus(BangunDatar, BangunRuang):

    def __init__(self, sisi):
        self.sisi = sisi

    def luas(self):
        return pow(self.sisi, 2)

    def volume(self):
        return pow(self.sisi, 3)

Nah, jika seperti ini mungkin lebih masuk akal dalam implementasi skenario awal. Sekarang mari kita ubah sedikit, agar kita bisa menyesuaikan kalkulasi di masing - masing tipe bangun datar atau bangun ruang dengan kelas Kalkulasi kita.

Penyesuaiannya kita lakukan dengan cara menambah kelas KelolaKalkulasi untuk menjembatani kelas - kelas bangun datar atau ruang dengan kelas Kalkulasi.

class KelolaKalkulasi:
    def calculate(self):
        pass

class Persegi(BangunDatar, KelolaKalkulasi):
    def __init__(self, panjang):
        self.panjang = panjang

    def luas(self):
        return pow(self.panjang, 2)

    def calculate(self):
        return self.luas()

class Kubus(BangunDatar, BangunRuang, KelolaKalkulasi):
    def __init__(self, sisi):
        self.sisi = sisi

    def luas(self):
        return pow(self.sisi, 2)

    def volume(self):
        return pow(self.sisi, 3)

    def calculate(self):
        return self.luas()

Sekarang untuk kelas Kalkulasi, kita ubah sedikit kode pada fungsi calculate supaya semua kompenen yang kita buat diatas kembali terhubung.

class Kalkulasi:

    def __init__(self, *args):
        self.bangunDatar = args

    def calculate(self):
        listLuas = []
        for objek in self.bangunDatar:
            if isinstance(objek, BangunDatar):
                luas = objek.calculate()
                listLuas.append(luas)
                continue

            raise Exception("This Object (",object,") Not Implementation of BangunDatar")

        return sum(listLuas)

    def output(self):
        return self.calculate()

Yaps, dengan ini penerapan prinsip interface segregation sudah sempurna.


5. Dependency Inversion Principle


Dependency Inversion adalah prinsip yang menyetakan bahwa sebuah entitas itu bergantung pada abstraksi. Sehingga sebuah modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah, tetapi bergantung kepada abstraksi.

Maksud dari level-rendah dan level-tinggi disini adalah :

  • modul level-tinggi (high-level) adalah interface atau abstraksi yang dikonsumsi secara langsung oleh kelas - kelas eksekutor atau user interface.

  • modul level-rendah (low-level) adalah kumpulan dari beberapa modul kecil (subsystem) yang membantu modul level-tinggi dalam proses pekerjaannya.

Prinsip ini mengajarkan untuk memisahkan setiap modul agar dapat independen.

Oke kita langsung aja ke contohnya. Dibawah ini ada kelas bernama ModelApp yang terkoneksi dengan database MySQL :

class MySQLConnection:

    def connect(self):
        # Logic untuk menghandle database connection
        return "Database Connection"


class ModelApp:

    def __init__(self, connection : MySQLConnection):
        # self connection
        self.connection = connection

Oke, yang pertama kelas MySQLConnection termasuk modul level-rendah, sedangkan kelas ModelApp adalah modul dengan level-tinggi.

Menurut prinsip Dependency Inversion konsep diatas menyalahi aturan, karena modul dengan level-tinggi() ModelApp) dipaksa untuk bergantung pada modul dengan level-rendah (MySQLConnection).

Bagaimana jika kita ingin mengganti koneksinya ke database yang lain seperti DB2 atau Oracle? Harus kalian ganti juga kodenya, kalau kalian ingin mengganti koneksinya. Inilah kenapa skenario diatas tidak memenuhi Dependency Inversion.

Maka dari itu, hal yang paling tepat adalah menambahkan sebuah abstraksi, dengan tujuan modul level tinggi tidak lagi bergantung dengan modul level-rendah, disini kita namakan dengan kelas Koneksi.

class Koneksi:

    def connect():
        pass

Abstraksi / Interface ini akan kita implementasikan ke modul MySQLConnection, serta menjadi argument untuk modul ModelApp.

Sehingga apapun koneksi databasenya, kita bisa mudah menyesuaikanya tanpa harus mengganti kode inti dan menyalahi prinsip Open-Close.

class Koneksi:

    def connect():
        pass

class MySQLConnection(Koneksi):

    def connect():
        # Logic untuk menghandle database connection
        return "Database Connection"

class ModelApp:

    def __init__(self, connection : Koneksi):
        # self connection
        self.connection = connection

Lihat, kode diatas membuat modul level-tinggi tidak lagi bergantung pada modul level-rendah, tetapi langsung bergantung pada Abstraksi / Interface.