DEV Community

Cover image for Mengenal SOLID: Memahami Setiap Prinsip dan Menerapkannya Secara Efektif
Juan Angela Alma
Juan Angela Alma

Posted on

Mengenal SOLID: Memahami Setiap Prinsip dan Menerapkannya Secara Efektif

Solid principle adalah suatu prinsip penulisan code pada pengambangan sofware dengan tujuan untuk membuat code lebih mudah dimengerti, mudah dibaca oleh developer lain secara kolaboratif.

SOLID singkatan dari:

S → Single-responsibility Principle

O → Open-close principle

L → Liskov Substitution Principle

I → Interface Segregation Principle

D → Dependency Inversion Principle

Single-responsibility Principle

Menurut Uncle Bob sebuah class atau module harus mempunyai satu tugas saja.

Sebagai contoh kita akan membuat class:

  1. Calculator → yang bertugas melakukan operasi mamematika.
  2. Output → yang betugas menangani tampilan hasil.
<?php

class Calculator {
  public function add($num1, $num2)
  {
    return $num1 + $num2;
  }
}

class Output {
  public function display($result)
  {
    echo "Hasil: $result\n";
  }
}

$calculator = new Calculator();
$output = new Output();

$result = $calculator->add(1, 2);
$output->display($result);
Enter fullscreen mode Exit fullscreen mode

Dalam hal ini Calculator hanya mempunyai tugas untuk melakukan operasi matematika, dia tidak menangani tampilan hasil, maka dari itu ada class Output yang bertugas menampilkan hasil dari penjumlahan sebelumnya. Jadi masing-masing memiliki satu tugas yang jelas.

Dalam contoh yang lebih rumit:

<?php

class ProductManager {
  public function saveProduct(Product $product)
  {
    echo "Menyimpan data product ke database: " . $product->getName();
  }
}

class Product {
  private $name;

  public function __construct($name)
  {
    $this->name = $name;
  }

  public function getName()
  {
    return $this->name;
  }
}

class EmailService {
  public function sendConfirmationEmail($email, $productName)
  {
    echo "Mengirim email konfirmasi untuk produk {$productName} ke {$email}\n";
  }
}

class SalesManager {
  private $productManager;
  private $emailService;

  public function __construct() 
  {
    $this->productManager = new ProductManager();
    $this->emailService = new EmailService();
  }

  public function sellProduct(Product $product, $customerEmail)
  {
    $this->productManager->saveProduct($product);
    $this->emailService->sendConfirmationEmail($customerEmail, $product->getName());
  }
}

$product = new Product("Laptop");
$salesManager = new SalesManager();
$salesManager->sellProduct($product, "example@gmail.com");
Enter fullscreen mode Exit fullscreen mode

Pada code di atas:

  1. ProductManager → bertugas untuk menyimpan data product ke database
  2. EmailService → bertugas untuk mengirimkan email konfirmasi ke customer email
  3. SalesManager → bertugas untuk mengelola penjualan yang melibatkan class ProductManager dan EmailService, namun tidak serta merta mengetahui detail implementasi dari proses menyimpan data dan mengirim email.

Dengan menerapkan Single-responsibility Principle, setiap kelas memiliki satu tanggung jawab utama dan tidak melakukan lebih dari yang diperlukan.

Open-close Principle

Suatu class harus dapat diturunkan atau diekstensi namun tidak boleh dimodifikasi. Dengan kata lain, code yang sudah ada seharusnya dapat dikembangkan tanpa harus mengubahnya.

<?php

class EmailMessage {
  public string $email;
  public string $subject;
  public string $body;

  public function __construct(string $email, string $subject, string $body)
  {
    $this->email = $email;
    $this->subject = $subject;
    $this->body = $body;
  }
}

class SmsMessage {
  public string $phone_number;
  public string $body;

  public function __construct(string $phone_number, string $body)
  {
    $this->phone_number = $phone_number;
    $this->body = $body;
  }
}

class MessageSender {
  public function sendMessage($message)
  {
    if (is_a($message, 'EmailMessage')) {
      echo "Process to send email with email: $message->email, subject: $message->subject, body: $message->body";
    } else if(is_a($message, 'SmsMessage')) {
      echo "Process to send sms with phone number: $message->phone_number, body: $message->body";
    }
  }
}

$messageSender = new MessageSender();

// Mengirimkan pesan melalui email
$emailMessage = new EmailMessage("john@gmail.com", "This is subject", "This is body");
$messageSender->sendMessage($emailMessage);

// Mengirimkan pesan melalui SMS
$smsMessage = new SmsMessage("083849203930", "This is body");
$messageSender->sendMessage($smsMessage);
Enter fullscreen mode Exit fullscreen mode

Dalam code di atas, terdapat pengecekan di method sendMessage pada class MessageSender, ini melanggar aturan open-close principle karena jika ada message baru misal PushNotification, anda harus mengubah implementasi dari method sendMessage dengan menambahkan if/else lagi.

Salah satu agar code tersebut lebih baik adalah menghapus logic pada methode sendMessage dan memindahkannya pada setiap class message yang ada.

<?php

class EmailMessage {
  public string $email;
  public string $subject;
  public string $body;

  public function __construct(string $email, string $subject, string $body)
  {
    $this->email = $email;
    $this->subject = $subject;
    $this->body = $body;
  }

    // method baru
  public function send()
  {
      echo "Process to send email with email: $this->email, subject: $this->subject, body: $this->body";
  }
}

class SmsMessage {
  public string $phone_number;
  public string $body;

  public function __construct(string $phone_number, string $body)
  {
    $this->phone_number = $phone_number;
    $this->body = $body;
  }

  // method baru
  public function send()
  {
    echo "Process to send sms with phone number: $this->phone_number, body: $this->body";
  }
}

class MessageSender {
  public function sendMessage($message)
  {
    $message->send();
  }
}

$messageSender = new MessageSender();

// Mengirimkan pesan melalui email
$emailMessage = new EmailMessage("john@gmail.com", "This is subject", "This is body");
$messageSender->sendMessage($emailMessage);

// Mengirimkan pesan melalui SMS
$smsMessage = new SmsMessage("083849203930", "This is body");
$messageSender->sendMessage($smsMessage);
Enter fullscreen mode Exit fullscreen mode

Namun, terdapat satu masalah. Bagaimana kita tahu bahwa object $message pada sendMessage mempunyai method send()?

Solusinya tambahkan interface, ini adalah salah satu dari penerapan SOLID juga.

interface MessageInterface {
  public function send();
}
Enter fullscreen mode Exit fullscreen mode
class EmailMessage implements MessageInterface
{
    // ...
}

class SmsMessage implements MessageInterface
{
    // ...
}

class MessageSender {
  public function sendMessage(MessageInterface $message)
  {
    /// ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Sekarang, setelah code dirubah sesuai prinsip open-close, maka jika ada class message baru maka tinggal menambahkan classnya dan implements MessageInterface.

Liskov Substitution Principle

Prinsip ini ditemukan oleh Barbara Liskov seorang ilmuan komputer pada tahun 1987. Prinsip Liskov Substitution menyatakan bahwa objek dari class turunan harus dapat menggantikan objek dari class induk tanpa mengganggu perilaku aplikasi.

Misalnya, jika class A memiliki method M(), dan kita memiliki class B yang mewarisi class A, maka ketika B digunakan sebagai pengganti A di dalam aplikasi, metode M() harus berperilaku sesuai yang diharapkan, tanpa menyebabkan error atau mengubah logika aplikasi.

Secara umum, prinsip Liskov Substitution mengadopsi polimorpisme yang benar, dimana objek class yang berbeda dapat digunakan seragam dengan mempertahankan perilaku yang konsisten.

<?php

class Employee {
  protected $name;
  protected $salary;

  public function __construct($name, $salary)
  {
    $this->name = $name;
    $this->salary = $salary;
  }

  public function calculate_pay()
  {
    return $this->salary;
  }
}

class PermanentEmployee extends Employee {
  public function __construct($name, $salary)
  {
    parent::__construct($name, $salary);
  }

  // untuk menghitung gaji karyawan tetap
  public function calculate_pay()
  {
    return $this->salary;
  }
}

class ContractEmployee extends Employee {
  protected $hours_worked;
  protected $hourly_rate;

  public function __construct($name, $hours_worked, $hourly_rate)
  {
    parent::__construct($name, 0);
    $this->hours_worked = $hours_worked;
    $this->hourly_rate = $hourly_rate;
  }

  // untuk menghitung gaji karyawan kontrak
  public function calculate_pay()
  {
    return $this->hourly_rate * $this->hours_worked;
  }
}

function total_pay($employees)
{
  $total = 0;
  foreach($employees as $employee) {
    $total += $employee->calculate_pay();
  }
  return $total;
}

$permanent_employee = new PermanentEmployee("John", 3000);
$contract_employee = new ContractEmployee("Jane", 40, 20);

$employees = [$permanent_employee, $contract_employee];

echo "Total gaji karyawan: " . total_pay($employees);
Enter fullscreen mode Exit fullscreen mode

Dalam contoh di atas, Kita punya dua class yaitu ContractEmployee dan PermanentEmployee, semua class itu turunan dari class Employee. Keduanya memilikki method calculate_pay() yang memiliki perilaku yang berbeda sesuai dengan jenis karyawan masing-masing. Hal ini menunjukkan bahwa class (PermanentEmployee dan ContractEmployee) dapat digunakan sebagai pengganti class induk Employee tanpa mengubah perilaku yang diharapkan sesuai dengan Prinsip Substitusi Liskov.

Interface Segregation Principle

Pada Interface Segregation Principle menyatakan bahwa klien tidak boleh dipaksa untuk menerapkan method yang sebenarnya tidak mereka gunakan.

Kita akan menggunakan contoh kasus Message tadi, misal kita punya message baru dengan class TelegramMessage. TelegramMessage mempunyai fitur untuk unsend message.

class TelegramMessage {
  public string $username;
  public string $message;

  public function __construct(string $username, string $message)
  {
    $this->username = $username;
    $this->message = $message;
  }

  public function send()
  {
    echo "Process to send telegram message to @$this->username: $this->message";
  }

  public function unsend()
  {
    echo "Process to unsend telegram message to @$this->username: $this->message";
  }
}
Enter fullscreen mode Exit fullscreen mode

Pastinya kita akan implements TelegramMessage ke interface MessageInterface.

class TelegramMessage implements MessageInterface {
  //
}
Enter fullscreen mode Exit fullscreen mode

Nah, ini mengharuskan penambahan method pada MessageInterface menjadi:

interface MessageInterface {
  public function send();
  public function unsend();
}
Enter fullscreen mode Exit fullscreen mode

Setelah penambahan method pada interface MessageInterface, muncul problem yaitu kita harus menambahkan method unsend pada EmailMessage dan SmsMessage. Padahal kedua class itu seharusnya tidak bisa melakukan unsend.

Mari kita coba perbaiki penerapan code nya.

Alih alih menambahkan methode unsend pada MessageInterface, mari kita bikin interface baru yang mempunyai method unsend:

interface UnsendableMessage {
  public function unsend();
}
Enter fullscreen mode Exit fullscreen mode

Dan mengimplementasikannya hanya pada TelegramMessage aja:

class EmailMessage implements MessageInterface
{
    // ...
}

class SmsMessage implements MessageInterface
{
    // ...
}

class TelegramMessage implements MessageInterface, UnsendableMessage {
  /// ...
}
Enter fullscreen mode Exit fullscreen mode

Dengan perubahan itu maka EmailMessage dan SmsMessage tidak harus mempunyai method unsend.

Dependency Incersion

Dependency Incersion menyatakan bahwa sebuah entitas itu bergantung pada abstraction. Artinya class tingkat tinggi tidak boleh bergantung pada class tingkat bawah. Berikut adalah contoh penerapan yang salah:

<?php

class MySQLConnection
{
    public function connect()
    {
        // handle the database connection
        return 'Database connection';
    }
}

class UserManager
{
    private $dbConnection;

    public function __construct(MySQLConnection $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}
Enter fullscreen mode Exit fullscreen mode

Disini MySQLConnection merupakan class tingkat bawah sedangkan UserManager merupakan class tingkat tinggi. Ini muncul permasalahan, jika kita ingin merubah engine database dari MySQL menjadi PostgreeSQL, maka kita harus mengubah code pada UserManager.

UserManager seharusnya tidak peduli tentang engine database yang digunakan. Untuk mengatasi problem ini maka kita harus mengubah UserManager agar bergantung pada abstaction.

interface DBConnectionInterface {
  public function connect();
}
Enter fullscreen mode Exit fullscreen mode

Mengubah MySQLConnection agar implements interface tersebut.

class MySQLConnection implements DBConnectionInterface
{
    /// ...
}
Enter fullscreen mode Exit fullscreen mode

Serta alih-alih menulis MySQLConnection pada construction UserManager, diganti dengan interface DBConnectionInterface karena UserManager tidak peduli database engine apa yang digunakan pada aplikasi tersebut.

class UserManager
{
    private $dbConnection;

    public function __construct(DBConnectionInterface $dbConnection)
    {
        /// ...
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)