Skip to content

SOLID Principles

Akmal edited this page Sep 25, 2020 · 3 revisions

Lima prinsip SOLID beserta kepanjangannya

Di dalam paradigma konsep pemrograman OOP (Object-Oriented Programming), SOLID Principles adalah lima prinsip penting dalam software design yang memastikan agar mudah dimengerti, fleksibel, adaptif dan mudah dalam segi maintenance. Prinsip SOLID ini dibuatkan oleh Robert C. Martin

Terdapat 5 prinsip SOLID antara lain:

  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Liskov Subtitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

Single Responsibility Principle (SRP)

Dalam prinsip Single Responsibility Principle, setiap abstraksi/class yang dibuatkan dalam suatu software design hanya boleh memegang 1 tanggung jawab saja. Tujuan dari prinsip ini adalah untuk mencegah adanya perubahan di berbagai bagian dikarenakan adanya berbagai alasan, misalnya pada perubahan fitur pada satu bagian ataupun perubahan struktur pada bagian lainnya.

Salah satu contoh kasus sederhana yang melanggar prinsip SRP adalah class Rectangle.java yang berisikan rumus, dan penggambaran bentuk persegi. Class ini mempunyai method area(), perimeter(), dan draw().

public class Rectangle {
  public double area() {
    // obtain rectangle area
  }
  public double perimeter() {
    // obtain rectangle perimeter
  }
  public void draw(boolean fillShape) {
    // draw rectangle
  }
}

Class ini melanggar prinsip SRP karena class Rectangle harus menjalankan 2 tugasnya yaitu menghitung luas/keliling sekaligus juga menggambarkan persegi sekaligus. Sebagai alternatifnya, kita bisa pecahkan menjadi 2 class yaitu Rectangle dan RectangleDrawer sehingga ketika ada perubahan yang dirujuk pada perumusan persegi, maka kita cukup berfokuskan pada tanggung jawabnya class Rectangle maupun sebaliknya jika terkait dengan penggambaran persegi, maka difokuskan pada tanggung jawabnya class RectangleDrawer.

public class Rectangle {
  public double area() {
    // obtain rectangle area
  }
  public double perimeter() {
    // obtain rectangle perimeter
  }
}

public class RectangleDrawer {
  private Rectangle rectangle;
  public RectangleDrawer(Rectangle rectangle) {
    this.rectangle = rectangle;
  }
  public void draw(boolean fillShape) {
    // draw rectangle
  }
}

Open/Closed Principle (OCP)

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

-- Robert C. Martin

Prinsip ini disebutkan bahwa modul/class/abstraksi dapat diimplementasikan oleh class mana saja tanpa harus menyentuh code-code yang telah kita kerjakan sebelumnya. Prinsip ini bertujuan agar class yang sudah dibuatkan sebelumnya tidak mengalami perubahan karena adanya penambahan class/object baru.

Salah satu contoh kasus yang tentunya melanggar prinsip OCP adalah pada saat menentukan behavior/logic dari suatu object berdasarkan type/class yang diinginkan dari user dari class ShapePrinter.java dan CharNeededCounter.java.

if(shape.equalsIgnoreCase("square")){
  ...
} else if(shape.equalsIgnoreCase("triangle")){
  ...
} else {
  ...
}

Kedua class tersebut tentunya akan melanggar OCP. Bayangkan bila ada tipe Shape baru yang perlu dibuat, tentu saja akan bertambah lagi if di masing-masing ShapePrinter dan CharNeededCounter.

Misal bertambah logic shape Circle. Violasi OCP terjadi di 2 class tersebut.

if(shape.equalsIgnoreCase("square")){
  ...
} else if(shape.equalsIgnoreCase("triangle")){
  ...
} else if(shape.equalsIgnoreCase("circle")){
  ...
} else {
  ...
}

Di dalam contoh ini, if-else square dan triangle ada di 2 class. Pada kondisi nyata bila hal ini dibiarkan terjadi, if-else square dan triangle akan terus beranak-pinak bila ada kebutuhan logic lain.

Contoh kasus OCP lainnya adalah mengenai authentikasi User dalam sebuah game. Anggap username terdefinisikan memiliki format "[username]#[room_server]#[negara]". Ketika developer menginginkan adanya perubahan sistem terhadap penamaan tersebut, tentunya sangat dirugikan bila terjadi perubahan isi code secara signifikan dan tentunya merusak isi-isi code di sekitaran line-of-statement dimana code tersebut diubah.

public class GameAuthenticationService {
  private static ArrayList<GameID> users = GameServices.fetchUsers(); //fetch from server

  public static GameID getGameUserID(String username) {
    if (!username.matches("^[A-Za-z0-9]+#[0-9]{3,3}#[A-Z]{2}")) {
      throw new IllegalArgumentException("Wrong username!");
    }

    for (GameID user : users) {
      if (!user.getUsername().equals(username)) {
        return user;
      }
    }

    throw new NoSuchElementException("User not exists!");
  }
}

Bagian sebagian programmer, hal seperti ini tampak lumrah untuk menempatkan match username sesuai dengan spesifikasi yang diinginkan pada game tersebut. Lah kalau developer minta username diubah menjadi hanya username dan negara? atau formatnya berubah menjadi role, username, dan room? Sungguh dengan code seperti itu sangat menyulitkan potensi class tersebut tetap bertahan bahkan masuk ke tahap "production" karena adanya perubahan yang berulang kali.

Sebagai salah satu solusinya, kita dapat memisahkan proses matching username ke class baru bernama GameIDFormat dengan basis class GameIDFormatBuilder dimana dalam class tersebut akan ditampung regex-regex yang diinginkan oleh developer beserta prosesnya sehingga tidak menimbulkan perubahan-perubahan yang tidak diinginkan oleh class tersebut dan membuka peluang fitur tersebut untuk divariasikan dengan berbagai macam variasi penamaan yang diinginkan oleh game tersebut.

public class GameAuthenticationService {
  private static ArrayList<GameID> users = GameServices.fetchUsers(); //fetch from server

  public static GameID getGameUserID(String username) {
    return getGameUserID(username, new GameIDFormat());
  }

  public static GameID getGameUserID(String username, GameIDFormatBuilder gameIDFormat) {
    if (matchGameIDFormat(username, gameIDFormat)) {
      throw new IllegalArgumentException("Wrong username format!");
    }

    for (GameID user : users) {
      if (!user.getUsername().equals(username)) {
        return user;
      }
    }

    throw new NoSuchElementException("User not exists!");
  }

  public static boolean matchGameIDFormat(String username, GameIDFormatBuilder gameIDFormat) {
    return gameIDFormat.match(username);
  }
}

public class GameIDFormat extends GameIDFormatBuilder {
  private static final String USERNAME_REGEX = "[A-Za-z0-9]+";
  private static final String ROOM_ID_REGEX = "[0-9]{3,5}";
  private static final String COUNTRY_REGEX = "[A-Z]{2}";
  private static final char HASH_FIELD_SEPARATOR = '#';

  public GameIDFormat() {
    super(HASH_FIELD_SEPARATOR);
  }

  @Override
  public boolean match(String username) {
    return match(username, USERNAME_REGEX, ROOM_ID_REGEX, COUNTRY_REGEX);
  }
}

public abstract class GameIDFormatBuilder {
  private char fieldSeparator;

  protected GameIDFormatBuilder(char fieldSeparator) {
    this.fieldSeparator = fieldSeparator;
  }

  private String constructRegex(String... fieldRegexes) {
    String constructedRegex = "^";
    for (int i = 0; i < fieldRegexes.length; i++) {
      constructedRegex = constructedRegex.concat(fieldRegexes[i])
                           .concat(((i < fieldRegexes.length - 1) ? fieldSeparator : "") + "");
    }
    return constructedRegex;
  }

  protected boolean match(String username, String... fieldRegexes) {
    return username.matches(constructRegex(fieldRegexes));
  }

  public abstract boolean match(String username);
}

Dalam menerapkan prinsip OCP, salah satu cara tepat yang sering dilakukan oleh programmer adalah dengan mengubah Type Code menjadi Subclasses dengan membuatkan abstract class sesuai typenya beserta subclassnya yang diextend dari abstract superclass yang dibuat dan mengubah Conditional statement menjadi Polymorphism dengan cukup memanggil abstract method yang sudah diimplementasikan di masing-masing subclass sehingga jika ada jenis/type/variasi baru, misalkan Circle, kita tinggal buatkan class dengan melakukan extends dari class Shape.

Dalam kasus lanjutan, jika sewaktu-waktu class GameAuthenticationService.java menginginkan adanya fitur baru (terutama jika game tersebut ingin diintegrasikan dengan akun Steam/PlayStation Network/XBOX), maka class yang ingin dibuat dapat diextend dari abstract class GameIDFormatBuilder.java yang telah kita refactorkan sebelumnya. Misalnya pada contoh code dibawah:

public class SteamIDFormat extends GameIDFormatBuilder {
  private static final String USERNAME_REGEX = "[A-Za-z0-9]+";
  private static final String STEAM_ID_REGEX = "[0-9]{8}";
  private static final char HASH_FIELD_SEPARATOR = '#';

  public GameIDFormat() {
    super(HASH_FIELD_SEPARATOR);
  }

  @Override
  public boolean match(String username) {
    return match(username, USERNAME_REGEX, STEAM_ID_REGEX);
  }
}

Liskov Subtitution Principle (LSP)

"Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program."

Dalam prinsip Liskov Subtitution Principle, setiap object dalam abstraksi/class dapat digantikan dengan object dalam subclassnya tanpa harus mengubah behavior/jalannya suatu program. Selain itu, dalam LSP juga harus memastikan bahwa class-class yang diturunkan dari base class berjalan sesuai ekspektasi pemakai & keperluan base class itu sendiri.

Misal terdapat hierarki class yang meliputi Unggas dimana dalam unggas terdapat Ayam, dan Bebek. Secara deklaratifnya, struktur class ini terdefinisikan sebagai berikut:

public abstract class Unggas {
  private int kaki = 2;
  private String warna = "putih";
  public abstract void makan();
  public abstract String bunyi();
}

public class Ayam extends Unggas {
  public void makan() {
    System.out.println("Makan cacing dan biji");
  }
  public String bunyi() {
    return "kukuruyuk (berkokok)";
  }
}

public class Bebek extends Unggas {
  public void makan() {
    System.out.println("Makan rumput dan serangga");
  }
  public String bunyi() {
    return "kwek kwek";
  }
}

Dalam kasus ini, ketika suatu class ingin mendeklarasikan objek Unggas berjenis apapun (baik Ayam ataupun Bebek) dan memanggil behavior secara umum, class cukup memanggil method abstract yang telah dideklarasikan oleh masing-masing subclassnya tanpa harus menentukan jenis apa yang harus dipilih untuk berinteraksi. Misalnya:

// Dapat meyakinkan user bahwa behavior yang dipanggil sesuai dengan pilihan user
Unggas ternakUnggas = new Ayam();
...
ternakUnggas.makan(); // alias ((Ayam) ternakUnggas).makan();

Output:

Makan cacing dan biji

Namun terkadang dalam kasus tertentu, mendelegasikan sebuah method yang diimplementasikan dari base class ke method abstract lain terkadang dapat menimbulkan keraguan secara konseptual bahkan menimbulkan penolakan inheritance meski tujuannya untuk menyamai apa yang diinginkan pengguna itu sendiri.

Salah satu kasus pelanggaran LSP sendiri adalah Rectangle dan Square class case, dimana class Square merupakan subclass dari class Rectangle, namun saat mengatur panjang dan lebar dari Square, terdapat method yang mendelegasikan nilai tersebut ke method abstract lainnya.

public class Rectangle {
  private int width, height;
  public Rectangle (int width, int height) {
    setWidth(width);
    setHeight(height);
  }
  public void setWidth(int width) {
    this.width = width;
  }
  public void setHeight(int height) {
    this.height = height;
  }
  public int getWidth(int width) {
    return width;
  }
  public int getHeight(int height) {
    return height;
  }
  public int area() {
    return width * height;
  }
}

public class Square extends Rectangle {
  @Override
  public void setWidth(int width) {
    this.width = width;
  }
  @Override
  public void setHeight(int height) {
    setWidth(height);
  }
}

Pendelegasian isi method ke method lain dapat menimbulkan smell Refused Bequest karena proses yang diinginkan method tersebut ternyata tidak sesuai dengan keinginan/tujuan dari base class tersebut. Untuk menyelesaikan masalah tersebut, lakukan extract class & buat basis class untuk menghindari adanya interrelative conflict antar kedua shape dan turunkan basis class kepada kedua class yang mempunyai konflik kepentingan.

Untuk informasi mengenai pelanggaran LSP itu sendiri, dapat dilihat pada smell Refused Bequest (Liskov Substitution Violation).

Interface Segregation Principle (ISP)

"Many client-specific interfaces are better than one general-purpose interface."

-- Robert C. Martin

Interface Segregation Adapter

Prinsip ini menegaskan bahwa interface hanya berisikan deklarasi method-method yang akan dipakai sesuai kebutuhan class pemakainya. Hal ini tentunya mencegah adanya interface yang terlalu besar dan memaksakan class-class pemakainya untuk memakai semua method yang dideklarasikan meski tidak semua class memerlukannya.

Salah satu contoh penerapan prinsip yang salah, adalah kasus dimana terdapat interface HewanTernak dimana interface ini berisikan method-method yang akan dipakai oleh hewan ternak baik Ayam dan Sapi.

public interface HewanTernak {
  // Untuk ayam
  public Egg layEgg();
  public boolean hatchEgg(Egg egg);
  // Untuk sapi
  public Milk produceMilk();
}

public class Ayam implements HewanTernak {
  public Egg layEgg() {
    // lay an egg
  }
  public boolean hatchEgg(Egg egg) {
    // tetaskan telur (jika dibuahkan akan menetas)
  }
  public Milk produceMilk() {
    throw new Exception("Ayam mana bisa menghasilkan susu");
  }
}

public class Sapi implements HewanTernak {
  public Egg layEgg() {
    throw new Exception("Sapi mana bisa bertelur");
  }
  public boolean hatchEgg(Egg egg) {
    throw new Exception("Sapi mana bisa tetasi telur");
  }
  public Milk produceMilk() {
    // produksi susu dari sapi
  }
}

Pada contoh code tersebut, terdapat pelanggaran dimana Ayam harus menerima method-method penghasil susu sedangkan Sapi harus menerima method-method untuk bertelur padahal sapi tersebut tidak dapat menghasilkan telur. Keduanya tentunya mau tak mau harus menolak method yang tidak ia maui misalnya pada class Sapi harus menolak method layEgg() dan hatchEgg() karena tidak bisa bertelur.

Smell ini tentunya merujuk pada Rebellious Hierarchy ataupun Refused Bequest karena class harus menolak inheritance dari class lain.

Salah satu jalan keluarnya adalah dengan memecahkan interface HewanTernak menjadi 2 interface yaitu Penelur dan PenghasilSusu dimana masing-masing interface dapat dipakai pada class-class yang memerlukannya (seperti interface Penelur pada class Ayam dan burung-burungan).

public interface Penelur {
  // Anggap ada object bernama Egg
  public Egg layEgg();
  public boolean hatchEgg(Egg egg);
}

public interface PenghasilSusu {
  public Milk produceMilk();
}

public class Ayam implements Penelur {
  public Egg layEgg() {
    // lay an egg
  }
  public boolean hatchEgg(Egg egg) {
    // tetaskan telur (jika dibuahkan akan menetas)
  }
}

public class Sapi implements PenghasilSusu {
  public Milk produceMilk() {
    // produksi susu dari sapi
  }
}

Sebagai contoh penerapan ISP yang tepat adalah kasus interface Penelur dan PenghasilSusu pada class Ayam dan Sapi dimana interface Penelur diaplikasikan pada class Ayam dan PenghasilSusu diterapkan pada class Sapi. Pemakaian interface dengan method yang tepat tentunya mempermudah pemakaian kembali pada class-class yang baru sesuai kebutuhan mereka masing-masing tanpa adanya pemaksaan dari interface tersebut.

Dependency Inversion Principle (DIP)

A. "High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces)."

B. "Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions."

-- Robert C. Martin

Prinsip ini memastikan bahwa class yang berada pada dasar hierarki (terutama abstract class) tidak boleh bergantung pada class-class implementasi dan sebaliknya, class-class seharusnya dan sebaiknya bergantung pada abstraksi (abstract class/interfaces) bukan pada class yang konkrit (mempunyai Object).

Hal ini tentunya bertujuan agar class tidak saling bergantung satu sama lain dan mengurangi tingkat coupling yang tinggi dan tentunya memaksimalkan modularization agar struktur hierarki dapat dikembangkan sesuai kebutuhan dan requirement.

Direct class dependency vs with Dependency Inversion Principle

Salah satu cara paling efektif dan efisien dalam menerapkan prinsip DIP (Dependency Inversion Principle) adalah dengan menggunakan interface dengan menyantumkan method-method yang akan dipakai oleh class penggunanya.

Pada contoh kasus class OrderMakanan, terdapat sebuah abstract class Makanan yang berisikan 2 subclass yaitu NasiGoreng dan Indomie yang sama-sama mempunyai method serve() namun mempunyai behavior yang berbeda-beda.

/**
 *  Class yang berisikan menu-menu makanan
 */
public interface Makanan {
  public void serve();
}

public class NasiGoreng extends Makanan {
  @Override
  public void serve() {
    siapkanBahan();
    masak();
    serveOnPlate();
  }

  private void siapkanBahan() {
    // persiapkan bahan-bahan
  }
  private void masak() {
    // masak nasi goreng
  }
  private void serveOnPlate() {
    // sajikan di piring
  }
}

public class Indomie extends Makanan {
  @Override
  public void order() {
    rebusMie();
  }

  private void rebusMie() {
    // rebus mie instan
  }
}

/**
 *  Pesan makanan dari aplikasi
 */
public class OrderMakanan {
  Vector<Makanan> orderList = new Vector<>();

  public void tambahOrder(Makanan makanan) {
    orderList.add(makanan);
  }

  public void sajikanMakanan() {
    for (Makanan order : orderList) {
      // panggilkan method objek Makanan tanpa harus mengetahui jenisnya
      order.serve();
    }
  }
}

Dimana class OrderMakanan memanggil referensi object Makanan melalui method serve() yang terdeklarasi pada interface untuk keperluan penyajian tanpa mempedulikan dan mengetahui variasi-variasi class yang ada dalam object tersebut.

Selain itu, interface Makanan tidak bergantung pada class-class implementasinya karena class cukup melakukan polymorphism method call dimana object yang terasosiasikan dengannya akan menjalankan method tersebut secara mandiri sesuai variasi yang dideklarasikan (sesuai prinsip Liskov Subtitution Principle (LSP)).

Referensi