paint-brush
Принципы SOLID в Java: руководство для начинающихк@ps06756
14,604 чтения
14,604 чтения

Принципы SOLID в Java: руководство для начинающих

к Pratik Singhal14m2024/03/22
Read on Terminal Reader

Слишком долго; Читать

Принципы SOLID — это принципы объектно-ориентированного программирования, необходимые для разработки масштабируемого программного обеспечения. Принципы: S: Принцип единой ответственности O: Принцип открытия/закрытости L: Принцип замены Лискова. I: Принцип разделения интерфейсов D: Принцип инверсии зависимостей
featured image - Принципы SOLID в Java: руководство для начинающих
Pratik Singhal HackerNoon profile picture
0-item

Принципы проектирования SOLID — это наиболее важные принципы проектирования, которые вам необходимо знать для написания чистого кода. Уверенное владение принципами SOLID — незаменимый навык для любого программиста. Они являются основой для разработки других шаблонов проектирования.


В этой статье мы рассмотрим принципы проектирования SOLID на реальных примерах и поймем их важность.


Вместе с полиморфизмом, абстракцией и наследованием принципы SOLID действительно важны для достижения успеха в объектно-ориентированном программировании.

Почему важны принципы SOLID?

Принципы SOLID важны по нескольким причинам:


  • Принципы SOLID позволяют нам писать чистый и удобный в сопровождении код: когда мы начинаем новый проект, изначально качество кода хорошее, потому что у нас есть ограниченный набор функций, которые мы реализуем. Однако по мере того, как мы добавляем больше функций, код начинает загромождаться.


  • Принципы SOLID основаны на основах абстракции, полиморфизма и наследования и приводят к созданию шаблонов проектирования для распространенных случаев использования. Понимание этих шаблонов проектирования помогает реализовать распространенные варианты использования в программировании.


  • Принципы SOLID помогают нам писать чистый код, который повышает его тестируемость. Это связано с тем, что код является модульным и слабосвязанным. Каждый модуль может быть разработан независимо и протестирован независимо.


Давайте теперь подробно рассмотрим каждый из принципов SOLID на реальных примерах.

1. Принцип единой ответственности

S в принципах SOLID означает принцип единой ответственности. Принцип единой ответственности гласит, что у класса должна быть только одна причина для изменений. Это ограничивает количество мест, в которых нам необходимо внести изменения при включении дополнительных требований в наш проект.

У каждого класса должна быть ровно одна причина для изменения.

Например, предположим, что мы разрабатываем банковское приложение на Java, где у нас есть класс SavingsAccount , который позволяет выполнять базовые операции, такие как дебет, кредит и sendUpdates . Метод sendUpdate принимает перечисление с именем NotificationMedium (например, электронная почта, SMS и т. д.) и отправляет обновление на соответствующем носителе. Мы напишем для этого код, как показано ниже.


 public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void sendNotification(NotificationMedium medium) { if (medium == NotificationMedium.SMS) { // Send SMS here } else if (medium == NotificationMedium.EMAIL) { // Send Email here } } }


 public enum NotificationMedium { SMS, EMAIL }


Теперь, если вы посмотрите на класс SavingsAccount выше, он может измениться по нескольким причинам:


  1. Если есть какие-либо изменения в основной логике класса SavingsAccount (например, debit , credit и т. д.).


  2. Если банк решит внедрить новое средство уведомлений (скажем, WhatsApp).


Это является нарушением принципа единой ответственности, изложенного в принципах SOLID. Чтобы исправить это, мы создадим отдельный класс, который будет отправлять уведомления.


Давайте проведем рефакторинг приведенного выше кода в соответствии с принципами SOLID.


 public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void printBalance() { System.out.println("Name: " + name+ " Account Balance: " + balance); } public void sendNotification(Medium medium) { Sender.sendNotification(medium, this); } }


 public enum NotificationMedium { SMS, EMAIL }


 public class Sender { public static void sendNotification(NotificationMedium medium, SavingsAccount account) { // extract account data from the account object if (medium == NotificationMedium.SMS) { //logic to send SMS here } else if (medium == NotificationMedium.EMAIL) { // logic to send Email here } } }


Теперь, поскольку мы провели рефакторинг кода, если будут какие-либо изменения в NotificationMedium или формате, мы изменим класс Sender . Однако если произойдут изменения в базовой логике SavingsAccount , изменения произойдут и в классе SavingsAccount .


Это исправляет нарушение, которое мы наблюдали в первом примере.

2. Принцип открытия/закрытия

Принцип Open Close гласит, что мы должны проектировать классы таким образом, чтобы они были открыты для расширения (в случае добавления дополнительных функций), но закрыты для модификации. Закрытие для внесения изменений помогает нам в двух отношениях:


  • Во многих случаях исходный исходный код класса может быть даже недоступен. Это может быть зависимость, потребляемая вашим проектом.


  • Сохранение исходного класса без изменений снижает вероятность возникновения ошибок. Поскольку могут существовать другие классы, зависящие от класса, который мы хотим изменить.


Чтобы увидеть пример принципа открытия и закрытия, давайте рассмотрим пример корзины покупок (например, реализованной на веб-сайтах электронной коммерции).


Я собираюсь создать класс под названием Cart , который будет содержать список Item , которые вы можете добавить в него. В зависимости от типа товара и налогообложения мы хотим создать метод, который рассчитывает общую стоимость корзины внутри класса Cart .


Классы должны быть открыты для расширения и закрыты для модификации.


 import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart() { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { if (item.getItemType() == GIFT) { // 8% tax on gift + 2% gift wrap cost value += (item.getValue()*1.08) + item.getValue()*0.02 } else if (item.getItemType() == ItemType.ELECTRONIC_ITEM) { value += (item.getValue()*1.11); } else { value += item.getValue()*1.10; } } return value; } }


 @Getter @Setter public abstract class Item { protected double price; private ItemType itemType; public double getValue() { return price; } }
 public enum ItemType { ELECTRONIC, GIFT }


В приведенном выше примере метод calculateCartValue вычисляет стоимость корзины, перебирая все элементы внутри корзины и вызывая логику в зависимости от типа элемента.


Хотя этот код выглядит правильно, он нарушает принципы SOLID.


Допустим, нам нужно добавить новое правило для другого типа товара (скажем, бакалеи) при расчете стоимости корзины. В этом случае нам придется изменить исходный класс Cart и написать внутри него еще одно условие else if , которое проверяет наличие товаров типа Grocery .


Однако при небольшом рефакторинге мы можем заставить код придерживаться принципа открытия/закрытия. Давайте посмотрим, как это сделать.


Сначала мы сделаем класс Item абстрактным и создадим конкретные классы для разных типов Item (ов), как показано ниже.


 public abstract class Item { protected double price; public double getValue() { return price; } }


 public class ElectronicItem extends Item { public ElectronicItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.11; } }


 public class GiftItem extends Item { public GiftItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.08 + super.getValue()*0.02; } }


 public class GroceryItem extends Item { public GroceryItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.03; } }


Внутри каждого конкретного класса Item, такого как GroceryItem , GiftItem и ElectronicItem , реализуется метод getValue() , который содержит бизнес-логику для налогообложения и расчета стоимости.


Теперь мы сделаем класс Cart зависимым от абстрактного класса Item и вызовем метод getValue() для каждого элемента, как показано ниже.


 import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } }


Теперь в этом рефакторинге кода, даже если вводятся новые типы Item (ов), класс Cart остаётся неизменным. Из-за полиморфизма, каким бы ни был фактический тип Item внутри items ArrayList , будет вызван метод getValue() этого класса.

3. Принцип замены Лискова.

Принцип замены Лискова гласит, что в данном коде, даже если мы заменим объект суперкласса объектом дочернего класса, код не должен сломаться. Другими словами, когда подкласс наследует суперкласс и переопределяет его методы, он должен поддерживать согласованность с поведением метода в суперклассе.


Например, если мы создадим следующие классы Vehicle и два класса Car и Bicycle class. Теперь предположим, что мы создаем метод startEngine() в классе Vehicle. Его можно переопределить в классе Car , но он не будет поддерживаться в классе Bicycle , поскольку Bicycle нет двигателя (см. пример кода ниже).


Подкласс должен поддерживать согласованность с поведением суперкласса при переопределении методов.

 class Vehicle { public void startEngine() { // start engine of the vehicle } } class Car extends Vehicle { @Override public void startEngine() { // Start Engine } } class Bicycle extends Vehicle { @Override public void startEngine(){ throw new UnsupportedOperationException("Bicycle doesn't have engine"); } }


Теперь предположим, что есть некоторый код, который ожидает объект типа автомобиль и использует метод startEngine() . Если при вызове этого фрагмента кода вместо передачи объекта типа Vehicle мы передадим объект Bicycle , это приведет к проблемам в коде. Поскольку метод класса Bicycle (s) выдаст исключение при вызове метода startEngine() . Это было бы нарушением принципов SOLID (принцип замены Лискова).


Чтобы решить эту проблему, мы можем создать два класса MotorizedVehicle и NonMotorizedVehicle и наследовать Car от класса MotorizedVehicle , а Bicycle — от NonMotorizedVehicle


 class Vehicle { } class MotorizedVehicle extends Vehicle { public void startEngine() { // start engine here } } class Car extends MotorizedVehicle { @Override public void startEngine() { // Start Engine } } class NonMotorizedVehicle extends Vehicle { public void startRiding() { // Start without engine } } class Bicycle extends NonMotorizedVehicle { @Override public void startRiding(){ // Start riding without the engine. } }


4. Принцип разделения интерфейса

Буква «I» в «Принципах SOLID» означает принцип разделения интерфейса.


Принцип разделения интерфейсов гласит, что вместо больших интерфейсов, которые заставляют реализующие классы реализовывать неиспользуемые методы, нам следует иметь меньшие интерфейсы и реализовывать классы. Таким образом, классы реализуют только соответствующие методы и остаются чистыми.

Разделите свои интерфейсы на несколько меньших интерфейсов, а не на один большой интерфейс.

Например, давайте посмотрим на встроенную структуру коллекций в Java. Помимо других структур данных, Java также предоставляет структуры данных LinkedList и ArrayList .


Класс ArrayList реализует следующие интерфейсы: Serializable , Cloneable , Iterable , Collection , List и RandomAccess .

Класс LinkedList реализует Serializable , Cloneable , Iterable , Collection , Deque , List и Queue .


Это довольно много интерфейсов!


Вместо того, чтобы иметь так много интерфейсов, разработчики Java могли бы объединить Serializable , Cloneable , Iterable , Collecton , List и RandomAccess в один интерфейс, скажем, интерфейс IList . Теперь оба класса ArrayList и LinkedList могли реализовать этот новый интерфейс IList .


Однако, поскольку LinkedList не поддерживает произвольный доступ, он мог бы реализовать методы интерфейса RandomAccess и выдать UnsupportedOperationException , когда кто-то попытается его вызвать.


Однако это было бы нарушением принципа сегрегации интерфейса в принципах SOLID, поскольку это «заставило бы» класс LinkedList реализовать методы внутри интерфейса RandomAccess , хотя это и не требуется.


Поэтому лучше разделить интерфейс на основе общего поведения и позволить каждому классу реализовывать множество интерфейсов, а не один большой интерфейс.

Принцип инверсии зависимостей

Принцип инверсии зависимостей гласит, что классы верхнего уровня не должны напрямую зависеть от классов нижнего уровня. Это вызывает плотную связь между двумя уровнями.


Вместо этого низшие классы должны предоставлять интерфейс, от которого должны зависеть классы верхнего уровня.


Зависите от интерфейсов, а не от классов


Например, давайте продолжим работу с примером Cart , который мы видели выше, и улучшим его, добавив несколько вариантов оплаты. Предположим, у нас есть два типа способов оплаты DebitCard и Paypal . Теперь в классе Cart мы хотим добавить метод placeOrder , который будет рассчитывать стоимость корзины и инициировать оплату на основе предоставленного платежа. метод.


Для этого мы могли бы добавить зависимость в приведенном выше примере с Cart , добавив два варианта оплаты в качестве полей внутри класса Cart . Однако это будет тесно связывать класс Cart с классами DebitCard и Paypal .


Вместо этого мы бы создали интерфейс Payment , и классы DebitCard и Paypal реализовали бы интерфейсы Payment . Теперь класс Cart будет зависеть от интерфейса Payment , а не от отдельных типов оплаты. Это сохраняет классы слабосвязанными.


См. код ниже.


 public interface Payment { void doPayment(double amount); } public class PaypalPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate paypal payment } } public class DebitCardPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate payment via debit card } } import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; private Payment paymentOption; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); this.paymentOption = paymentOption; } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } public void placeOrder() { this.paymentOption.doPayment(calculateCartValue()); } }


Если вы хотите узнать больше об объектно-ориентированном программировании, шаблонах проектирования GoF и собеседованиях по низкоуровневому дизайну, посетите мой высокорейтинговый курс «Объектно-ориентированное программирование + шаблоны проектирования Java».