SOLID: Prinsip Desain OOP yang Baik

10 min read
SOLID
Object Oriented Programming
Design Principles
Software Development
Clean Code

Jika kamu seorang software engineer atau developer, kamu mungkin pernah mendengar tentang SOLID. SOLID adalah singkatan dari lima prinsip desain OOP yang diperkenalkan oleh Robert C. Martin, seorang praktisi pemrograman dan software consultant. Prinsip SOLID membantumu mengembangkan code yang mudah dipelihara dan diextend, dan sering digunakan dalam software development.

SOLID ini sering kali dijadikan sebagai salah satu tolak ukur ketika kita sedang interview untuk posisi software engineer. Jadi, mari kita pelajari SOLID secara mendetail. Semoga bisa sedikit membantu kamu dalam mempersiapkan interview kamu nanti. Hitung-hitung saya juga bisa sedikit me-refresh pengetahuan tentang SOLID. Hehe 😁


Apa itu OOP?

Sebelum kita mempelajari SOLID, kita harus memahami terlebih dahulu apa itu OOP. OOP adalah singkatan dari Object Oriented Programming. OOP adalah sebuah paradigma pemrograman yang berfokus pada object. Pada paradigma ini, object adalah sebuah entity yang memiliki state dan behavior.

Contoh object yang umum digunakan di OOP adalah class. Class adalah sebuah blueprint yang digunakan untuk membuat object. Sebuah Class memiliki state dan behavior.

Contoh:

public class Person {
    // State atau keadaan pada class Person
    private String name;
    private int age;
    private String address;

    // Constructor
    public Person(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    // Behavior atau perilaku pada class Person
    public void sayHello() {
        System.out.println("Hello, my name is " + name + ".");
    }

    public void haveBirthday() {
        age++;
        System.out.println("It's my birthday! Now I'm " + age + " years old.");
    }

    public void move(String newAddress) {
        address = newAddress;
        System.out.println("I'm moving to " + newAddress + ".");
    }
}

Pada contoh di atas, class Person memiliki state name, age, dan address. Class Person juga memiliki behavior sayHello(), haveBirthday(), dan move(). Class Person juga memiliki constructor yang digunakan untuk membuat object dari class tersebut.

Selain itu, Class juga memiliki attribute dan method. Attribute adalah variabel yang ada di dalam class. Sedangkan method adalah fungsi yang ada di dalam class. Contoh:

public class Car {
    // State atau keadaan pada class Car
    private String color;
    private int speed;

    // Attribute atau atribut pada class Car
    private static int numberOfCars = 0;

    // Constructor
    public Car(String color, int speed) {
        this.color = color;
        this.speed = speed;
        numberOfCars++;
    }

    // Behavior atau perilaku pada class Car
    public void start() {
        System.out.println("The car is starting...");
    }

    public void accelerate(int delta) {
        speed += delta;
        System.out.println("The car is accelerating to " + speed + " km/h.");
    }

    public void stop() {
        speed = 0;
        System.out.println("The car is stopping...");
    }

    // Method pada class Car
    public static int getNumberOfCars() {
        return numberOfCars;
    }
}

Pada contoh di atas, class Car memiliki state color dan speed. Class Car juga memiliki behavior start(), accelerate(), dan stop(). Class Car juga memiliki attribute numberOfCars yang dideklarasikan sebagai variabel static, yang menyimpan jumlah mobil yang sudah dibuat dari class ini. Class Car juga memiliki method getNumberOfCars(). Attribute dan method ini bisa diakses tanpa harus membuat object dari class tersebut. Dengan adanya state dan behavior pada class Car ini, objek-objek yang dibuat dari class ini dapat merepresentasikan mobil dengan atribut-atribut yang dimilikinya dan perilaku-perilaku yang dapat dilakukan oleh mobil tersebut. Sedangkan attribute dan method pada class Car berkaitan dengan class itu sendiri dan digunakan untuk mengatur dan mengakses variabel-variabel dan fungsi-fungsi pada class Car.

Class juga memiliki constructor yang digunakan untuk membuat object dari class tersebut.


Apa itu SOLID?

Lalu, apa itu SOLID?

SOLID adalah singkatan dari lima prinsip desain OOP yang diperkenalkan oleh Robert C. Martin, seorang praktisi pemrograman dan software consultant. Prinsip SOLID membantumu mengembangkan code yang mudah dipelihara dan diextend, dan sering digunakan dalam software development.

Berikut adalah lima prinsip SOLID:

Single Responsibility Principle (SRP)

Prinsip ini mengatakan bahwa sebuah class hanya boleh memiliki satu responsibility atau tanggung jawab. Responsibility atau tanggung jawab disini adalah sebuah behavior atau perilaku yang dimiliki oleh class tersebut. Contoh:

public class OrderProcessor {
    public void processOrder(Order order) {
        // melakukan validasi terhadap order
        if (!order.isValid()) {
            throw new InvalidOrder("Invalid order!");
        }

        // melakukan pengiriman order
        ShippingService shippingService = new ShippingService();
        shippingService.shipOrder(order);

        // mengirim email konfirmasi
        EmailService emailService = new EmailService();
        emailService.sendEmail(order.getCustomerEmail(), "Order Confirmation", "Thank you for your order!");

        // membuat laporan
        ReportService reportService = new ReportService();
        reportService.generateOrderReport(order);
    }
}

Pada contoh di atas, class OrderProcessor hanya bertanggung jawab untuk memproses pesanan (order). Class ini melakukan validasi terhadap order, melakukan pengiriman order, mengirim email konfirmasi, dan membuat laporan terkait dengan order tersebut. Setiap fungsi tersebut memiliki tanggung jawab yang berbeda, namun semuanya terkait dengan proses pesanan.

Open-Closed Principle (OCP)

Prinsip ini mengatakan bahwa sebuah class harus terbuka untuk extension namun tertutup untuk modification. Prinsip ini juga dikenal dengan istilah Open for extension, closed for modification. Contoh:

public abstract class Shape {
    public abstract double calculateArea();
}

public class Rectangle extends Shape {
    private double length;
    private double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    public double calculateArea() {
        return length * width;
    }
}

public class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

public class AreaCalculator {
    public double calculateTotalArea(Shape[] shapes) {
        double totalArea = 0;
        for (Shape shape : shapes) {
            totalArea += shape.calculateArea();
        }
        return totalArea;
    }
}

Pada contoh di atas, class Shape merupakan abstract class yang memiliki method calculateArea() yang digunakan untuk menghitung luas dari sebuah bentuk. Class ini memiliki dua class turunan, yaitu Rectangle dan Circle, yang masing-masing memiliki implementasi sendiri untuk method calculateArea(). Selain itu, terdapat juga class AreaCalculator yang memiliki method calculateTotalArea() yang dapat menghitung total luas dari beberapa objek Shape.

Dengan menerapkan Open-Closed Principle, class Shape telah dibuat terbuka untuk di-extend, sehingga jika kita ingin menambahkan jenis shape baru, kita hanya perlu membuat class baru yang meng-extend class Shape dan mengimplementasikan method calculateArea()-nya sendiri, tanpa harus merubah code pada class Shape, Rectangle, atau Circle.

Dalam contoh ini, AreaCalculator juga tertutup untuk modifikasi, karena meskipun kita menambahkan jenis shape baru, class AreaCalculator tidak perlu dimodifikasi dan masih dapat menghitung total luas dari semua objek Shape yang ada.

Liskov Substitution Principle (LSP)

Prinsip ini mengatakan bahwa sebuah class turunan harus dapat digantikan dengan class induknya tanpa mengubah perilaku dari class induk tersebut. Artinya, ketika kamu membuat class turunan dari class dasar, class turunan tersebut harus memiliki perilaku yang sama atau setidaknya kompatibel dengan class dasarnya. Contoh:

public class Shape {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

public class Rectangle extends Shape {
    public int getArea() {
        return width * height;
    }
}

public class Square extends Shape {
    public void setWidth(int width) {
        this.width = width;
        this.height = width;
    }

    public void setHeight(int height) {
        this.width = height;
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

public class AreaCalculator {
    public int calculateTotalArea(Shape[] shapes) {
        int totalArea = 0;
        for (Shape shape : shapes) {
            totalArea += shape.getArea();
        }
        return totalArea;
    }
}

Pada contoh di atas, class Shape merupakan class dasar yang memiliki method setWidth(), setHeight(), dan getArea(). Class ini memiliki dua class turunan, yaitu Rectangle dan Square, yang masing-masing memiliki implementasi sendiri untuk method getArea(). Selain itu, terdapat juga class AreaCalculator yang memiliki method calculateTotalArea() yang dapat menghitung total luas dari beberapa objek Shape.

Pada contoh tersebut, objek dari class Square dapat digunakan sebagai pengganti objek dari class Shape atau Rectangle tanpa menimbulkan kesalahan dalam program. Meskipun class Square memiliki implementasi yang berbeda untuk method setWidth() dan setHeight(), tetapi behavior yang diharapkan dari method getArea() tetap sama dengan behavior pada class Shape atau Rectangle.

Interface Segregation Principle (ISP)

Prinsip ini mengatakan bahwa sebuah interface tidak boleh terlalu besar, sehingga class yang meng-implement interface tersebut tidak terlalu banyak. Contoh:

public interface BankAccount {
    void deposit(double amount);
    void withdraw(double amount);
    double getBalance();
    void transfer(BankAccount destination, double amount);
    void calculateInterest();
}

Pada contoh di atas, terdapat sebuah interface BankAccount yang memiliki beberapa method seperti deposit(), withdraw(), getBalance(), transfer(), dan calculateInterest(). Namun, pada kenyataannya, tidak semua jenis akun di bank perlu mengimplementasikan semua method tersebut.

Sebagai contoh, jenis akun tabungan mungkin tidak memerlukan method transfer() karena tidak dapat melakukan transfer ke rekening bank lain. Begitu juga dengan jenis akun deposito yang tidak perlu mengimplementasikan method calculateInterest() karena bunganya sudah dihitung sebelumnya.

Untuk menerapkan Interface Segregation Principle pada contoh ini, kita dapat memecah interface BankAccount menjadi beberapa interface yang lebih spesifik, seperti DepositAccount, SavingsAccount, dan LoanAccount. Setiap interface hanya akan memiliki method yang diperlukan oleh jenis akun yang sesuai.

Contoh implementasi dapat dilihat di bawah ini:

public interface DepositAccount {
    void deposit(double amount);
    void withdraw(double amount);
    double getBalance();
}

public interface SavingsAccount extends DepositAccount {
    void calculateInterest();
}

public interface LoanAccount {
    void deposit(double amount);
    void withdraw(double amount);
    double getBalance();
    void transfer(BankAccount destination, double amount);
}

Dependency Inversion Principle (DIP)

Prinsip ini mengatakan bahwa sebuah class tidak boleh bergantung pada class lain secara langsung, melainkan harus bergantung pada interface dari class tersebut.

Contoh:

public class Database {
    private MySQLDatabase mySQLDatabase;
    private OracleDatabase oracleDatabase;

    public Database(MySQLDatabase mySQLDatabase, OracleDatabase oracleDatabase) {
        this.mySQLDatabase = mySQLDatabase;
        this.oracleDatabase = oracleDatabase;
    }
}

Pada contoh di atas, class Database merupakan class yang bergantung pada class lain, yaitu class MySQLDatabase dan OracleDatabase. Karena class Database bergantung pada class MySQLDatabase dan OracleDatabase, maka class Database tidak dapat digunakan tanpa class MySQLDatabase dan OracleDatabase.

Contoh lain:

public interface Notification {
    void sendNotification(String message);
}

public class EmailNotification implements Notification {
    public void sendNotification(String message) {
        // logika untuk mengirim notifikasi melalui email
    }
}

public class SMSNotification implements Notification {
    public void sendNotification(String message) {
        // logika untuk mengirim notifikasi melalui SMS
    }
}

public class NotificationService {
    private Notification notification;

    public NotificationService(Notification notification) {
        this.notification = notification;
    }

    public void sendNotification(String message) {
        notification.sendNotification(message);
    }
}

Pada contoh di atas, terdapat sebuah interface Notification yang digunakan sebagai abstraksi untuk mengirim notifikasi, dan dua class EmailNotification dan SMSNotification yang mengimplementasikan interface tersebut. Kemudian, terdapat juga sebuah class NotificationService yang bergantung pada interface Notification sebagai abstraksi. Class ini memiliki method sendNotification() yang menerima pesan dan mengirim notifikasi melalui Notification yang disediakan.

Dengan menerapkan Dependency Inversion Principle pada contoh ini, class NotificationService tidak bergantung pada class EmailNotification atau SMSNotification secara langsung, melainkan bergantung pada abstraksi Notification. Hal ini memungkinkan kita untuk dengan mudah mengganti jenis notifikasi yang digunakan tanpa perlu merubah implementasi pada NotificationService.

Sebagai contoh, jika di masa depan kita ingin menambahkan jenis notifikasi baru seperti push notification, kita dapat membuat class baru yang mengimplementasikan interface Notification, dan kemudian memasukkan instance dari class PushNotification ke dalam NotificationService tanpa perlu merubah implementasi pada class NotificationService.

Penutup dan Kesimpulan

Pada artikel ini, kita telah mempelajari tentang SOLID principles, yaitu lima prinsip dasar dalam pemrograman berorientasi objek. Prinsip-prinsip ini dapat digunakan untuk membuat code yang lebih mudah dipahami, lebih mudah diperbaiki, dan lebih mudah untuk ditambahkan fitur baru. Mengimplementasikan SOLID principles pada code awalnya mungkin terasa berat, tetapi akan membantu kita dalam menulis code yang lebih baik dan mudah diperbaiki di masa depan.

Semoga artikel ini dapat bermanfaat bagi kita semua. Terima kasih. 😄