Prinsip SOLID dari Pemrograman Berorientasi Objek Dijelaskan dalam Bahasa Inggris Biasa

Prinsip SOLID adalah lima prinsip desain kelas Berorientasi Objek. Mereka adalah seperangkat aturan dan praktik terbaik yang harus diikuti saat merancang struktur kelas.

Kelima prinsip ini membantu kita memahami kebutuhan pola desain tertentu dan arsitektur perangkat lunak secara umum. Jadi saya percaya bahwa ini adalah topik yang harus dipelajari setiap pengembang.

Artikel ini akan mengajarkan Anda semua yang perlu Anda ketahui untuk menerapkan prinsip SOLID ke proyek Anda.

Kami akan mulai dengan melihat sejarah istilah ini. Kemudian kita akan masuk ke detail intinya - mengapa dan bagaimana setiap prinsip - dengan membuat desain kelas dan memperbaikinya selangkah demi selangkah.

Jadi, ambillah secangkir kopi atau teh dan ayo langsung masuk!

Latar Belakang

Prinsip SOLID pertama kali diperkenalkan oleh Ilmuwan Komputer terkenal Robert J. Martin (alias Paman Bob) dalam makalahnya pada tahun 2000. Namun akronim SOLID diperkenalkan kemudian oleh Michael Feathers.

Paman Bob juga penulis buku laris Clean Code and Clean Architecture , dan merupakan salah satu peserta "Agile Alliance".

Oleh karena itu, tidak mengherankan jika semua konsep pengkodean yang bersih, arsitektur berorientasi objek, dan pola desain ini entah bagaimana terhubung dan saling melengkapi satu sama lain.

Mereka semua melayani tujuan yang sama:

"Untuk membuat kode yang dapat dipahami, dapat dibaca, dan dapat diuji yang dapat dikerjakan secara kolaboratif oleh banyak pengembang."

Mari kita lihat setiap prinsip satu per satu. Mengikuti akronim SOLID, mereka adalah:

  • The S perapian di tungku Prinsip Tanggung Jawab
  • The O pena-Tertutup Prinsip
  • The L iskov Prinsip Pergantian
  • The I nterface Pemisahan Prinsip
  • The D ependency Pembalikan Prinsip

Prinsip Tanggung Jawab Tunggal

Prinsip Tanggung Jawab Tunggal menyatakan bahwa kelas harus melakukan satu hal dan oleh karena itu harus memiliki hanya satu alasan untuk berubah .

Untuk menyatakan prinsip ini secara lebih teknis: Hanya satu potensi perubahan (logika database, logika logging, dan sebagainya.) Dalam spesifikasi perangkat lunak yang dapat memengaruhi spesifikasi kelas.

Ini berarti bahwa jika kelas adalah wadah data, seperti kelas Buku atau kelas Siswa, dan memiliki beberapa bidang terkait entitas itu, itu harus berubah hanya ketika kita mengubah model data.

Mengikuti Prinsip Tanggung Jawab Tunggal itu penting. Pertama-tama, karena banyak tim yang berbeda dapat mengerjakan proyek yang sama dan mengedit kelas yang sama untuk alasan yang berbeda, ini dapat menyebabkan modul yang tidak kompatibel.

Kedua, membuat kontrol versi lebih mudah. Misalnya, kita memiliki kelas persistensi yang menangani operasi database, dan kita melihat perubahan dalam file tersebut di GitHub commit. Dengan mengikuti SRP, kita akan tahu bahwa ini terkait dengan penyimpanan atau hal-hal yang berhubungan dengan database.

Gabungkan konflik adalah contoh lain. Mereka muncul ketika tim yang berbeda mengubah file yang sama. Tetapi jika SRP diikuti, konflik yang akan muncul lebih sedikit - file akan memiliki satu alasan untuk berubah, dan konflik yang ada akan lebih mudah diselesaikan.

Jebakan dan Anti-pola Umum

Pada bagian ini kita akan melihat beberapa kesalahan umum yang melanggar Prinsip Tanggung Jawab Tunggal. Kemudian kita akan membahas beberapa cara untuk memperbaikinya.

Kami akan melihat kode untuk program faktur toko buku sederhana sebagai contoh. Mari kita mulai dengan menentukan kelas buku yang akan digunakan dalam faktur kita.

class Book { String name; String authorName; int year; int price; String isbn; public Book(String name, String authorName, int year, int price, String isbn) { this.name = name; this.authorName = authorName; this.year = year; this.price = price; this.isbn = isbn; } } 

Ini adalah kelas buku sederhana dengan beberapa bidang. Tidak ada yang mewah. Saya tidak membuat bidang menjadi pribadi sehingga kita tidak perlu berurusan dengan pengambil dan penyetel dan sebagai gantinya dapat fokus pada logika.

Sekarang mari kita buat kelas faktur yang akan berisi logika untuk membuat faktur dan menghitung harga total. Untuk saat ini, anggaplah toko buku kita hanya menjual buku dan tidak ada yang lain.

public class Invoice { private Book book; private int quantity; private double discountRate; private double taxRate; private double total; public Invoice(Book book, int quantity, double discountRate, double taxRate) { this.book = book; this.quantity = quantity; this.discountRate = discountRate; this.taxRate = taxRate; this.total = this.calculateTotal(); } public double calculateTotal() { double price = ((book.price - book.price * discountRate) * this.quantity); double priceWithTaxes = price * (1 + taxRate); return priceWithTaxes; } public void printInvoice() { System.out.println(quantity + "x " + book.name + " " + book.price + "$"); System.out.println("Discount Rate: " + discountRate); System.out.println("Tax Rate: " + taxRate); System.out.println("Total: " + total); } public void saveToFile(String filename) { // Creates a file with given name and writes the invoice } }

Inilah kelas faktur kami. Ini juga berisi beberapa bidang tentang faktur dan 3 metode:

  • calculateTotal metode, yang menghitung harga total,
  • printInvoice , yang harus mencetak faktur ke konsol, dan
  • saveToFile , bertanggung jawab untuk menulis faktur ke file.

Anda harus memberi diri Anda waktu sejenak untuk memikirkan tentang apa yang salah dengan desain kelas ini sebelum membaca paragraf berikutnya.

Ok jadi apa yang terjadi disini? Kelas kami melanggar Prinsip Tanggung Jawab Tunggal dengan berbagai cara.

Pelanggaran pertama adalah metode printInvoice , yang berisi logika pencetakan kami. SRP menyatakan bahwa kelas kita seharusnya hanya memiliki satu alasan untuk berubah, dan alasan tersebut adalah perubahan dalam perhitungan faktur untuk kelas kita.

Tetapi dalam arsitektur ini, jika kita ingin mengubah format pencetakan, kita perlu mengubah kelasnya. Inilah sebabnya mengapa kita tidak boleh memiliki logika pencetakan yang dicampur dengan logika bisnis di kelas yang sama.

Ada metode lain yang melanggar SRP di kelas kami: metode saveToFile . Ini juga merupakan kesalahan yang sangat umum untuk mencampur logika ketekunan dengan logika bisnis.

Jangan hanya berpikir dalam istilah menulis ke file - itu bisa menyimpan ke database, membuat panggilan API, atau hal-hal lain yang terkait dengan persistensi.

Jadi bagaimana kami dapat memperbaiki fungsi cetak ini, Anda mungkin bertanya.

Kita dapat membuat kelas baru untuk pencetakan dan logika persistensi kita sehingga kita tidak perlu lagi mengubah kelas faktur untuk tujuan tersebut.

Kami membuat 2 kelas, InvoicePrinter dan InvoicePersistence, dan memindahkan metode.

public class InvoicePrinter { private Invoice invoice; public InvoicePrinter(Invoice invoice) { this.invoice = invoice; } public void print() { System.out.println(invoice.quantity + "x " + invoice.book.name + " " + invoice.book.price + " $"); System.out.println("Discount Rate: " + invoice.discountRate); System.out.println("Tax Rate: " + invoice.taxRate); System.out.println("Total: " + invoice.total + " $"); } }
public class InvoicePersistence { Invoice invoice; public InvoicePersistence(Invoice invoice) { this.invoice = invoice; } public void saveToFile(String filename) { // Creates a file with given name and writes the invoice } }

Sekarang struktur kelas kami mematuhi Prinsip Tanggung Jawab Tunggal dan setiap kelas bertanggung jawab atas satu aspek aplikasi kami. Bagus!

Prinsip Tertutup Terbuka

Prinsip Tertutup Terbuka mensyaratkan bahwa kelas harus terbuka untuk perluasan dan tertutup untuk modifikasi.

Modifikasi berarti mengubah kode kelas yang ada, dan ekstensi berarti menambahkan fungsionalitas baru.

Jadi apa yang prinsip ini ingin katakan adalah: Kita harus bisa menambahkan fungsionalitas baru tanpa menyentuh kode yang ada untuk kelas. Ini karena setiap kali kami memodifikasi kode yang ada, kami mengambil risiko menciptakan potensi bug. Jadi kita harus menghindari menyentuh kode produksi yang teruji dan andal (kebanyakan) jika memungkinkan.

Tetapi bagaimana kami akan menambahkan fungsionalitas baru tanpa menyentuh kelas, Anda mungkin bertanya. Ini biasanya dilakukan dengan bantuan antarmuka dan kelas abstrak.

Sekarang kita telah membahas dasar-dasar prinsipnya, mari kita terapkan ke aplikasi Faktur kita.

Katakanlah bos kita mendatangi kita dan berkata bahwa mereka ingin faktur disimpan ke database sehingga kita dapat mencarinya dengan mudah. Kami pikir oke, ini gampang bos, tunggu sebentar!

Kami membuat database, menyambungkannya, dan menambahkan metode penyimpanan ke kelas InvoicePersistence kami :

public class InvoicePersistence { Invoice invoice; public InvoicePersistence(Invoice invoice) { this.invoice = invoice; } public void saveToFile(String filename) { // Creates a file with given name and writes the invoice } public void saveToDatabase() { // Saves the invoice to database } }

Sayangnya kami, sebagai pengembang malas untuk toko buku, tidak merancang kelas agar mudah diperpanjang di masa mendatang. Jadi untuk menambahkan fitur ini, kami telah memodifikasi kelas InvoicePersistence .

Jika desain kelas kita mematuhi prinsip Terbuka-Tertutup, kita tidak perlu mengubah kelas ini.

Jadi, sebagai pengembang malas tapi pintar untuk toko buku, kami melihat masalah desain dan memutuskan untuk memfaktorkan ulang kode untuk mematuhi prinsip.

interface InvoicePersistence { public void save(Invoice invoice); }

Kami mengubah jenis InvoicePersistence ke Interface dan menambahkan metode penyimpanan. Setiap kelas persistensi akan menerapkan metode penyimpanan ini.

public class DatabasePersistence implements InvoicePersistence { @Override public void save(Invoice invoice) { // Save to DB } }
public class FilePersistence implements InvoicePersistence { @Override public void save(Invoice invoice) { // Save to file } }

Jadi struktur kelas kita sekarang terlihat seperti ini:

Sekarang logika ketekunan kami dengan mudah dapat diperpanjang. Jika bos kami meminta kami untuk menambahkan database lain dan memiliki 2 jenis database yang berbeda seperti MySQL dan MongoDB, kami dapat dengan mudah melakukannya.

Anda mungkin berpikir bahwa kita bisa membuat banyak kelas tanpa antarmuka dan menambahkan metode penyimpanan ke semuanya.

Tapi mari kita mengatakan bahwa kita memperluas aplikasi kami dan memiliki beberapa kelas ketekunan seperti InvoicePersistence , BookPersistence dan kami membuat PersistenceManager kelas yang mengelola semua kelas ketekunan:

public class PersistenceManager { InvoicePersistence invoicePersistence; BookPersistence bookPersistence; public PersistenceManager(InvoicePersistence invoicePersistence, BookPersistence bookPersistence) { this.invoicePersistence = invoicePersistence; this.bookPersistence = bookPersistence; } }

Kita sekarang dapat meneruskan kelas apa pun yang mengimplementasikan antarmuka InvoicePersistence ke kelas ini dengan bantuan polimorfisme. Ini adalah fleksibilitas yang disediakan antarmuka.

Prinsip Substitusi Liskov

The Liskov Substitution Principle states that subclasses should be substitutable for their base classes.

This means that, given that class B is a subclass of class A, we should be able to pass an object of class B to any method that expects an object of class A and the method should not give any weird output in that case.

This is the expected behavior, because when we use inheritance we assume that the child class inherits everything that the superclass has. The child class extends the behavior but never narrows it down.

Therefore, when a class does not obey this principle, it leads to some nasty bugs that are hard to detect.

Liskov's principle is easy to understand but hard to detect in code. So let's look at an example.

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

We have a simple Rectangle class, and a getArea function which returns the area of the rectangle.

Now we decide to create another class for Squares. As you might know, a square is just a special type of rectangle where the width is equal to the height.

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

Our Square class extends the Rectangle class. We set height and width to the same value in the constructor, but we do not want any client (someone who uses our class in their code) to change height or weight in a way that can violate the square property.

Therefore we override the setters to set both properties whenever one of them is changed. But by doing that we have just violated the Liskov substitution principle.

Let's create a main class to perform tests on the getArea function.

class Test { static void getAreaTest(Rectangle r) { int width = r.getWidth(); r.setHeight(10); System.out.println("Expected area of " + (width * 10) + ", got " + r.getArea()); } public static void main(String[] args) { Rectangle rc = new Rectangle(2, 3); getAreaTest(rc); Rectangle sq = new Square(); sq.setWidth(5); getAreaTest(sq); } }

Your team's tester just came up with the testing function getAreaTest and tells you that your getArea function fails to pass the test for square objects.

In the first test, we create a rectangle where the width is 2 and the height is 3 and call getAreaTest. The output is 20 as expected, but things go wrong when we pass in the square. This is because the call to setHeight function in the test is setting the width as well and results in an unexpected output.

Interface Segregation Principle

Segregation means keeping things separated, and the Interface Segregation Principle is about separating the interfaces.

The principle states that many client-specific interfaces are better than one general-purpose interface. Clients should not be forced to implement a function they do no need.

This is a simple principle to understand and apply, so let's see an example.

public interface ParkingLot { void parkCar(); // Decrease empty spot count by 1 void unparkCar(); // Increase empty spots by 1 void getCapacity(); // Returns car capacity double calculateFee(Car car); // Returns the price based on number of hours void doPayment(Car car); } class Car { }

We modeled a very simplified parking lot. It is the type of parking lot where you pay an hourly fee. Now consider that we want to implement a parking lot that is free.

public class FreeParking implements ParkingLot { @Override public void parkCar() { } @Override public void unparkCar() { } @Override public void getCapacity() { } @Override public double calculateFee(Car car) { return 0; } @Override public void doPayment(Car car) { throw new Exception("Parking lot is free"); } }

Our parking lot interface was composed of 2 things: Parking related logic (park car, unpark car, get capacity) and payment related logic.

But it is too specific. Because of that, our FreeParking class was forced to implement payment-related methods that are irrelevant. Let's separate or segregate the interfaces.

Kami sekarang telah memisahkan tempat parkir. Dengan model baru ini, kami bahkan dapat melangkah lebih jauh dan membagi PaidParkingLot untuk mendukung berbagai jenis pembayaran.

Sekarang model kami jauh lebih fleksibel, dapat diperpanjang, dan klien tidak perlu menerapkan logika yang tidak relevan karena kami hanya menyediakan fungsionalitas terkait parkir di antarmuka tempat parkir.

Prinsip Pembalikan Ketergantungan

Prinsip Dependency Inversion menyatakan bahwa kelas kita harus bergantung pada antarmuka atau kelas abstrak daripada kelas dan fungsi konkret.

Dalam artikelnya (2000), Paman Bob merangkum prinsip tersebut sebagai berikut:

"Jika OCP menyatakan tujuan arsitektur OO, DIP menyatakan mekanisme utama".

These two principles are indeed related and we have applied this pattern before while we were discussing the Open-Closed Principle.

We want our classes to be open to extension, so we have reorganized our dependencies to depend on interfaces instead of concrete classes. Our PersistenceManager class depends on InvoicePersistence instead of the classes that implement that interface.

Conclusion

In this article, we started with the history of SOLID principles, and then we tried to acquire a clear understanding of the why's and how's of each principle. We even refactored a simple Invoice application to obey SOLID principles.

I want to thank you for taking the time to read the whole article and I hope that the above concepts are clear.

I suggest keeping these principles in mind while designing, writing, and refactoring your code so that your code will be much more clean, extendable, and testable.

If you are interested in reading more articles like this, you can subscribe to my blog's mailing list to get notified when I publish a new article.