Solid code structure by SOLID principles
Struktur kode yang baik sangat diperlukan dalam sebuah software development. Dengan memiliki struktur kode yang baik, software yang dihasilkan akan lebih flexible, maintainable, serta mencapai fungsi yang diinginkan dan diperlukan. Tak hanya itu, struktur kode yang baik juga dapat memberi kemudahan untuk pengembangan selanjutnya dan mencegah kemungkinan adanya technical issue di kemudian hari. Namun, untuk mencapai hal tersebut, diperlukan waktu yang lebih lama dalam proses software development dibanding membuat software yang tidak mempedulikan struktur kodenya.
Dalam setiap model pemrograman, terdapat pendekatan dan guideline yang berbeda untuk memiliki struktur kode yang baik. Pembahasan kali ini akan lebih mengarah untuk model pemrograman OOP (Object Oriented Programming). Dalam model pemrograman OOP sendiri, terdapat SOLID principles yang ditemukan oleh Robert C. Martin di awal tahun 2000. SOLID principles merupakan akronim dari 5 object-oriented principles yang digunakan untuk meningkatkan desain dan implementasi dari object-oriented software.
Single Responsibility Principle
A class should have only one reason to change
Pada principle ini, sebuah kelas hanya dapat memiliki tepat 1 tanggung jawab, tidak boleh lebih. Tujuannya agar menghindari adanya high coupling pada kode. Coupling disini maksudnya adalah ketergantungan kode pada code lain. Jika terdapat sebuah perubahan pada salah satu bagian kode, maka berdampak pada bagian kode lainnya dan harus dilakukan perubahan pada kode tersebut juga.
class Employee {
public void save() {
// save employee data to database
}
public void calculateSalary() {
// calculate employee's salary
}
}
Design code diatas adalah contoh yang melanggar Single Responsibility Principle, karena class Employee memiliki 2 responsibility, yaitu untuk save data dan calculateSalary. Dengan code tersebut, rawan terdapat high coupling dan jika terdapat perubahan dapat berdampak ke kode lain.
class EmployeeDatabase {
public void save(Employee employee) {
// save employee data to database
}
}
class SalaryCalculator {
public double calculateSalary(Employee employee) {
// calculate employee's salary
}
}
Untuk mencegah high coupling, kedua responsibility sebelumnya, dapat dipisah dan dibuat menjadi class baru. Dengan begitu, jika ada perubahan implementasi pada salah satu class, akan memperkecil atau bahkan menghilangkan dampak perubahan pada class lainnya.
Open/Closed Principle
Software entities (classes, modules, functions, etc) should be open for extension but closed for modification.
Principle ini meng-encourage developer untuk menggunakan inheritance, composition, dan teknik lainnya untuk mencapai flexibility tanpa harus memodifikasi kode yang sudah ada. Dengan begitu, kode yang dimiliki akan memiliki scalability yang baik, karena dapat terus di extend untuk mencapai fungsi yang diperlukan di kemudian hari.
public class TransportCostCalculator {
public Double getCosts(Transport transport, Double distanceInKm) {
if (transport instanceof Car)
return distanceInKm * 10000;
if (transport instanceof Airplane)
return distanceInKm * 200000;
}
}
Implementasi kode diatas merupakan contoh kode yang melanggar Open/Closed principle. Dengan implementasi tersebut, jika terdapat suatu perubahan, misalnya penambahan jenis transport, maka class TransportCostCalculator tersebut perlu dilakukan perubahan kembali.
public interface TransportCostCalculator {
Double getCosts(Double distanceInKm);
}
public class CarCostCalculator implements TransportCostCalculator {
public Double getCosts(Double distanceInKm) {
return distanceInKm * 10000;
}
public class AirplaneCostCalculator implements TransportCostCalculator {
public Double getCosts(Double distanceInKm) {
return distanceInKm * 200000;
}
Setelah diubah, class tersebut menjadi lebih baik karena menerapkan Open/Closed principle. Dengan begitu, class TransportCostCalculator tidak perlu diubah kembali, dan hanya perlu mengextend class tersebut jika terdapat penambahan jenis transport baru. Implementasi diatas merupakan salah satu contoh penerapan Strategy Pattern.
Liskov Substitution Principle
Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
Inti dari principle ini adalah pada setiap object subclass harus bisa menggantikan object superclassnya tanpa mempengaruhi kebenaran program.
public class Rectangle {
private double height;
private double width;
public void setHeight(double h) { height = h; }
public void setWidht(double w) { width = w; }
...
}
public class Square extends Rectangle {
public void setHeight(double h) {
super.setHeight(h);
super.setWidth(h);
}
public void setWidth(double w) {
super.setHeight(w);
super.setWidth(w);
}
}
Kode diatas merupakan contoh kode yang melanggar Liskov Substitution Principle. Hal tersebut terjadi, karena class Square mengextends class Rectangle, yang berarti seharusnya class Square dapat mensubstitusi class Rectangle. Namun, pada class Square terdapat constraint tertentu yang mengharuskan width dan height bernilai sama, sehingga dapat mengakibatkan unexpected behaviour pada hasil lainnya, misalnya pada method getCircumference dan getArea.
Untuk mengatasi hal tersebut, dapat dibuat sebuah abstract class baru yaitu Shape yang memiliki abstract method getArea dan getCircumference.
public abstract class Shape {
public abstract int getArea();
}
public class Rectangle extends Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
public class Square extends Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
Liskov Substitution Principle merupakan principle yang baik, namun terdapat case dimana principle ini mungkin akan dilanggar. Salah satunya yaitu pada penerapan Template Pattern. Pattern tersebut memberikan kebebasan kepada developer untuk mengextend skeleton code yang berupa abstract class tanpa harus menerapkan semua abstract method nya. Hal tersebut dapat melanggar principle ini, karena subclass nya dapat mempengaruhi kebenaran program saat mensubstitusi superclassnya.
Interface Segregation Principle
Clients should not be forced to depend on interfaces they do not use.
Principle ini menyatakan bahwa clients seharusnya tidak dipaksakan untuk menggunakan sebuah interface yang tidak dibutuhkan client. Dengan begitu, seharusnya sebuah interface lebih terfokus pada behaviour tertentu dan tidak monolithic (semua interface digabungkan menjadi 1).
public interface Animal {
public void menyusui();
public void bertelur();
public void jalan();
public void terbang();
}
Dengan design interface seperti itu, saat kita menggunakan salah satu method interface, kita dipaksa untuk menggunakan method lainnya, padahal belum tentu diperlukan. Misalnya kita ingin membuat class Sapi yang hanya menggunakan method menyusui() dan jalan(), maka method lainnya tidak diperlukan, tetapi tetap perlu diimplement. Seharusnya method bertelur() dan terbang() tidak perlu diimplement.
public interface Lactatable {
public void menyusui();
}
public interface Walkable {
public void jalan();
}
public interface Spawnable {
public void bertelur();
}
public interface Flyable {
public void terbang();
}
Dengan desain ini, maka clients lebih fleksibel dalam memlih interface yang dibutuhkan untuk digunakan.
Dependency Inversion Principle
High-level modules should not depend on low-level modules; both should depend on abstractions.
Maksud dari high-level modules adalah modul yang memiliki cakupan lebih besar dari business logic dan functionality dari sistem. High-level modules memiliki ketergantungan (dependency) terhadap low-level modules yang merupakan modul dengan cakupan yang lebih kecil dan memiliki fungsionalitas yang lebih spesifik.
Dependency terhadap concrete class pada high-level dan low-level modules dapat membatasi fleksibilitasnya. Oleh karena itu, seharusnya keduanya bergantung pada abstraksi seperti class interface dan abstract.
public class SmartPhone {
private Processor processor;
public SmartPhone(Processor proc) {
processor = proc;
}
public Processor getProcessor(){ return processor; }
public void on() {
processor.start()
}
}
public class Processor {
public void start() {...}
}
Kode diatas merupakan contoh implementasi yang melanggar Dependency Inversion Principle. Hal tersebut terjadi karena class Smartphone memiliki dependency processor pada class Processor yang merupakan concrete class. Dengan implementasi seperti ini, processor menjadi hanya 1 jenis. Padahal, seharusnya processor yang digunakan oleh smartphone dapat beragam jenisnya. Oleh karena itu, diperlukan abstraksi untuk memberikan fleksibilitas pada dependency tersebut.
public class SmartPhone {
private Processor processor;
public SmartPhone(Processor proc) {
processor = proc;
}
public Processor getProcessor(){ return processor; }
public void on() {
processor.start();
}
}
public interface Processor {
public void start();
}
public class MediaTek implements Processor {
public void start() {...};
}
public class Snapdragon implements Processor {
public void start() {...};
}
Kesimpulan
SOLID principles merupakan guidelines yang sangat baik digunakan untuk mengembangkan desain dan implementasi object-oriented code. Namun, bukan berarti dalam kondisi apapun, kita harus menerapkan kelima principles tersebut. Terdapat kondisi tertentu dimana salah satu atau lebih principle tidak dapat diterapkan, dan jika dipaksakan untuk diterapkan justru membuat kode lebih kompleks dan susah untuk di maintain. Oleh karena itu, kita sebagai software developer harus peka dan menyadari akan kebutuhan software dan dapat menentukan penggunaan principle dengan tepat.