ČVRSTI Principi objektno orijentiranog programiranja objašnjeni na običnom engleskom jeziku

ČVRSTI Principi pet su principa objektno orijentiranog dizajna klasa. Oni su skup pravila i najboljih praksi kojih se treba pridržavati tijekom dizajniranja strukture razreda.

Ovih pet principa pomažu nam razumjeti potrebu za određenim uzorcima dizajna i općenito softverskom arhitekturom. Stoga vjerujem da je to tema koju bi svaki programer trebao naučiti.

Ovaj će vas članak naučiti svemu što trebate znati kako biste primijenili SOLID principe na svoje projekte.

Za početak ćemo pogledati povijest ovog pojma. Zatim ćemo ući u glatke detalje - zašto i kako svaki princip - stvaranjem dizajna klase i poboljšanjem korak po korak.

Dakle, uzmite šalicu kave ili čaja i krenimo odmah!

Pozadina

SOLID principe prvi je uveo poznati računalni znanstvenik Robert J. Martin (zvani Ujak Bob) u svom radu 2000. Ali SOLID kraticu je kasnije uveo Michael Feathers.

Ujak Bob također je autor bestselera Čisti kodeks i čista arhitektura , a jedan je od sudionika "Agile Alliance".

Stoga nije iznenađenje da su svi ti koncepti čistog kodiranja, objektno orijentirane arhitekture i dizajnerski obrasci nekako povezani i međusobno se nadopunjuju.

Svi oni imaju istu svrhu:

"Stvoriti razumljiv, čitljiv i provjerljiv kôd na kojem mnogi programeri mogu zajednički raditi."

Pogledajmo svaki princip jedan po jedan. Nakon kratice SOLID, to su:

  • S Ingle odgovornost princip
  • O olovke zatvaranja Princip
  • Je L iskov Zamjena Princip
  • Sam nterface Segregacija Princip
  • D ependency inverzije Princip

Načelo jedinstvene odgovornosti

Načelo jedinstvene odgovornosti navodi da bi klasa trebala činiti jedno te bi stoga trebala imati samo jedan razlog za promjenu .

Da tehnički navedemo ovo načelo: Samo jedna potencijalna promjena (logika baze podataka, logika zapisivanja itd.) U specifikaciji softvera trebala bi moći utjecati na specifikaciju klase.

To znači da ako je klasa spremnik podataka, poput klase Book ili klase Student i ako ima neka polja u vezi s tim entitetom, trebala bi se promijeniti samo kada promijenimo podatkovni model.

Slijeđenje načela jedinstvene odgovornosti je važno. Prije svega, jer mnogo različitih timova može raditi na istom projektu i uređivati ​​isti razred iz različitih razloga, to bi moglo dovesti do nekompatibilnih modula.

Drugo, to olakšava kontrolu verzija. Na primjer, recimo da imamo klasu postojanosti koja obrađuje operacije baze podataka i da vidimo promjenu u toj datoteci u GitHub urezima. Slijedeći SRP, znat ćemo da je povezan sa pohranom ili s bazama podataka.

Konflikti spajanja su još jedan primjer. Pojavljuju se kada različiti timovi promijene istu datoteku. No ako se slijedi SRP, pojavit će se manje sukoba - datoteke će imati jedan razlog za promjenu, a sukobi koji postoje bit će lakše riješiti.

Uobičajene zamke i anti-uzorci

U ovom ćemo dijelu pogledati neke uobičajene pogreške koje krše Načelo jedinstvene odgovornosti. Tada ćemo razgovarati o nekim načinima kako ih popraviti.

Kao primjer ćemo pogledati kod jednostavnog programa faktura u knjižari. Počnimo s definiranjem klase knjige koja će se koristiti na našoj fakturi.

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; } } 

Ovo je jednostavan tečaj knjige s nekim poljima. Ništa otmjeno. Ne pravim polja privatnima, tako da se ne trebamo baviti geterima i postavljačima i da se umjesto toga možemo usredotočiti na logiku.

Sada kreirajmo klasu fakture koja će sadržavati logiku za izradu fakture i izračunavanje ukupne cijene. Za sada pretpostavimo da naša knjižara prodaje samo knjige i ništa drugo.

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 } }

Ovdje je naša klasa fakture. Sadrži i neka polja o fakturiranju i 3 metode:

  • calculateTotal metoda, koja izračunava cijenu ukupnog,
  • metoda printInvoice , koja bi trebala ispisati račun na konzolu, i
  • metoda saveToFile , odgovorna za pisanje fakture u datoteku.

Trebali biste si dati trenutak da razmislite što nije u redu s ovim dizajnom nastave prije čitanja sljedećeg odlomka.

Ok, što se ovdje događa? Naš razred krši Načelo jedinstvene odgovornosti na više načina.

Prvo kršenje je metoda printInvoice , koja sadrži našu logiku ispisa. SRP navodi da bi naša klasa trebala imati samo jedan razlog za promjenu, a taj bi razlog trebala biti promjena u izračunu računa za našu klasu.

Ali u ovoj arhitekturi, ako bismo željeli promijeniti format ispisa, trebali bismo promijeniti klasu. Zbog toga ne bismo trebali imati logiku ispisa pomiješanu s poslovnom logikom u istoj klasi.

Postoji još jedna metoda koja krši SRP u našoj klasi: metoda saveToFile . Također je izuzetno česta pogreška miješanje logike ustrajnosti s poslovnom logikom.

Ne razmišljajte samo u smislu pisanja u datoteku - to bi moglo biti spremanje u bazu podataka, upućivanje API poziva ili druge stvari povezane s postojanošću.

Pa, kako možemo popraviti ovu funkciju ispisa, možete pitati.

Možemo stvoriti nove klase za našu logiku ispisa i trajanja, tako da više nećemo trebati mijenjati klasu fakture u te svrhe.

Izrađujemo 2 razreda, InvoicePrinter i InvoicePersistence, i premještamo 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 } }

Sada se naša klasa uvažava Načelom jedinstvene odgovornosti i svaki je razred odgovoran za jedan aspekt naše prijave. Sjajno!

Otvoreno-zatvoreno načelo

Otvoreno-zatvoreno načelo zahtijeva da razredi budu otvoreni za proširenje i zatvoreni za izmjene.

Izmjena znači promjenu koda postojeće klase, a proširenje znači dodavanje nove funkcionalnosti.

So what this principle wants to say is: We should be able to add new functionality without touching the existing code for the class. This is because whenever we modify the existing code, we are taking the risk of creating potential bugs. So we should avoid touching the tested and reliable (mostly) production code if possible.

But how are we going to add new functionality without touching the class, you may ask. It is usually done with the help of interfaces and abstract classes.

Now that we have covered the basics of the principle, let's apply it to our Invoice application.

Let's say our boss came to us and said that they want invoices to be saved to a database so that we can search them easily. We think okay, this is easy peasy boss, just give me a second!

We create the database, connect to it, and we add a save method to our InvoicePersistence class:

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 } }

Unfortunately we, as the lazy developer for the book store, did not design the classes to be easily extendable in the future. So in order to add this feature, we have modified the InvoicePersistence class.

If our class design obeyed the Open-Closed principle we would not need to change this class.

So, as the lazy but clever developer for the book store, we see the design problem and decide to refactor the code to obey the principle.

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

We change the type of InvoicePersistence to Interface and add a save method. Each persistence class will implement this save method.

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 } }

So our class structure now looks like this:

Now our persistence logic is easily extendable. If our boss asks us to add another database and have 2 different types of databases like MySQL and MongoDB, we can easily do that.

You may think that we could just create multiple classes without an interface and add a save method to all of them.

But let's say that we extend our app and have multiple persistence classes like InvoicePersistence, BookPersistence and we create a PersistenceManager class that manages all persistence classes:

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

We can now pass any class that implements the InvoicePersistence interface to this class with the help of polymorphism. This is the flexibility that interfaces provide.

Liskov Substitution Principle

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.

Sad smo odvojili parkiralište. S ovim novim modelom možemo čak i ići dalje i podijeliti PaidParkingLot za podršku različitim vrstama plaćanja.

Sada je naš model puno fleksibilniji, proširiv, a klijenti ne trebaju implementirati nikakvu irelevantnu logiku jer u sučelju parkirališta pružamo samo funkcije povezane s parkiranjem.

Načelo inverzije ovisnosti

Princip inverzije ovisnosti kaže da bi naše klase trebale ovisiti o sučeljima ili apstraktnim klasama umjesto o konkretnim klasama i funkcijama.

U svom članku (2000.), ujak Bob sažeo je ovo načelo na sljedeći način:

"Ako OCP navodi cilj OO arhitekture, DIP navodi primarni mehanizam".

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.