Prinsip SOLID Pengaturcaraan Berorientasikan Objek Dijelaskan dalam Bahasa Inggeris Plain

Prinsip SOLID adalah lima prinsip reka bentuk kelas Berorientasikan Objek. Mereka adalah sekumpulan peraturan dan amalan terbaik yang harus dipatuhi semasa merancang struktur kelas.

Lima prinsip ini membantu kita memahami perlunya corak reka bentuk tertentu dan seni bina perisian secara umum. Oleh itu, saya percaya bahawa ia adalah topik yang harus dipelajari oleh setiap pembangun.

Artikel ini akan mengajar anda semua yang perlu anda ketahui untuk menerapkan prinsip SOLID pada projek anda.

Kita akan mulakan dengan melihat sejarah istilah ini. Kemudian kita akan masuk ke dalam perincian yang menarik - mengapa dan bagaimana setiap prinsip - dengan membuat reka bentuk kelas dan memperbaikinya selangkah demi selangkah.

Oleh itu, ambil secawan kopi atau teh dan mari masuk!

Latar belakang

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

Uncle Bob juga merupakan pengarang buku terlaris Clean Code and Clean Architecture , dan merupakan salah satu peserta "Agile Alliance".

Oleh itu, tidak mengejutkan bahawa semua konsep pengkodan bersih ini, seni bina berorientasikan objek, dan corak reka bentuk entah bagaimana saling berkaitan dan saling melengkapi antara satu sama lain.

Semuanya mempunyai tujuan yang sama:

"Untuk membuat kod yang dapat difahami, dapat dibaca, dan dapat diuji yang dapat diusahakan oleh banyak pembangun secara kolaboratif."

Mari kita perhatikan setiap prinsip satu persatu. Berikutan singkatan SOLID, mereka adalah:

  • The S ingle Tanggungjawab Prinsip
  • The O pen-Closed Prinsip
  • The L iskov Penggantian Prinsip
  • Prinsip Pengasingan Bahagian I
  • The D ependency penyongsangan Prinsip

Prinsip Tanggungjawab Tunggal

Prinsip Tanggungjawab Tunggal menyatakan bahawa kelas harus melakukan satu perkara dan oleh itu ia mesti mempunyai satu sebab untuk berubah .

Untuk menyatakan prinsip ini dengan lebih teknikal: Hanya satu perubahan yang berpotensi (logik pangkalan data, logik log, dan sebagainya.) Dalam spesifikasi perisian harus dapat mempengaruhi spesifikasi kelas.

Ini bermaksud bahawa jika kelas adalah wadah data, seperti kelas Buku atau kelas Pelajar, dan mempunyai beberapa bidang berkenaan dengan entiti itu, kelas itu harus berubah hanya apabila kita mengubah model data.

Mengikuti Prinsip Tanggungjawab Tunggal adalah penting. Pertama sekali, kerana banyak pasukan yang berbeza dapat mengerjakan projek yang sama dan mengedit kelas yang sama untuk alasan yang berbeza, ini dapat menyebabkan modul tidak sesuai.

Kedua, ia menjadikan kawalan versi lebih mudah. Sebagai contoh, katakan kita mempunyai kelas kegigihan yang mengendalikan operasi pangkalan data, dan kita melihat perubahan dalam fail tersebut di GitHub melakukan. Dengan mengikuti SRP, kita akan mengetahui bahawa ia berkaitan dengan penyimpanan atau perkara yang berkaitan dengan pangkalan data.

Percanggahan gabungan adalah contoh lain. Mereka muncul apabila pasukan berbeza menukar fail yang sama. Tetapi jika SRP diikuti, lebih sedikit konflik akan muncul - fail akan mempunyai satu alasan untuk berubah, dan konflik yang ada akan lebih mudah diselesaikan.

Perangkap dan Anti Corak biasa

Di bahagian ini kita akan melihat beberapa kesalahan biasa yang melanggar Prinsip Tanggungjawab Tunggal. Kemudian kita akan membincangkan beberapa cara untuk memperbaikinya.

Kami akan melihat kod untuk program invois kedai buku ringkas sebagai contoh. Mari mulakan dengan menentukan kelas buku yang akan digunakan dalam invois kami.

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 ringkas dengan beberapa bidang. Tidak ada yang mewah. Saya tidak menjadikan bidang bersifat peribadi sehingga kita tidak perlu berurusan dengan pemula dan pengatur dan boleh fokus pada logik sebagai gantinya.

Sekarang mari kita buat kelas invois yang akan mengandungi logik untuk membuat invois dan mengira jumlah harga. Buat masa ini, anggap bahawa kedai buku kami 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 invois kami. Ini juga mengandungi beberapa bidang mengenai penginvoisan dan 3 kaedah:

  • hitungTotal kaedah, yang mengira jumlah harga,
  • printInvoice kaedah, yang harus mencetak invois untuk konsol, dan
  • kaedah saveToFile , bertanggungjawab untuk menulis invois ke fail.

Anda harus meluangkan masa untuk berfikir tentang apa yang salah dengan reka bentuk kelas ini sebelum membaca perenggan seterusnya.

Ok jadi apa yang berlaku di sini? Kelas kami melanggar Prinsip Tanggungjawab Tunggal dengan pelbagai cara.

Pelanggaran pertama adalah kaedah printInvoice , yang mengandungi logik percetakan kami. SRP menyatakan bahawa kelas kami hanya boleh mempunyai satu sebab untuk berubah, dan alasan itu harus menjadi perubahan dalam pengiraan invois untuk kelas kami.

Tetapi dalam seni bina ini, jika kita ingin mengubah format percetakan, kita perlu mengubah kelas. Inilah sebabnya mengapa kita tidak seharusnya mencetak logik bercampur dengan logik perniagaan dalam kelas yang sama.

Terdapat kaedah lain yang melanggar SRP di kelas kami: kaedah saveToFile . Ini juga merupakan kesalahan yang sangat biasa untuk mencampuradukkan logik kegigihan dengan logik perniagaan.

Jangan hanya berfikir dari segi menulis ke fail - ia boleh menyimpan ke pangkalan data, membuat panggilan API, atau perkara lain yang berkaitan dengan kegigihan.

Oleh itu, bagaimana kita dapat memperbaiki fungsi cetak ini, anda mungkin bertanya.

Kami boleh membuat kelas baru untuk logik pencetakan dan ketekunan kami sehingga kami tidak perlu lagi mengubah kelas invois untuk tujuan tersebut.

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

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 Tanggungjawab Tunggal dan setiap kelas bertanggungjawab untuk satu aspek aplikasi kami. Hebat!

Prinsip Tertutup Terbuka

Prinsip Tertutup Terbuka menghendaki kelas harus dibuka untuk peluasan dan ditutup kepada pengubahsuaian.

Pengubahsuaian bermaksud mengubah kod kelas yang ada, dan peluasan bermaksud menambahkan fungsi baru.

Oleh itu, prinsip ini ingin dinyatakan: Kita seharusnya dapat menambahkan fungsi baru tanpa menyentuh kod yang ada untuk kelas. Ini kerana setiap kali kita mengubah kod yang ada, kita mengambil risiko membuat bug yang berpotensi. Oleh itu, kita harus mengelak daripada menyentuh kod pengeluaran (yang diuji) dan boleh dipercayai jika mungkin.

Tetapi bagaimana kita akan menambah fungsi baru tanpa menyentuh kelas, anda mungkin bertanya. Ia biasanya dilakukan dengan bantuan antara muka dan kelas abstrak.

Sekarang setelah kita membahas asas asas, mari kita terapkan pada aplikasi Invoice kami.

Katakan bos kami datang kepada kami dan mengatakan bahawa mereka mahu invois disimpan ke pangkalan data supaya kami dapat mencarinya dengan mudah. Kami fikir baik-baik saja, ini adalah bos peasy yang mudah, beri saya sekejap!

Kami membuat pangkalan data, menyambungkannya, dan kami menambahkan kaedah simpan 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 pemaju malas untuk kedai buku, tidak merancang kelas untuk diperluaskan dengan mudah pada masa akan datang. Oleh itu, untuk menambahkan ciri ini, kami telah mengubahsuai kelas InvoicePersistence .

Sekiranya reka bentuk kelas kita mematuhi prinsip Open-Closed, kita tidak perlu mengubah kelas ini.

Oleh itu, sebagai pemaju yang malas tetapi pandai untuk kedai buku, kami melihat masalah reka bentuk dan memutuskan untuk memfaktorkan semula kod untuk mematuhi prinsip tersebut.

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

Kami menukar jenis InvoicePersistence ke Interface dan menambah kaedah save. Setiap kelas kegigihan akan melaksanakan kaedah simpan 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 kami sekarang kelihatan seperti ini:

Kini logik kegigihan kita mudah dilanjutkan. Sekiranya bos kami meminta kami menambahkan pangkalan data lain dan mempunyai 2 jenis pangkalan data yang berbeza seperti MySQL dan MongoDB, kami dapat melakukannya dengan mudah.

Anda mungkin berfikir bahawa kami hanya boleh membuat beberapa kelas tanpa antara muka dan menambahkan kaedah simpan pada semua kelas.

Tetapi katakan bahawa kami memperluaskan aplikasi kami dan mempunyai beberapa kelas ketekunan seperti InvoicePersistence , BookPersistence dan kami membuat kelas PersistenceManager yang menguruskan semua kelas kegigihan:

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

Kita sekarang boleh lulus kelas yang melaksanakan antara muka InvoicePersistence ke kelas ini dengan bantuan polimorfisme. Inilah fleksibiliti yang disediakan antara muka.

Prinsip Penggantian 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 letak kereta. Dengan model baru ini, kita dapat melangkah lebih jauh dan memecah PaidParkingLot untuk menyokong pelbagai jenis pembayaran.

Sekarang model kami jauh lebih fleksibel, dapat diperluas, dan pelanggan tidak perlu menerapkan logik yang tidak relevan kerana kami hanya menyediakan fungsi yang berkaitan dengan tempat letak kereta di antara muka tempat letak kereta.

Prinsip Penukaran Ketergantungan

Prinsip Dependency Inversion menyatakan bahawa kelas kita harus bergantung pada antara muka atau kelas abstrak dan bukannya kelas dan fungsi konkrit.

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

"Sekiranya OCP menyatakan matlamat seni bina 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.