1. Pengenalan
Dalam tutorial ini, kita akan membincangkan prinsip SOLID Reka Bentuk Berorientasikan Objek.
Pertama, kita akan mulakan dengan meneroka sebab-sebab mengapa mereka muncul dan mengapa kita harus mempertimbangkannya semasa mereka bentuk perisian. Kemudian, kami akan menggariskan setiap prinsip di samping beberapa kod contoh untuk menekankan pokoknya.
2. Sebab Prinsip SOLID
Prinsip-prinsip SOLID pertama kali dikonseptualisasikan oleh Robert C. Martin dalam makalah 2000nya, Prinsip Reka Bentuk dan Corak Reka Bentuk. Konsep-konsep ini kemudian dibangun oleh Michael Feathers, yang memperkenalkan kami kepada singkatan SOLID. Dan dalam 20 tahun terakhir, 5 prinsip ini telah merevolusikan dunia pengaturcaraan berorientasikan objek, mengubah cara kita menulis perisian.
Jadi, apa itu SOLID dan bagaimana ia membantu kita menulis kod yang lebih baik? Ringkasnya, prinsip reka bentuk Martin dan Feathers mendorong kita untuk membuat perisian yang lebih dapat dipelihara, difahami, dan fleksibel . Oleh kerana itu , apabila aplikasi kita bertambah besar, kita dapat mengurangkan kerumitannya dan menyelamatkan diri dari banyak sakit kepala di jalan!
5 konsep berikut membentuk prinsip SOLID kami:
- S Tanggungjawab ingle
- O pen / Tertutup
- Penggantian L iskov
- Saya merangkumi Segregasi
- D pembalikan ependensi
Walaupun sebilangan perkataan ini terdengar menakutkan, kata-kata itu mudah difahami dengan beberapa contoh kod ringkas. Pada bahagian berikut, kita akan menyelami apa yang dimaksudkan oleh setiap prinsip ini, bersama dengan contoh Java ringkas untuk menggambarkan masing-masing.
3. Tanggungjawab Tunggal
Mari kita mulakan dengan prinsip tanggungjawab tunggal. Seperti yang kita jangkakan, prinsip ini menyatakan bahawa kelas hanya harus mempunyai satu tanggungjawab. Tambahan lagi, ia hanya boleh mempunyai satu sebab untuk berubah.
Bagaimana prinsip ini membantu kita membina perisian yang lebih baik? Mari lihat beberapa faedahnya:
- Ujian - Kelas dengan satu tanggungjawab akan mempunyai kes ujian yang jauh lebih sedikit
- Gandingan lebih rendah - Kurang fungsi dalam satu kelas akan mempunyai lebih sedikit pergantungan
- Organisasi - Kelas yang lebih kecil dan teratur lebih senang dicari berbanding kelas monolitik
Ambil, sebagai contoh, kelas untuk mewakili buku ringkas:
public class Book { private String name; private String author; private String text; //constructor, getters and setters }
Dalam kod ini, kami menyimpan nama, pengarang, dan teks yang berkaitan dengan contoh Buku .
Mari sekarang tambahkan beberapa kaedah untuk bertanya teks:
public class Book { private String name; private String author; private String text; //constructor, getters and setters // methods that directly relate to the book properties public String replaceWordInText(String word){ return text.replaceAll(word, text); } public boolean isWordInText(String word){ return text.contains(word); } }
Sekarang, kelas Buku kami berfungsi dengan baik, dan kami dapat menyimpan seberapa banyak buku yang kami suka dalam aplikasi kami. Tetapi, apa gunanya menyimpan maklumat jika kita tidak dapat mengeluarkan teks ke konsol kita dan membacanya?
Mari berhati-hati dan tambahkan kaedah cetak:
public class Book { //... void printTextToConsole(){ // our code for formatting and printing the text } }
Walau bagaimanapun, kod ini melanggar prinsip tanggungjawab tunggal yang telah kami gariskan sebelumnya. Untuk memperbaiki kekacauan kita, kita harus melaksanakan kelas terpisah yang hanya berkaitan dengan mencetak teks kita:
public class BookPrinter { // methods for outputting text void printTextToConsole(String text){ //our code for formatting and printing the text } void printTextToAnotherMedium(String text){ // code for writing to any other location.. } }
Hebat. Kami bukan sahaja telah mengembangkan kelas yang melegakan Buku daripada tugas percetakannya, tetapi kami juga dapat memanfaatkan kelas BookPrinter kami untuk menghantar teks kami ke media lain.
Sama ada e-mel, pembalakan, atau apa sahaja, kami mempunyai kelas tersendiri yang dikhaskan untuk yang satu ini.
4. Terbuka untuk Sambungan, Ditutup untuk Pengubahsuaian
Sekarang, masa untuk 'O' - lebih formal dikenali sebagai prinsip tertutup terbuka . Ringkasnya, kelas harus dibuka untuk peluasan, tetapi ditutup untuk pengubahsuaian. Dengan berbuat demikian, kita menghentikan diri kita daripada mengubah kod yang ada dan menyebabkan bug baru yang berpotensi dalam aplikasi yang senang.
Sudah tentu, satu-satunya pengecualian dari peraturan adalah ketika memperbaiki pepijat dalam kod yang ada.
Mari kita teliti konsepnya dengan contoh kod ringkas. Sebagai sebahagian daripada projek baru, bayangkan kami telah melaksanakan kelas Guitar .
Ia penuh dan bahkan mempunyai tombol kelantangan:
public class Guitar { private String make; private String model; private int volume; //Constructors, getters & setters }
Kami melancarkan aplikasi, dan semua orang menyukainya. Namun, setelah beberapa bulan, kami memutuskan bahawa Gitar sedikit membosankan dan dapat dilakukan dengan corak nyalaan yang hebat untuk menjadikannya kelihatan lebih 'rock and roll'.
Pada ketika ini, mungkin menggoda untuk membuka kelas Gitar dan menambahkan corak nyalaan - tetapi siapa yang tahu kesalahan apa yang mungkin timbul dalam aplikasi kami.
Sebagai gantinya, mari kita berpegang pada prinsip tertutup dan hanya melanjutkan kelas Gitar kami :
public class SuperCoolGuitarWithFlames extends Guitar { private String flameColor; //constructor, getters + setters }
Dengan melanjutkan kelas Guitar kita dapat memastikan bahawa aplikasi kita yang ada tidak akan terjejas.
5. Penggantian Liskov
Berikut senarai kami adalah penggantian Liskov, yang boleh dikatakan paling rumit dari 5 prinsip. Ringkasnya, jika kelas A adalah subtipe kelas B , maka kita seharusnya dapat menggantikan B dengan A tanpa mengganggu tingkah laku program kita.
Mari kita terus ke kod untuk membantu menyelesaikan konsep ini:
public interface Car { void turnOnEngine(); void accelerate(); }
Di atas, kami menentukan antara muka Car yang ringkas dengan beberapa kaedah yang mesti dipenuhi oleh semua kereta - menghidupkan enjin, dan mempercepat ke hadapan.
Mari laksanakan antara muka kami dan berikan beberapa kod untuk kaedahnya:
public class MotorCar implements Car { private Engine engine; //Constructors, getters + setters public void turnOnEngine() { //turn on the engine! engine.on(); } public void accelerate() { //move forward! engine.powerOn(1000); } }
Seperti yang dijelaskan oleh kod kami, kami mempunyai enjin yang dapat kami hidupkan, dan kami dapat meningkatkan kuasa. Tetapi tunggu, 2019, dan Elon Musk telah menjadi orang yang sibuk.
We are now living in the era of electric cars:
public class ElectricCar implements Car { public void turnOnEngine() { throw new AssertionError("I don't have an engine!"); } public void accelerate() { //this acceleration is crazy! } }
By throwing a car without an engine into the mix, we are inherently changing the behavior of our program. This is a blatant violation of Liskov substitution and is a bit harder to fix than our previous 2 principles.
One possible solution would be to rework our model into interfaces that take into account the engine-less state of our Car.
6. Interface Segregation
The ‘I ‘ in SOLID stands for interface segregation, and it simply means that larger interfaces should be split into smaller ones. By doing so, we can ensure that implementing classes only need to be concerned about the methods that are of interest to them.
For this example, we're going to try our hands as zookeepers. And more specifically, we'll be working in the bear enclosure.
Let's start with an interface that outlines our roles as a bear keeper:
public interface BearKeeper { void washTheBear(); void feedTheBear(); void petTheBear(); }
As avid zookeepers, we're more than happy to wash and feed our beloved bears. However, we're all too aware of the dangers of petting them. Unfortunately, our interface is rather large, and we have no choice than to implement the code to pet the bear.
Let's fix this by splitting our large interface into 3 separate ones:
public interface BearCleaner { void washTheBear(); } public interface BearFeeder { void feedTheBear(); } public interface BearPetter { void petTheBear(); }
Now, thanks to interface segregation, we're free to implement only the methods that matter to us:
public class BearCarer implements BearCleaner, BearFeeder { public void washTheBear() { //I think we missed a spot... } public void feedTheBear() { //Tuna Tuesdays... } }
And finally, we can leave the dangerous stuff to the crazy people:
public class CrazyPerson implements BearPetter { public void petTheBear() { //Good luck with that! } }
Going further, we could even split our BookPrinter class from our example earlier to use interface segregation in the same way. By implementing a Printer interface with a single print method, we could instantiate separate ConsoleBookPrinter and OtherMediaBookPrinter classes.
7. Dependency Inversion
The principle of Dependency Inversion refers to the decoupling of software modules. This way, instead of high-level modules depending on low-level modules, both will depend on abstractions.
To demonstrate this, let's go old-school and bring to life a Windows 98 computer with code:
public class Windows98Machine {}
But what good is a computer without a monitor and keyboard? Let's add one of each to our constructor so that every Windows98Computer we instantiate comes pre-packed with a Monitor and a StandardKeyboard:
public class Windows98Machine { private final StandardKeyboard keyboard; private final Monitor monitor; public Windows98Machine() { monitor = new Monitor(); keyboard = new StandardKeyboard(); } }
This code will work, and we'll be able to use the StandardKeyboard and Monitor freely within our Windows98Computer class. Problem solved? Not quite. By declaring the StandardKeyboard and Monitor with the new keyword, we've tightly coupled these 3 classes together.
Not only does this make our Windows98Computer hard to test, but we've also lost the ability to switch out our StandardKeyboard class with a different one should the need arise. And we're stuck with our Monitor class, too.
Let's decouple our machine from the StandardKeyboard by adding a more general Keyboard interface and using this in our class:
public interface Keyboard { }
public class Windows98Machine{ private final Keyboard keyboard; private final Monitor monitor; public Windows98Machine(Keyboard keyboard, Monitor monitor) { this.keyboard = keyboard; this.monitor = monitor; } }
Here, we're using the dependency injection pattern here to facilitate adding the Keyboard dependency into the Windows98Machine class.
Let's also modify our StandardKeyboard class to implement the Keyboard interface so that it's suitable for injecting into the Windows98Machine class:
public class StandardKeyboard implements Keyboard { }
Now our classes are decoupled and communicate through the Keyboard abstraction. If we want, we can easily switch out the type of keyboard in our machine with a different implementation of the interface. We can follow the same principle for the Monitor class.
Excellent! We've decoupled the dependencies and are free to test our Windows98Machine with whichever testing framework we choose.
8. Conclusion
In this tutorial, we've taken a deep dive into the SOLID principles of object-oriented design.
Kami bermula dengan sedikit sejarah SOLID dan sebab prinsip-prinsip ini wujud.
Huruf demi huruf, kami telah menguraikan makna setiap prinsip dengan contoh kod ringkas yang melanggarnya. Kami kemudian melihat cara memperbaiki kod kami dan membuatnya mematuhi prinsip SOLID.
Seperti biasa, kodnya tersedia di GitHub.