Prinsip Pemrograman S.O.L.I.D

Pengenalan

S.O.L.I.D merupakan akronim yang digunakan untuk lima prinsip desain dalam pemrograman berorientasi objek (OOP). Konsep ini pertama kali diperkenalkan oleh Robert C. Martin aka. 'Uncle Bob' pada awal tahun 2000-an. Tujuan dari prinsip-prinsip ini adalah untuk membantu pengembang perangkat lunak menghindari desain perangkat lunak yang buruk, yang berimplikasi pada perangkat lunak yang sulit untuk diperbaiki dan dikembangkan seiring berjalannya waktu.

Prinsip-prinsip ini telah menjadi dasar dari banyak metodologi pengembangan perangkat lunak modern, dan banyak digunakan oleh para programmer untuk memastikan kode mereka lebih modular, mudah dipahami, dan lebih mudah untuk dikelola. Kelima prinsip ini adalah:

  • Single Responsibility Principle (SRP)

  • Open/Close Principle (OCP)

  • Liskov Substitution Principle (LSP)

  • Interface Segregation Principle (ISP)

  • Dependency Inversion Principle (DIP)

Manfaat dari penerapan prinsip-prinsip S.O.L.I.D antara lain:

  1. Meningkatkan Readability.

  2. Meningkatkan Maintainability.

  3. Meningkatkan Reusability.

  4. Mengurangi Risiko Bug.

Single Responsibility Principle (SRP)

“A class should have only one reason to change”.

Pengertian

Prinsip Tanggung Jawab Tunggal (Single Responsibility Principle atau SRP) adalah prinsip pertama dalam S.O.L.I.D yang menyatakan bahwa setiap komponen perangkat lunak harus memiliki satu, dan hanya satu, tanggung jawab. Dengan kata lain, sebuah kelas atau modul hanya boleh memiliki satu alasan untuk berubah.

Tujuan SRP

  • Cohesion yang Tinggi: Mencapai kohesi yang tinggi berarti setiap bagian dari komponen perangkat lunak terkait erat satu sama lain dalam melakukan satu tugas spesifik.

  • Coupling yang Rendah: Mengurangi ketergantungan antara berbagai komponen perangkat lunak, sehingga perubahan pada satu komponen tidak mempengaruhi komponen lainnya.

Contoh Kasus: Cohesion

Kohesi adalah tingkat di mana sejumlah bagian dari suatu komponen perangkat lunak saling terkait.

Kode Awal:

class Square:
    def __init__(self):
        self.side = 5

    def calculate_area(self):
        return self.side * self.side

    def calculate_perimeter(self):
        return self.side * 4

    def draw(self) -> None:
        print("rendering the square image")

    def rotate(self, degree) -> None:
        print(f"rotating the image of the square clockwise to {degree} degree")
        self.draw()

square = Square()
print(f"Square Area: {square.calculate_area()}")
print(f"Square Perimeter: {square.calculate_perimeter()}")

square.draw()
square.rotate(25)

Pada kode di atas, kelas Square bertanggung jawab melakukan beberapa tugas seperti menghitung area, menghitung keliling, menggambar gambar persegi, dan memutar gambar. Hal ini melanggar prinsip SRP karena kelas Square bertanggung jawab pada lebih dari satu tugas.

Kode yang Direfaktor:

class Square:
    def __init__(self):
        self.side = 5

    def calculate_area(self) -> int:
        return self.side * self.side

    def calculate_perimeter(self) -> int:
        return self.side * 4

class SquareUI:
    def __init__(self, square: Square):
        self.square: Square = square

    def draw(self) -> None:
        print("rendering the square image")

    def rotate(self, degree: int) -> None:
        print(f"rotating the image of the square clockwise to {degree} degree")
        self.draw()

square = Square()
print(f"Square Area: {square.calculate_area()}")
print(f"Square Perimeter: {square.calculate_perimeter()}")

square_ui = SquareUI(square)
square_ui.draw()
square_ui.rotate(25)

Pada kode yang telah direfaktor, kita memisahkan tanggung jawab menjadi dua kelas: Square untuk perhitungan area dan keliling, serta SquareUI untuk menggambar dan memutar gambar. Ini meningkatkan kohesi dengan memastikan setiap kelas memiliki satu tanggung jawab.

Contoh Kasus: Coupling

Coupling adalah tingkat ketergantungan antara berbagai komponen perangkat lunak.

Kode Awal:

class Student:
    def __init__(self, name: str, age: int, grade: int):
        self.name: str = name
        self.age: int = age
        self.grade: int = grade

    def save(self) -> None:
        # Simulate saving student data to a file
        with open('students.txt', 'a') as file:
            file.write(f"{self.name}, {self.age}, {self.grade}\n")

# Create a student object
student = Student("John Doe", 18, "12th")

# Save the student data
student.save()

Pada kode di atas, kelas Student bertanggung jawab untuk menyimpan data siswa ke dalam sebuah file. Hal ini menyebabkan coupling tinggi karena kelas Student memiliki tanggung jawab yang tidak seharusnya, yaitu menyimpan data.

Kode yang Direfaktor:

class Student:
    def __init__(self, name: str, age: int, grade: int):
        self.name: str = name
        self.age: int = age
        self.grade: int = grade

class StudentRepository:
    def save(self, student: Student) -> None:
        # Simulate saving student data to a file
        with open('students.txt', 'a') as file:
            file.write(f"{student.name}, {student.age}, {student.grade}\n")

# Create a student object
student = Student("John Doe", 18, "12th")

# Save the student data using StudentRepository
student_repo = StudentRepository()
student_repo.save(student)

Pada kode yang telah direfaktor, kita memisahkan tanggung jawab penyimpanan data ke dalam kelas StudentRepository. Hal ini mengurangi coupling dengan memastikan bahwa kelas Student hanya bertanggung jawab untuk menyimpan data siswa, sementara kelas StudentRepository bertanggung jawab untuk operasi penyimpanan data.

Dengan memisahkan tanggung jawab seperti yang ditunjukkan dalam contoh-contoh di atas, kita dapat mencapai desain perangkat lunak yang lebih modular danmudah dipelihara. Prinsip SRP membantu kita untuk memastikan bahwa setiap kelas atau modul dalam perangkat lunak memiliki satu tanggung jawab, sehingga membuat kode lebih mudah untuk dikelola dan dipahami.

Open/Close Principle (OCP)

software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

Pengertian

Prinsip Buka/Tutup (Open/Close Principle atau OCP) adalah prinsip kedua dalam S.O.L.I.D. Prinsip ini menyatakan bahwa komponen perangkat lunak harus terbuka untuk ekstensi tetapi tertutup untuk modifikasi. Dengan kata lain, kita harus dapat menambahkan fungsionalitas baru ke sebuah kelas tanpa mengubah kode yang sudah ada.

Tujuan OCP

  • Kemudahan Menambahkan Fitur Baru: Memungkinkan pengembang untuk menambahkan fitur baru tanpa mengganggu kode yang sudah ada.

  • Minimalkan Biaya Pengembangan dan Pengujian: Dengan tidak mengubah kode yang sudah ada, risiko bug baru berkurang dan pengujian ulang dapat diminimalkan.

  • Decoupling: OCP seringkali membutuhkan pemisahan ketergantungan, yang pada gilirannya secara otomatis mengikuti SRP.

Contoh Kasus: Health Insurance

Kode Awal:

class HealthInsuranceCustomerProfile:
    def is_loyal(self) -> bool:
        # some complex business logic to determine 
        # a loyal health insurance customer
        return True # or False

class InsurancePremiumDiscountCalculator:
    def calculate_discount(self, customer: HealthInsuranceCustomerProfile) -> int:
        if customer.is_loyal():
            return 20
        return 0

customer_profile = HealthInsuranceCustomerProfile()
calculator = InsurancePremiumDiscountCalculator()
discount = calculator.calculate_discount(customer_profile)
print(f"The discount: {discount}")

Pada kode di atas, InsurancePremiumDiscountCalculator menghitung diskon untuk pelanggan asuransi kesehatan berdasarkan loyalitas mereka. Namun, jika kita ingin menambahkan jenis asuransi lain seperti asuransi kendaraan, kita harus memodifikasi InsurancePremiumDiscountCalculator.

Permasalahan: Health + Vehicle Insurance

class HealthInsuranceCustomerProfile:
    def is_loyal(self) -> bool:
        # some complex business logic to determine 
        # a loyal health insurance customer
        return True # or False

class VehicleInsuranceCustomerProfile:
    def is_loyal(self) -> bool:
        # some complex business logic to determine 
        # a loyal vehicle insurance customer
        return True # or False

class InsurancePremiumDiscountCalculator:
    def calculate_health_insurance_discount(self, customer: HealthInsuranceCustomerProfile) -> int:
        if customer.is_loyal():
            return 20
        return 0

    def calculate_vehicle_insurance_discount(self, customer: VehicleInsuranceCustomerProfile) -> int:
        if customer.is_loyal():
            return 20
        return 0

calculator = InsurancePremiumDiscountCalculator()

health_customer = HealthInsuranceCustomerProfile()
health_discount = calculator.calculate_health_insurance_discount(health_customer)
print(f"health discount: {health_discount}")

vehicle_customer = VehicleInsuranceCustomerProfile()
vehicle_discount = calculator.calculate_vehicle_insurance_discount(vehicle_customer)
print(f"vehicle discount: {vehicle_discount}")

Pada kode ini, kita menambahkan kelas VehicleInsuranceCustomerProfile dan metode calculate_vehicle_insurance_discount di InsurancePremiumDiscountCalculator. Namun, ini melanggar prinsip OCP karena kita harus memodifikasi kelas yang sudah ada untuk menambahkan fungsionalitas baru.

Refaktor Kode

Untuk mematuhi prinsip OCP, kita dapat menggunakan abstraksi untuk membuat kelas InsurancePremiumDiscountCalculator dapat diperluas tanpa perlu dimodifikasi.

from abc import ABC, abstractmethod

class CustomerProfile(ABC):
    @abstractmethod
    def is_loyal(self) -> bool:
        pass

class HealthInsuranceCustomerProfile(CustomerProfile):    
    def is_loyal(self) -> bool:
        # some complex business logic to determine 
        # a loyal health insurance customer
        return True # or False

class VehicleInsuranceCustomerProfile(CustomerProfile):
    def is_loyal(self) -> bool:
        # some complex business logic to determine 
        # a loyal vehicle insurance customer
        return True # or False  

class InsurancePremiumDiscountCalculator:
    def calculate_discount(self, customer: CustomerProfile) -> int:
        if customer.is_loyal():
            return 20
        return 0

calculator = InsurancePremiumDiscountCalculator()

health_customer = HealthInsuranceCustomerProfile()
health_discount = calculator.calculate_discount(health_customer)
print(f"health discount: {health_discount}")

vehicle_customer = VehicleInsuranceCustomerProfile()
vehicle_discount = calculator.calculate_discount(vehicle_customer)
print(f"vehicle discount: {vehicle_discount}")

Pada kode yang telah direfaktor, kita membuat abstraksi CustomerProfile yang merupakan kelas abstrak dengan metode is_loyal. HealthInsuranceCustomerProfile dan VehicleInsuranceCustomerProfile mengimplementasikan CustomerProfile. InsurancePremiumDiscountCalculator hanya perlu mengetahui bahwa ia berurusan dengan CustomerProfile tanpa harus mengetahui detail spesifik dari jenis profil asuransi apa yang digunakan. Ini memungkinkan kita untuk menambahkan jenis asuransi baru tanpa harus mengubah InsurancePremiumDiscountCalculator.

Menambahkan Home Insurance? No Problem.

Sekarang, jika kita ingin menambahkan jenis asuransi baru seperti asuransi rumah, kita cukup membuat kelas baru yang mengimplementasikan CustomerProfile tanpa mengubah kode yang sudah ada.

class HomeInsuranceCustomerProfile(CustomerProfile):
    def is_loyal(self) -> bool:
        # some complex business logic to determine 
        # a loyal home insurance customer
        return True # or False  

home_customer = HomeInsuranceCustomerProfile()
home_discount = calculator.calculate_discount(home_customer)
print(f"home discount: {home_discount}")

Dengan menambahkan kelas HomeInsuranceCustomerProfile, kita dapat menghitung diskon untuk pelanggan asuransi rumah tanpa memodifikasi InsurancePremiumDiscountCalculator. Ini menunjukkan bahwa kode kita mematuhi prinsip OCP.

Tentu, berikut adalah penjelasan rinci tentang Liskov Substitution Principle (LSP) beserta kronologis sebab akibat dari kode yang diberikan. Saya juga akan menyesuaikan kode agar sesuai dengan contoh yang diberikan.

Liskov Substitution Principle (LSP)

“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program”

Prinsip Substitusi Liskov (Liskov Substitution Principle atau LSP) adalah prinsip ketiga dalam S.O.L.I.D yang menyatakan bahwa objek harus dapat digantikan dengan subkelas mereka tanpa mempengaruhi kebenaran program. Prinsip ini dinamai berdasarkan nama Barbara Liskov, yang pertama kali memperkenalkan konsep ini pada tahun 80-an.

Tujuan LSP

  • Mengganti "Is-A" dengan Pola Pikir yang Benar: LSP menekankan bahwa hanya karena sebuah objek adalah subclass dari objek lain, tidak berarti objek tersebut dapat menggantikan superclass dalam semua situasi.

  • Abstraksi yang Tepat: Jika sebuah objek tampak seperti bebek dan berbunyi seperti bebek, tetapi memerlukan baterai, kemungkinan besar kita memiliki abstraksi yang salah.

Contoh Kasus: Jangan Memaksa Penguin Terbang

Kode Awal:

class Bird:
    def fly(self) -> None:
        print("Generic bird flying")

class Pigeon(Bird):
    def fly(self) -> None:
        print("Pigeon style flying")

pigeon = Pigeon()
pigeon.fly()

class Penguin(Bird):
    pass 

#     def fly(self) -> None:
#         raise TypeError("Penguin cannot fly")

penguin = Penguin()
penguin.fly()

Pada kode di atas, Penguin adalah subclass dari Bird, tetapi penguin tidak dapat terbang. Jika kita memanggil metode fly pada objek Penguin, kita melanggar LSP karena perilaku ini tidak sesuai dengan perilaku Bird.

Refaktor Kode:

from abc import ABC, abstractmethod

class Bird(ABC):
    @abstractmethod
    def move(self) -> None:
        pass

class Pigeon(Bird):
    def move(self) -> None:
        print("Pigeon style flying")

class Penguin(Bird):
    def move(self) -> None:
        print("Penguin waddling")

pigeon = Pigeon()
pigeon.move()

penguin = Penguin()
penguin.move()

Pada kode yang telah direfaktor, kita memperkenalkan abstraksi move untuk Bird. Sekarang Pigeon dan Penguin tidak melanggar LSP karena move mengakomodasi pergerakan kedua burung tersebut dengan cara yang sesuai.

Contoh 1: F1 Car

Kode Awal:

class Car:
    def show_cabin_width(self) -> None:
        print("Showing cabin width")

class FormulaOne(Car):
#     def show_cabin_width(self) -> None:
#         pass

#     def show_cabin_width(self) -> None:
#         raise TypeError("Racing car doesn't have cabin")

#     def show_cabin_width(self):
#         self.show_cockpit_width()

    def show_cockpit_width(self) -> None:
        print("Showing cockpit width")

cars = []
cars.append(Car())
cars.append(Car())
cars.append(FormulaOne())

for car in cars:
    car.show_cabin_width()

Pada kode ini, FormulaOne adalah subclass dari Car, tetapi FormulaOne tidak memiliki kabin. Jika kita memanggil show_cabin_width pada objek FormulaOne, kita melanggar LSP karena perilaku ini tidak sesuai dengan perilaku Car.

Refaktor Kode:

from abc import ABC, abstractmethod

class Vehicle(ABC):
    @abstractmethod
    def show_interior_width(self) -> None:
        pass

class Car(Vehicle):
    def show_interior_width(self) -> None:
        self.show_cabin_width()

    def show_cabin_width(self) -> None:
        print("Showing cabin width")

class FormulaOne(Vehicle):
    def show_interior_width(self) -> None:
        self.show_cockpit_width()

    def show_cockpit_width(self) -> None:
        print("Showing cockpit width")

vehicles = []
vehicles.append(Car())
vehicles.append(Car())
vehicles.append(FormulaOne())

for vehicle in vehicles:
    vehicle.show_interior_width()

Pada kode yang telah direfaktor, kita memperkenalkan abstraksi Vehicle dengan metode show_interior_width. Sekarang Car dan FormulaOne dapat menggantikan Vehicle tanpa melanggar LSP karena show_interior_width mengakomodasi lebar interior kedua kendaraan tersebut dengan cara yang sesuai.

Contoh 2: Diskon Produk

Kode Awal:

class Product:
    def __init__(self):
        self.discount: float = 0.25

    def get_discount(self) -> float:
        return self.discount

class InHouseProduct(Product):
    def apply_extra_discount(self) -> None:
        self.discount = self.discount * 1.5

products = []
products.append(Product())
products.append(Product())
products.append(InHouseProduct())

for product in products:
    if isinstance(product, InHouseProduct):
        product.apply_extra_discount()

    print(f"Final discount: {product.get_discount()}")

Pada kode di atas, kita menggunakan isinstance untuk memeriksa apakah produk adalah InHouseProduct sebelum menerapkan diskon tambahan. Ini melanggar LSP karena kita harus mengetahui jenis spesifik produk sebelum melakukan operasi.

Refaktor Kode:

class Product:
    def __init__(self):
        self.discount: float = 0.25

    def get_discount(self) -> float:
        return self.discount

class InHouseProduct(Product):
    def get_discount(self) -> float:
        self.apply_extra_discount()
        return self.discount

    def apply_extra_discount(self) -> None:
        self.discount = self.discount * 1.5

products = []
products.append(Product())
products.append(Product())
products.append(InHouseProduct())

for product in products:       
    print(f"Final discount: {product.get_discount()}")

Pada kode yang telah direfaktor, kita memperkenalkan metode get_discount yang diterapkan oleh InHouseProduct untuk menambahkan diskon tambahan sebelum mengembalikan nilai diskon. Ini memastikan bahwa setiap produk dapat menggantikan Product tanpa memerlukan pemeriksaan jenis, sehingga mematuhi LSP.

Tentu, berikut adalah penjelasan rinci tentang Interface Segregation Principle (ISP) beserta contoh kasusnya yang dijelaskan dengan detail.

Interface Segregation Principle (ISP)

“No client should be forced to depend on methods it does not use”.

Prinsip Pemisahan Antarmuka (Interface Segregation Principle atau ISP) adalah prinsip keempat dalam S.O.L.I.D yang menyatakan bahwa klien tidak boleh dipaksa untuk bergantung pada antarmuka yang tidak digunakannya. Dengan kata lain, lebih baik memiliki beberapa antarmuka spesifik yang kecil daripada satu antarmuka besar yang berisi banyak metode yang tidak relevan untuk semua klien.

Tujuan ISP

  • Menghindari Fat Interfaces: Antarmuka besar yang memiliki terlalu banyak metode.

  • Meningkatkan Kohesi: Membagi antarmuka besar menjadi beberapa antarmuka kecil yang lebih kohesif.

  • Menghindari Implementasi Metode Kosong: Menghindari situasi di mana kelas harus mengimplementasikan metode yang tidak digunakannya.

Contoh Kasus: Multi Function Printer

Kode Awal:

from abc import ABC, abstractmethod

class MultiFunctionPrinter(ABC):
    @abstractmethod
    def do_print(self) -> None:
        pass

    @abstractmethod
    def do_scan(self) -> None:
        pass

    @abstractmethod
    def do_fax(self) -> None:
        pass

class XeroxWorkCentre(MultiFunctionPrinter):
    def do_print(self) -> None:
        print("Xerox printing")

    def do_scan(self) -> None:
        print("Xerox scanning")

    def do_fax(self) -> None:
        print("Xerox faxing")

xerox = XeroxWorkCentre()
xerox.do_print()
xerox.do_scan()
xerox.do_fax()

class HPPrintNScan(MultiFunctionPrinter):
    def do_print(self) -> None:
        print("HP printing")

    def do_scan(self) -> None:
        print("HP scanning")

    def do_fax(self) -> None:
        pass

hp = HPPrintNScan()
hp.do_print()
hp.do_scan()
hp.do_fax()

Pada kode di atas, kita memiliki antarmuka MultiFunctionPrinter dengan tiga metode: do_print, do_scan, dan do_fax. XeroxWorkCentre mengimplementasikan ketiga metode ini, sedangkan HPPrintNScan hanya mengimplementasikan do_print dan do_scan, dengan do_fax dibiarkan kosong. Ini melanggar ISP karena HPPrintNScan dipaksa untuk mengimplementasikan metode do_fax yang tidak digunakannya.

Refaktor Kode:

from abc import ABC, abstractmethod

class Printer(ABC):
    @abstractmethod
    def do_print(self) -> None:
        pass

class Scanner(ABC):
    @abstractmethod
    def do_scan(self) -> None:
        pass

class Faxer(ABC):
    @abstractmethod
    def do_fax(self) -> None:
        pass

class XeroxWorkCentre(Printer, Scanner, Faxer):
    def do_print(self) -> None:
        print("Xerox printing")

    def do_scan(self) -> None:
        print("Xerox scanning")

    def do_fax(self) -> None:
        print("Xerox faxing")

class HPPrintNScan(Printer, Scanner):
    def do_print(self) -> None:
        print("HP printing")

    def do_scan(self) -> None:
        print("HP scanning")

class CanonBasicPrinter(Printer):
    def do_print(self) -> None:
        print("Canon printing")

xerox = XeroxWorkCentre()
hp = HPPrintNScan()
canon = CanonBasicPrinter()

printers = [xerox, hp, canon]
for printer in printers:
    printer.do_print()

scanners = [xerox, hp]
for scanner in scanners:
    scanner.do_scan()

Pada kode yang telah direfaktor, kita memecah antarmuka MultiFunctionPrinter menjadi tiga antarmuka terpisah: Printer, Scanner, dan Faxer. Setiap kelas sekarang hanya mengimplementasikan antarmuka yang relevan dengan fungsinya. XeroxWorkCentre mengimplementasikan ketiga antarmuka, HPPrintNScan mengimplementasikan Printer dan Scanner, dan CanonBasicPrinter hanya mengimplementasikan Printer. Ini mematuhi ISP karena setiap kelas hanya bergantung pada metode yang digunakannya.

Dengan menerapkan prinsip ISP, kita memastikan bahwa setiap kelas hanya mengimplementasikan metode yang relevan dengan fungsinya, sehingga menghindari antarmuka besar yang berisi banyak metode yang tidak digunakan.

Tentu, berikut adalah penjelasan rinci tentang Dependency Inversion Principle (DIP) beserta contoh kasusnya yang dijelaskan dengan detail.

Dependency Inversion Principle (DIP)

Prinsip Pembalikan Ketergantungan (Dependency Inversion Principle atau DIP) adalah prinsip kelima dalam S.O.L.I.D yang menyatakan bahwa modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah. Keduanya harus bergantung pada abstraksi. Selain itu, abstraksi tidak boleh bergantung pada detail. Detail harus bergantung pada abstraksi.

Tujuan DIP

  • Mengurangi Ketergantungan: Mengurangi ketergantungan langsung antara modul tingkat tinggi dan modul tingkat rendah.

  • Meningkatkan Keterpisahan: Meningkatkan keterpisahan antar komponen dengan menggunakan abstraksi untuk mendefinisikan interaksi antara modul.

  • Mempermudah Perubahan: Mempermudah perubahan pada satu modul tanpa mempengaruhi modul lainnya.

Contoh Kasus: Product Catalog

Kode Awal:

class ProductCatalog:
    def show_all_products(self) -> None:
        product_repo = SQLProductRepository()
        product_list = product_repo.get_all_product_names()
        print(f"List of all products: {product_list}")


class SQLProductRepository:
    def get_all_product_names(self) -> list[str]:
        return ["soap", "tooth-paste"]

catalog = ProductCatalog()
catalog.show_all_products()

Pada kode di atas, ProductCatalog (modul tingkat tinggi) secara langsung bergantung pada SQLProductRepository (modul tingkat rendah). Ini melanggar prinsip DIP karena perubahan pada SQLProductRepository dapat mempengaruhi ProductCatalog.

Refaktor Kode:

from abc import ABC, abstractmethod

class ProductRepository(ABC):
    @abstractmethod
    def get_all_product_names(self) -> list[str]:
        pass

class ProductFactory:
    def create() -> ProductRepository:
        return SQLProductRepository()

class ProductCatalog:
    def show_all_products(self) -> None:
        product_repo = ProductFactory.create()
        product_list = product_repo.get_all_product_names()
        print(f"List of all products: {product_list}")

class SQLProductRepository(ProductRepository):
    def get_all_product_names(self) -> list[str]:
        return ["soap", "tooth-paste"]

catalog = ProductCatalog()
catalog.show_all_products()

Pada kode yang telah direfaktor, kita memperkenalkan antarmuka ProductRepository yang merupakan abstraksi dari penyimpanan produk. SQLProductRepository mengimplementasikan antarmuka ini. ProductCatalog sekarang bergantung pada ProductRepository alih-alih kepada SQLProductRepository langsung. ProductFactory digunakan untuk membuat instance dari ProductRepository, sehingga ProductCatalog tidak mengetahui detail implementasi penyimpanan produk.

Bonus: Dependency Injection

Dependency Injection adalah teknik di mana sebuah objek menerima objek lain yang diandalkannya. DI adalah salah satu cara untuk menerapkan DIP.

from abc import ABC, abstractmethod

class ProductRepository(ABC):
    @abstractmethod
    def get_all_product_names(self) -> list[str]:
        pass

class ProductFactory:
    def create() -> ProductRepository:
        return SQLProductRepository()

class SQLProductRepository(ProductRepository):
    def get_all_product_names(self) -> list[str]:
        return ["soap", "tooth-paste"]

class ProductCatalog:
    def __init__(self, product_repository: ProductRepository):
        self.__product_repository = product_repository

    def show_all_products(self) -> None:
        product_list = self.__product_repository.get_all_product_names()
        print(f"List of all products: {product_list}")

product_repo = ProductFactory.create()
catalog = ProductCatalog(product_repo)
catalog.show_all_products()

Pada contoh ini, kita menggunakan teknik Dependency Injection. ProductCatalog menerima ProductRepository sebagai dependensi melalui konstruktor. Ini memungkinkan kita untuk menginjeksi dependensi dari luar kelas, sehingga mengurangi ketergantungan langsung dan membuat kode lebih fleksibel dan mudah untuk diuji.

Dengan menerapkan prinsip DIP, kita dapat memastikan bahwa modul tingkat tinggi tidak bergantung pada modul tingkat rendah. Sebaliknya, keduanya bergantung pada abstraksi, yang membuat sistem lebih modular, mudah dipelihara, dan dapat diperluas.


Penerapan dari S.O.L.I.D akan membantu dalam membuat kode yang lebih bersih, terstruktur, dan mudah dipelihara. Terima kasih sudah membaca artikel ini, selamat belajar dan happy coding 😁

Referensi:

https://www.youtube.com/live/bBsFSR9YOYY?si=Ur5RJwm0Am6rPZEG