Wszystko, co musisz wiedzieć o solidnych zasadach w Javie



W tym artykule dowiesz się szczegółowo, jakie są zasady Solid w Javie, wraz z przykładami i ich znaczenie w prawdziwym życiu.

W świecie (OOP), istnieje wiele wytycznych projektowych, wzorców lub zasad. Pięć z tych zasad jest zwykle zgrupowanych razem i jest nazywanych akronimem SOLID. Chociaż każda z tych pięciu zasad opisuje coś konkretnego, nakładają się one również na siebie, tak że przyjęcie jednej z nich implikuje lub prowadzi do przyjęcia innej. W tym artykule zrozumiemy zasady SOLID w Javie.

Historia zasad SOLID w Javie

Robert C. Martin podał pięć zasad projektowania zorientowanego obiektowo, w których zastosowano akronim „S.O.L.I.D”. Kiedy łączysz wszystkie zasady S.O.L.I.D., tworzenie oprogramowania, którym można łatwo zarządzać, staje się łatwiejsze. Inne cechy korzystania z S.O.L.I.D to:





  • Pozwala uniknąć zapachów kodu.
  • Szybki kod refraktora.
  • Potrafi tworzyć oprogramowanie adaptacyjne lub zwinne.

Korzystając z zasady S.O.L.I.D w swoim kodowaniu, zaczynasz pisać kod, który jest zarówno wydajny, jak i skuteczny.



Jakie jest znaczenie S.O.L.I.D?

Solid reprezentuje pięć zasad javy, którymi są:

  • S : Zasada pojedynczej odpowiedzialności
  • LUB : Zasada otwarcia-zamknięcia
  • L : Zasada substytucji Liskova
  • ja : Zasada segregacji interfejsów
  • re : Zasada inwersji zależności

Na tym blogu szczegółowo omówimy wszystkie pięć zasad SOLID w Javie.



Zasada pojedynczej odpowiedzialności w Javie

Co to mówi?

Robert C. Martin opisuje to jako jedną klasę, która powinna mieć tylko jedną i jedyną odpowiedzialność.

Zgodnie z zasadą pojedynczej odpowiedzialności powinien istnieć tylko jeden powód, dla którego należy zmienić klasę. Oznacza to, że klasa powinna mieć jedno zadanie do wykonania. Ta zasada jest często określana jako subiektywna.

Zasadę można dobrze zrozumieć na przykładzie. Wyobraź sobie, że istnieje klasa, która wykonuje następujące operacje.

  • Połączono z bazą danych

  • Przeczytaj dane z tabel bazy danych

  • Na koniec zapisz go do pliku.

Czy wyobrażałeś sobie scenariusz? Tutaj klasa ma wiele powodów do zmiany, a kilka z nich to modyfikacja pliku wyjściowego, przyjęcie nowej bazy danych. Kiedy mówimy o odpowiedzialności za jedną zasadę, powiedzielibyśmy, że jest zbyt wiele powodów, dla których klasa ma się zmieniać, dlatego nie pasuje ona odpowiednio do zasady pojedynczej odpowiedzialności.

Na przykład klasa Automobile może sama się uruchomić lub zatrzymać, ale zadanie jej mycia należy do klasy CarWash. W innym przykładzie klasa Book ma właściwości do przechowywania własnej nazwy i tekstu. Ale zadanie drukowania książki musi należeć do klasy Book Printer. Klasa Book Printer może drukować na konsoli lub innym nośniku, ale takie zależności są usuwane z klasy Book

php.mysql_fetch_array

Dlaczego ta zasada jest wymagana?

Kiedy przestrzega się zasady pojedynczej odpowiedzialności, testowanie jest łatwiejsze. Przy jednej odpowiedzialności klasa będzie miała mniej przypadków testowych. Mniejsza funkcjonalność oznacza również mniej zależności od innych klas. Prowadzi to do lepszej organizacji kodu, ponieważ mniejsze i dobrze zaprojektowane klasy są łatwiejsze do wyszukiwania.

Przykład wyjaśniający tę zasadę:

Załóżmy, że zostaniesz poproszony o zaimplementowanie usługi UserSetting, w której użytkownik może zmienić ustawienia, ale przed tym użytkownik musi zostać uwierzytelniony. Jednym ze sposobów realizacji tego byłoby:

public class UserSettingService {public void changeEmail (User user) {if (checkAccess (user)) {// Przyznaj opcję zmiany}} public boolean checkAccess (User user) {// Sprawdź, czy użytkownik jest ważny. }}

Wszystko wygląda dobrze, dopóki nie zechcesz ponownie użyć kodu checkAccess w innym miejscu LUB chcesz wprowadzić zmiany w sposobie wykonywania checkAccess. We wszystkich 2 przypadkach skończyłoby się na zmianie tej samej klasy, aw pierwszym przypadku musiałbyś również użyć UserSettingService do sprawdzenia dostępu.
Jednym ze sposobów rozwiązania tego problemu jest zdekomponowanie usługi UserSettingService na usługę UserSettingService i SecurityService. I przenieś kod checkAccess do SecurityService.

public class UserSettingService {public void changeEmail (User user) {if (SecurityService.checkAccess (user)) {// Przyznaj opcję zmiany}}} public class SecurityService {public static boolean checkAccess (użytkownik użytkownika) {// sprawdź dostęp. }}

Open Closed Principle w Javie

Robert C. Martin opisuje to jako Komponenty oprogramowania powinny być otwarte na rozszerzenie, ale zamknięte na modyfikacje.

Mówiąc ściślej, zgodnie z tą zasadą klasa powinna być napisana w taki sposób, aby bezbłędnie wykonywała swoją pracę bez założenia, że ​​ludzie w przyszłości po prostu przyjdą i ją zmienią. Dlatego klasa powinna pozostać zamknięta do modyfikacji, ale powinna mieć możliwość rozszerzenia. Sposoby przedłużenia zajęć obejmują:

  • Dziedziczenie z klasy

  • Zastępowanie wymaganych zachowań z klasy

  • Rozszerzanie niektórych zachowań klasy

Doskonały przykład zasady open-closed można zrozumieć przy pomocy przeglądarek. Czy pamiętasz instalowanie rozszerzeń w swojej przeglądarce Chrome?

Podstawową funkcją przeglądarki Chrome jest przeglądanie różnych witryn. Czy chcesz sprawdzić gramatykę podczas pisania wiadomości e-mail za pomocą przeglądarki Chrome? Jeśli tak, możesz po prostu użyć rozszerzenia Grammarly, które zapewnia sprawdzenie gramatyki treści.

Ten mechanizm, w którym dodajesz rzeczy zwiększające funkcjonalność przeglądarki, jest rozszerzeniem. Dlatego przeglądarka jest doskonałym przykładem funkcjonalności, która jest otwarta na rozszerzenie, ale zamknięta na modyfikacje. Krótko mówiąc, możesz zwiększyć funkcjonalność, dodając / instalując wtyczki w przeglądarce, ale nie możesz tworzyć niczego nowego.

Dlaczego ta zasada jest wymagana?

OCP jest ważne, ponieważ klasy mogą przychodzić do nas za pośrednictwem bibliotek zewnętrznych. Powinniśmy być w stanie rozszerzyć te klasy bez martwienia się, czy te klasy bazowe mogą obsługiwać nasze rozszerzenia. Jednak dziedziczenie może prowadzić do podklas, które zależą od implementacji klasy bazowej. Aby tego uniknąć, zaleca się używanie interfejsów. Ta dodatkowa abstrakcja prowadzi do luźnego powiązania.

Powiedzmy, że musimy obliczyć obszary o różnych kształtach. Zaczynamy od stworzenia klasy dla naszego pierwszego kształtu Rectanglektóry ma 2 atrybuty długościi szerokość.

public class Rectangle {publiczna podwójna długość publiczna podwójna szerokość}

Następnie tworzymy klasę do obliczania pola tego Rectanglektóry ma metodę obliczRectangleAreaktóry przyjmuje Rectanglejako parametr wejściowy i oblicza jego powierzchnię.

public class AreaCalculator {public double calcRectangleArea (Rectangle rectangle) {return rectangle.length * rectangle.width}}

Jak na razie dobrze. Teraz powiedzmy, że otrzymujemy koło o drugim kształcie. Dlatego niezwłocznie tworzymy nową klasę Circlez jednym promieniem atrybutu.

klasa publiczna Circle {public double radius}

Następnie modyfikujemy Areacalculatorklasa, aby dodać obliczenia koła za pomocą nowej metody obliczCircleaArea ()

public class AreaCalculator {public double obliczRectangleArea (Rectangle rectangle) {return rectangle.length * rectangle.width} public double obliczCircleArea (Circle circle) {return (22/7) * circle.radius * circle.radius}}

Zwróć jednak uwagę, że w powyższym sposobie zaprojektowania naszego rozwiązania były wady.

Powiedzmy, że mamy pięciokąt w nowym kształcie. W takim przypadku ponownie zmodyfikujemy klasę AreaCalculator. Wraz ze wzrostem typów kształtów staje się to coraz bardziej bałaganiarskie, ponieważ AreaCalculator ciągle się zmienia, a wszyscy użytkownicy tej klasy będą musieli aktualizować swoje biblioteki, które zawierają AreaCalculator. W rezultacie klasa AreaCalculator nie będzie z pewnością bazowa (finalizowana), ponieważ za każdym razem, gdy pojawi się nowy kształt, będzie on modyfikowany. Tak więc ten projekt nie jest zamknięty do modyfikacji.

AreaCalculator będzie musiał dodawać logikę obliczeń w nowszych metodach. Tak naprawdę nie rozszerzamy zakresu kształtów, a raczej po prostu robimy rozwiązanie z kawałkami (krok po kroku) dla każdego dodanego kształtu.

Modyfikacja powyższego projektu w celu dostosowania do zasady otwarty / zamknięty:

Przyjrzyjmy się teraz bardziej eleganckiemu projektowi, który rozwiązuje wady powyższego projektu, stosując się do zasady otwartego / zamkniętego. Przede wszystkim sprawimy, że projekt będzie rozszerzalny. W tym celu musimy najpierw zdefiniować typ bazowy Shape i mieć interfejs Shape - Circle & Rectangle.

public interface Shape {public double calculatorArea ()} public class Rectangle implements Shape {double length double width public double obliczArea () {return length * width}} public class Circle implements Shape {public double radius public double obliczArea () {return (22 / 7) * radius * radius}}

Istnieje podstawowy kształt interfejsu. Wszystkie kształty implementują teraz podstawowy interfejs Shape. Interfejs Shape ma abstrakcyjną metodę ObliczArea (). Zarówno okrąg, jak i prostokąt udostępniają własną, nadpisaną implementację metody obliczArea () przy użyciu własnych atrybutów.
Wprowadziliśmy pewien stopień rozszerzalności, ponieważ kształty są teraz przykładem interfejsów Shape. Dzięki temu możemy używać Shape zamiast pojedynczych klas
Ostatni punkt wyżej wymieniony konsument tych kształtów. W naszym przypadku konsumentem będzie klasa AreaCalculator, która teraz wyglądałaby tak.

public class AreaCalculator {public double obliczShapeArea (kształt kształtu) {return shape.calculateArea ()}}

Ten AreaCalculatorclass teraz całkowicie usuwa nasze wady projektowe wymienione powyżej i zapewnia czyste rozwiązanie, które jest zgodne z zasadą otwartego-zamkniętego. Przejdźmy do innych SOLID Principles w Javie

Zasada podstawienia Liskova w Javie

Robert C. Martin opisuje to jako typy pochodne muszą być całkowicie zastępowalne dla ich typów podstawowych.

Zasada podstawienia Liskova zakłada, że ​​q (x) jest własnością, dającą się udowodnić o bytach x, które należą do typu T. Teraz, zgodnie z tą zasadą, q (y) powinno być teraz możliwe do udowodnienia dla obiektów y, które należą do typu S, i S jest w rzeczywistości podtypem T. Czy jesteś teraz zdezorientowany i nie wiesz, co właściwie oznacza zasada podstawienia Liskova? Definicja może być nieco złożona, ale w rzeczywistości jest dość łatwa. Jedyną rzeczą jest to, że każda podklasa lub klasa pochodna powinna być substytucyjna dla swojej klasy nadrzędnej lub bazowej.

Można powiedzieć, że jest to wyjątkowa zasada zorientowana obiektowo. Zasada może być dodatkowo uproszczona przez typ dziecka określonego typu rodzica bez komplikowania lub wysadzania rzeczy, powinien mieć możliwość zastępowania tego rodzica. Zasada ta jest ściśle związana z zasadą substytucji Liskova.

Dlaczego ta zasada jest wymagana?

Pozwala to uniknąć niewłaściwego wykorzystania dziedziczenia. Pomaga nam dostosować się do relacji „jest-a”. Można też powiedzieć, że podklasy muszą spełniać kontrakt zdefiniowany przez klasę bazową. W tym sensie jest to związane zZaprojektuj według umowyto zostało po raz pierwszy opisane przez Bertranda Meyera. Na przykład kusi stwierdzenie, że okrąg jest rodzajem elipsy, ale okręgi nie mają dwóch ognisk ani głównych / pomocniczych osi.

LSP jest popularnie wyjaśniany na przykładzie kwadratu i prostokąta. jeśli przyjmiemy relację ISA między Square i Rectangle. Dlatego nazywamy „Kwadrat to Prostokąt”. Poniższy kod przedstawia związek.

public class Rectangle {private int length private int widthth public int getLength () {return length} public void setLength (int length) {this.length = length} public int getBreadth () {return breadth} public void setBreadth (int breadth) { this.breadth = breadth} public int getArea () {return this.length * this.breadth}}

Poniżej znajduje się kod dla Square. Zauważ, że Kwadrat rozciąga Prostokąt.

public class Square extends Rectangle {public void setBreadth (int szerokość) {super.setBreadth (szerokość) super.setLength (szerokość)} public void setLength (int length) {super.setLength (length) super.setBreadth (length)}}

W tym przypadku próbujemy ustanowić relację ISA między Square i Rectangle tak, aby wywołanie „Square is a Rectangle” w poniższym kodzie zaczęło zachowywać się nieoczekiwanie, jeśli zostanie przekazana instancja Square. W przypadku sprawdzania „obszaru” i sprawdzania „szerokości” zostanie zgłoszony błąd asercji, chociaż program zakończy działanie, gdy zostanie zgłoszony błąd asercji z powodu niepowodzenia sprawdzenia obszaru.

publiczna klasa LSPDemo {public void wyliczArea (Rectangle r) {r.setBreadth (2) r.setLength (3) assert r.getArea () == 6: printError ('area', r) assert r.getLength () == 3: printError ('length', r) assert r.getBreadth () == 2: printError ('width', r)} private String printError (String errorIdentifer, Rectangle r) {return 'Nieoczekiwana wartość' + errorIdentifer + ' na przykład '+ r.getClass (). getName ()} public static void main (String [] args) {LSPDemo lsp = new LSPDemo () // Instancja klasy Rectangle jest przekazywana lsp.calculateArea (new Rectangle ()) // Instancja Square jest przekazywana lsp.calculateArea (new Square ())}}

Klasa demonstruje zasadę zastępowania Liskova (LSP) Zgodnie z zasadą funkcje, które używają odwołań do klas bazowych, muszą mieć możliwość korzystania z obiektów klasy pochodnej, nie wiedząc o tym.

Zatem w poniższym przykładzie funkcja obliczArea, która korzysta z odwołania „Rectangle”, powinna być w stanie korzystać z obiektów klasy pochodnej, takiej jak Square, i spełniać wymagania wynikające z definicji Rectangle. Należy zauważyć, że zgodnie z definicją prostokąta, następujące dane muszą być zawsze prawdziwe, biorąc pod uwagę poniższe dane:

  1. Długość musi zawsze być równa długości przekazanej jako dane wejściowe do metody setLength
  2. Szerokość musi być zawsze równa szerokości przekazanej jako dane wejściowe do metody, setBreadth
  3. Powierzchnia musi być zawsze równa iloczynowi długości i szerokości

W przypadku, gdy próbujemy ustalić relację ISA między Square i Rectangle w taki sposób, że nazwiemy „Square is a Rectangle”, powyższy kod zacznie zachowywać się nieoczekiwanie, jeśli instancja Square zostanie przekazana Błąd potwierdzenia zostanie wyrzucony w przypadku sprawdzenia obszaru i sprawdzenia na szerokość, chociaż program zakończy działanie, ponieważ zostanie zgłoszony błąd potwierdzenia z powodu niepowodzenia sprawdzania obszaru.

Klasa Square nie potrzebuje metod takich jak setBreadth czy setLength. Klasa LSPDemo musiałaby znać szczegóły klas pochodnych klasy Rectangle (takich jak Square), aby odpowiednio zakodować, aby uniknąć generowania błędów. Zmiana istniejącego kodu przede wszystkim łamie zasadę open-closed.

Zasada segregacji interfejsów

Robert C. Martin opisuje to jako klienta, który nie powinien być zmuszany do wdrażania niepotrzebnych metod, z których nie będą korzystać.

WedługZasada segregacji interfejsówklient, bez względu na to, co nigdy nie powinno być zmuszane do implementacji interfejsu, z którego nie korzysta, lub klient nie powinien być nigdy zobowiązany do polegania na żadnej metodzie, która nie jest przez niego używana, a więc w zasadzie zasady segregacji interfejsów tak, jak wolisz interfejsy, które są małe, ale specyficzne dla klienta zamiast monolitycznego i większego interfejsu. Krótko mówiąc, byłoby źle, gdybyś zmusił klienta do polegania na pewnej rzeczy, której nie potrzebuje.

Na przykład pojedynczy interfejs rejestrowania do zapisywania i odczytywania dzienników jest przydatny w przypadku bazy danych, ale nie w przypadku konsoli. Czytanie dzienników nie ma sensu dla rejestratora konsoli. Przechodząc do tego artykułu SOLID Principles in Java.

Dlaczego ta zasada jest wymagana?

Powiedzmy, że istnieje interfejs restauracji, który zawiera metody przyjmowania zamówień od klientów online, klientów telefonicznych lub telefonicznych oraz klientów przychodzących. Zawiera również metody obsługi płatności online (dla klientów online) i osobistych (dla klientów przychodzących, a także klientów telefonicznych, gdy ich zamówienie jest dostarczane do domu).

Teraz stwórzmy interfejs Java dla restauracji i nazwijmy go jako RestaurantInterface.java.

public interface RestaurantInterface {public void acceptOnlineOrder () public void takeTelephoneOrder () public void payOnline () public void walkInCustomerOrder () public void payInPerson ()}

W interfejsie RestaurantInterface zdefiniowano 5 metod, które służą do przyjmowania zamówień online, przyjmowania zamówień telefonicznych, przyjmowania zamówień od klienta przychodzącego, przyjmowania płatności online i przyjmowania płatności osobiście.

Zacznijmy od wdrożenia interfejsu RestaurantInterface dla klientów internetowych jako OnlineClientImpl.java

public class OnlineClientImpl implementuje RestaurantInterface {public void acceptOnlineOrder () {// logika składania zamówienia online} public void takeTelephoneOrder () {// Nie dotyczy zamówienia online zgłoś nowy UnsupportedOperationException ()} public void payOnline () {// logika płatności online} public void walkInCustomerOrder () {// Nie dotyczy zamówienia online zgłoś nowy UnsupportedOperationException ()} public void payInPerson () {// Nie dotyczy zamówienia online zgłoś nowy wyjątek UnsupportedOperationException ()}}
  • Ponieważ powyższy kod (OnlineClientImpl.java) dotyczy zamówień online, należy zgłosić wyjątek UnsupportedOperationException.

  • Klienci online, telefoniczni i chodzący korzystają z implementacji RestaurantInterface specyficznej dla każdego z nich.

  • Klasy implementacji dla klienta telefonicznego i klienta Walk-in będą miały nieobsługiwane metody.

  • Ponieważ 5 metod jest częścią RestaurantInterface, klasy implementacji muszą zaimplementować wszystkie 5 z nich.

  • Metody, które każda z klas implementacji zgłasza UnsupportedOperationException. Jak widać - wdrażanie wszystkich metod jest nieefektywne.

  • Każda zmiana w którejkolwiek z metod interfejsu RestaurantInterface będzie propagowana do wszystkich klas implementacji. Utrzymanie kodu staje się wtedy naprawdę uciążliwe, a skutki zmian w regresji będą się nasilać.

    co jest awt w java
  • RestaurantInterface.java łamie zasadę pojedynczej odpowiedzialności, ponieważ logika płatności i składania zamówień jest zgrupowana w jednym interfejsie.

Aby przezwyciężyć powyższe problemy, stosujemy zasadę segregacji interfejsów do refaktoryzacji powyższego projektu.

  1. Oddziel funkcje płatności i składania zamówień na dwa oddzielne interfejsy Lean: PaymentInterface.java i OrderInterface.java.

  2. Każdy z klientów korzysta z jednej implementacji PaymentInterface i OrderInterface. Na przykład - OnlineClient.java używa OnlinePaymentImpl, OnlineOrderImpl i tak dalej.

  3. Zasada pojedynczej odpowiedzialności jest teraz dołączona jako interfejs płatności (PaymentInterface.java) i interfejs zamówień (OrderInterface).

  4. Zmiana w jednym interfejsie zamówienia lub płatności nie wpływa na inny. Są teraz niezależne, więc nie będzie potrzeby wykonywania żadnej fałszywej implementacji ani zgłaszania wyjątku UnsupportedOperationException, ponieważ każdy interfejs ma tylko metody, których będzie zawsze używać.

Po zastosowaniu ISP

Zasada odwrócenia zależności

Robert C. Martin opisuje to jako zależne od abstrakcji, a nie od konkrecji, zgodnie z którym moduł wysokiego poziomu nigdy nie może polegać na żadnym module niskiego poziomu. na przykład

Idziesz do lokalnego sklepu, aby coś kupić, i decydujesz się za to zapłacić kartą debetową. Tak więc, kiedy oddajesz swoją kartę urzędnikowi w celu dokonania płatności, urzędnik nie zadaje sobie trudu, aby sprawdzić, jaki rodzaj karty podałeś.

Nawet jeśli dałeś kartę Visa, nie wystawi maszyny Visa do przeciągnięcia karty. Rodzaj karty kredytowej lub debetowej, którą masz do zapłaty, nie ma nawet znaczenia, po prostu ją przeciągną. Tak więc w tym przykładzie widać, że zarówno ty, jak i urzędnik jesteście zależni od abstrakcji karty kredytowej i nie martwicie się o jej specyfikę. Na tym polega zasada inwersji zależności.

Dlaczego ta zasada jest wymagana?

Pozwala programiście usunąć zakodowane na stałe zależności, dzięki czemu aplikacja staje się luźno powiązana i rozszerzalna.

public class Student {private Address address public Student () {address = new Address ()}}

W powyższym przykładzie klasa Student wymaga obiektu Address i jest odpowiedzialna za inicjalizację i używanie obiektu Address. Jeśli klasa adresu zostanie zmieniona w przyszłości, musimy również wprowadzić zmiany w klasie studenta. To powoduje ścisłe powiązanie między obiektami Student i Address. Możemy rozwiązać ten problem za pomocą wzorca projektowego inwersji zależności. tj. obiekt Adres zostanie zaimplementowany niezależnie i zostanie dostarczony Studentowi, gdy Student zostanie utworzony przy użyciu odwrócenia zależności na podstawie konstruktora lub metody ustawiającej.

W ten sposób dochodzimy do końca SOLID Principles w Javie.

Sprawdź autorstwa Edureka, zaufanej firmy zajmującej się edukacją online, z siecią ponad 250 000 zadowolonych uczniów rozsianych po całym świecie. Szkolenie i certyfikacja J2EE i SOA firmy Edureka jest przeznaczony dla studentów i profesjonalistów, którzy chcą zostać programistą Java. Kurs ma na celu zapewnienie przewagi w programowaniu w języku Java i przeszkolenie zarówno podstawowych, jak i zaawansowanych koncepcji języka Java, a także różnych struktur Java, takich jak Hibernate i Spring.

Masz do nas pytanie? Wspomnij o tym w sekcji komentarzy na blogu „SOLID Principles in Java”, a my skontaktujemy się z Tobą tak szybko, jak to możliwe.