Back to blog
May 10, 2023
7 min read

SOLID Principle and its Implementation

Explaining SOLID principle and their example.

Membuat kode yang maintainable merupakan sebuah tantangan bagi seorang software developer. Kode yang maintainable akan memudahkan proses development baik untuk sekarang maupun di masa depan. Pada artikel ini, akan dibahas pembuatan kode yang maintainable dengan menerapkan SOLID principle.

SOLID Principle

SOLID adalah kumpulan prinsip desain software yang diusung oleh Robert C Martin. SOLID bertujuan untuk membantu developer dalam menciptakan code yang mudah untuk di-develop dan di-maintain. SOLID terdiri dari 5 prinsip yaitu

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

Untuk membantu dalam memahami SOLID principle, artikel ini akan membahas setiap prinsip SOLID beserta dengan contoh penerapannya

Single Responsibility Principle

Prinsip ini menyatakan bahwa sebuah class (dan function) hendaknya memiliki satu dan hanya satu tanggung jawab. Dengan kata lain, sebuah class (dan function) hendaknya hanya memiliki satu alasan untuk berubah.

def get_loan_product_list(
        self,
        product_id: str = None,
        product_name: str = None,
        description: str = None,
        category: str = None,
        create_date_start: str = None,
        create_date_end: str = None,
        update_date_start: str = None,
        update_date_end: str = None
):
    query = Q()
    dateformat = '%Y-%m-%d'
    if product_id:
        query &= Q(id__icontains=product_id)
    if product_name:
        query &= Q(name__icontains=product_name)
    if description:
        query &= Q(description__icontains=description)
    if category:
        query &= Q(category__icontains=category)
    try:
        if create_date_start:
            create_date_start = datetime.strptime(create_date_start, dateformat)
            query &= Q(create_date__gte=create_date_start)
        if create_date_end:
            create_date_end = datetime.strptime(create_date_end, dateformat) + timedelta(days=1)
            query &= Q(create_date__lte=create_date_end)
        if update_date_start:
            update_date_start = datetime.strptime(update_date_start, dateformat)
            query &= Q(update_date__gte=update_date_start)
        if update_date_end:
            update_date_end = datetime.strptime(update_date_end, dateformat) + timedelta(days=1)
            query &= Q(update_date__lte=update_date_end)
    except ValueError:
        raise HttpError(400, "Invalid date format. Use YYYY-MM-DD.")

    loan_products = LoanProduct.objects.filter(query).annotate(
        created_date=Cast(TruncMinute("create_date"), CharField()),
        updated_date=Cast(TruncMinute("update_date"), CharField())
    )
    return list(loan_products)

Untuk melihat penerapannya, perhatikan kode diatas. Function pada kode diatas merepresentasikan API get loan product list. Function tersebut juga bertanggung jawab terhadap proses filter dari loan product list. Hal tersebut menyalahi single responsibility principle karena jika kita ingin mengubah mekanisme dari filter (menambah atribut filter, dsb) maka kita harus mengubah function get loan product list.

Kita dapat mengatasi hal tersebut dengan memisahkan filter dari function get loan product list. Sehingga jika kita ingin mengubah mekanisme filter, kita hanya perlu mengubah function filter. Berikut contoh solusinya

def get_filtered_objects(model, query, dateformat, **kwargs):
    try:
        if kwargs.get('create_date_start'):
            create_date_start = datetime.strptime(kwargs.pop('create_date_start'), dateformat)
            query &= Q(create_date__gte=create_date_start)
        if kwargs.get('create_date_end'):
            create_date_end = datetime.strptime(kwargs.pop('create_date_end'), dateformat) + timedelta(days=1)
            query &= Q(create_date__lte=create_date_end)
        if kwargs.get('update_date_start'):
            update_date_start = datetime.strptime(kwargs.pop('update_date_start'), dateformat)
            query &= Q(update_date__gte=update_date_start)
        if kwargs.get('update_date_end'):
            update_date_end = datetime.strptime(kwargs.pop('update_date_end'), dateformat) + timedelta(days=1)
            query &= Q(update_date__lte=update_date_end)
    except ValueError:
        raise HttpError(400, "Invalid date format. Use YYYY-MM-DD.")
    for key, value in kwargs.items():
        if value:
            query &= Q(**{key + '__icontains': value})
    objects = model.objects.filter(query).annotate(
        created_date=Cast(TruncMinute("create_date"), CharField()),
        updated_date=Cast(TruncMinute("update_date"), CharField())
    )
    return objects

def get_loan_product_list(
        self,
        product_id: str = None,
        product_name: str = None,
        description: str = None,
        category: str = None,
        create_date_start: str = None,
        create_date_end: str = None,
        update_date_start: str = None,
        update_date_end: str = None
):
    query = Q()
    dateformat = '%Y-%m-%d'
    loanproducts = get_filtered_objects(LoanProduct, query, dateformat, id=product_id, name=product_name,
                                        description=description, category=category,
                                        create_date_start=create_date_start,
                                        create_date_end=create_date_end, update_date_start=update_date_start,
                                        update_date_end=update_date_end)
    return list(loanproducts)

Open/Closed Principle

Prinsip ini menyatakan bahwa sebuah class atau function seharusnya terbuka untuk ekstensi tetapi tertutup untuk modifikasi. Artinya, ketika ada perubahan dalam kebutuhan atau fungsi, developer seharusnya dapat menambahkan fitur baru tanpa memodifikasi kode yang sudah ada.

Untuk melihat contoh penerapannya, kita bisa melihat function API get_loan_product_list sebelumnya. Misalkan kita ingin menambahkan fungsionalitas baru seperti pagination pada API tersebut. Bagaimana caranya agar kita dapat mengimplementasikan pagination tanpa mengubah kode yang sudah ada? Salah satu caranya adalah dengan menggunakan decorator pattern. Pada kasus ini saya menggunakan decorator pattern yaitu paginate dari package yang saya gunakan untuk membuat API yaitu django-ninja

@paginate()
def get_loan_product_list(
        self,
        product_id: str = None,
        product_name: str = None,
        description: str = None,
        category: str = None,
        create_date_start: str = None,
        create_date_end: str = None,
        update_date_start: str = None,
        update_date_end: str = None
):
    query = Q()
    dateformat = '%Y-%m-%d'
    loanproducts = get_filtered_objects(LoanProduct, query, dateformat, id=product_id, name=product_name,
                                        description=description, category=category,
                                        create_date_start=create_date_start,
                                        create_date_end=create_date_end, update_date_start=update_date_start,
                                        update_date_end=update_date_end)
    return list(loanproducts)

Liskov Substitution Principle

Prinsip ini menyatakan bahwa sebuah objek dari kelas child harus dapat digunakan sebagai pengganti objek dari kelas parent tanpa mengubah perilaku program. Dalam kata lain, objek child harus memiliki semua perilaku yang dimiliki oleh objek parent.

Sebagai contoh, perhatikan dua model (LoanProduct dan Section) yang merupakah turunan dari django Model dan mempunyai str function

class LoanProduct(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField()
    category = models.CharField(max_length=100)
    create_date = models.DateTimeField(default=timezone.now)
    update_date = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return f"Loan product - {self.name}"

class Section(models.Model):
    name = models.CharField(max_length=100)
    minimum_score = models.PositiveIntegerField(null=True, blank=True, default=0)
    create_date = models.DateTimeField(default=timezone.now)
    update_date = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return f"Section - {self.name}"

Berdasarkan LiskovΓÇÖs Substitution Principle, kita seharusnya dapat menggunakan instance dari django Model di mana saja instance dari LoanProduct atau Section diharapkan. Misalnya, kita dapat menentukan fungsi yang menerima instance django Model sebagai argumen dan mencetak instance tersebut:

def print_model(model):  
  print(model)

Kemudian kita membuat instance dari model LoanProduct dan Section yang keduannya melakukan override terhadap method str sehingga kita dapat memangil fungsi print_model menggunakan instance dari model LoanProduct dan Section tanpa ada masalah

loanproduct = LoanProduct.objects.create(name="Modal Usaha")  
section = Cat.objects.create(name="Ekonomi")  
  
print_model(loanproduct)  # outputs: Loan product - Modal Usaha  
print_model(section)  # outputs: Section - Ekonomi

Interface Segregation Principle

Prinsip ini menyatakan bahwa sebuah klien hanya boleh bergantung pada interface yang dibutuhkannya, dan tidak boleh bergantung pada interface yang tidak dibutuhkan. Dalam kata lain, kita harus memisahkan interface yang berbeda-beda dalam kelas yang terpisah agar klien hanya menggunakan interface yang dibutuhkannya.

Sebagai contoh, perhatikan implementasi dari view pada django digunakan untuk meng-handle upload file seorang applicant

class UploadApplicantFileView(View):  
    def get(self, request):  
        # handle get  
    def post(self, request):  
        # handle post  
    def update(self, request):  
        # handle update  
    def delete(self, request):  
        # handle delete

Class diatas melanggar Interface Segregation Principle karena ΓÇÿmemaksaΓÇÖ klien untuk bergantung pada interface yang tidak dibutuhkan (dalam hal ini interface update dan delete). Solusinya adalah kita mengimplementasikan view hanya dengan interface yang dibutuhkan saja

class UploadApplicantFileView(View):  
    def get(self, request):  
        # handle get  
    def post(self, request):  
        # handle post

Dependency Inversion Principle

Prinsip ini menyetakan bahwa sebuah kelas sebaiknya bergantung pada abstraksi. Dengan kata lain, prinsip ini menekankan bahwa kelas-kelas yang lebih tinggi tidak seharusnya bergantung pada kelas-kelas yang lebih rendah dalam sistem.

Sebagai contoh, perhatikan kode berikut dimana terjadi pelanggaran terhadap Dependency Inversion Principle.

class PostgreSQLConnection:  
    def connect(self):  
        # Handle database connection  
        return "Database Connection"  
  
class ModelApp:  
    def __init__(self, connection : PostgreSQLConnection):  
        # self connection  
        self.connection = connection

ModelApp pada kode diatas bergantung kepada implementasi low level class yaitu PostgreSQLConnection. Hal tersebut melanggar Dependency Inversion Principle. Untuk dapat mengatasi hal tersebut, kita dapat memanfaatkan abastraksi sehingga high level class seperti ModelApp dapat bergantu pada abstraksi. Berikut adalah contoh solusinya

class Connection:  
    def connect():  
        pass  
  
class PostgreSQLConnection(Koneksi):  
    def connect():  
        # Logic untuk menghandle database connection  
        return "Database Connection"  
  
class ModelApp:  
    def __init__(self, connection : Connection):  
        # self connection  
        self.connection = connection

Kesimpulan

SOLID principle adalah seperangkat prinsip-prinsip perancangan software yang bertujuan untuk memudahkan pengembangan, pemeliharaan, dan perubahan kode. Prinsip-prinsip tersebut terdiri dari Single Responsibility Principle (SRP), Open-Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), dan Dependency Inversion Principle (DIP).

Dengan mengikuti SOLID principle, software developer dapat memperbaiki kualitas kode mereka, meningkatkan modularitas dan memudahkan perubahan di masa depan. Prinsip-prinsip tersebut membantu software developer membangun aplikasi yang lebih mudah diuji, dipelihara, dan ditingkatkan. Oleh karena itu, SOLID principle sangat penting bagi setiap software developer untuk dipahami dan diterapkan dalam praktek mereka.