Хотите писать лучший код? Вот как это сделать! Объяснил мемами 😏

Принцип S.O.L.I.D: супергеройское руководство по написанию чистого кода!

Спасите свой код от темной бездны спагетти-кода с помощью этих простых принципов!

Если вы разработчик программного обеспечения, возможно, вы слышали о S.O.L.I.D. Это аббревиатура, которая означает:

  • Принцип единой ответственности (SRP)
  • Открытый/закрытый принцип (OCP)
  • Принцип замещения Лисков (LSP)
  • Принцип разделения интерфейсов (ISP)
  • Принцип инверсии зависимостей (DIP)

Эти принципы были впервые представлены Робертом С. Мартином (также известным как дядя Боб), и они направлены на то, чтобы помочь разработчикам создавать программное обеспечение, которое легче поддерживать, расширять и понимать.

Давайте углубимся в каждый принцип более подробно, приведя несколько примеров кода Java, чтобы проиллюстрировать концепции.

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

SRP утверждает, что класс должен иметь только одну единственную цель. Другими словами, у класса должна быть только одна обязанность. Этот принцип помогает гарантировать, что ваш код будет целенаправленным, читабельным и простым в сопровождении. Мы не должны нагружать класс излишней функциональностью.

Допустим, у вас есть класс User, который обрабатывает аутентификацию пользователей и уведомления по электронной почте. Вот пример того, как вы можете реорганизовать этот класс, чтобы он соответствовал SRP:

// Before refactoring

public class User {
  public void login(String username, String password) {
    // Authenticate user
  }
  
  public void sendEmail(String to, String subject, String body) {
    // Send email
  }
}
// After refactoring

public class User {
  public void login(String username, String password) {
    // Authenticate user
  }
}

public class EmailService {
  public void sendEmail(String to, String subject, String body) {
    // Send email
  }
}

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

OCP утверждает, что программные объекты (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации. Другими словами, вы должны иметь возможность добавлять новые функции в свой код, не изменяя существующий код.

Допустим, у вас есть класс PaymentProcessor, который обрабатывает платежи для различных способов оплаты. Вот пример того, как вы можете реорганизовать этот класс, чтобы он следовал OCP:

// Before refactoring

public class PaymentProcessor {
  public void processPayment(Payment payment) {
    if (payment.getMethod() == PaymentMethod.CREDIT_CARD) {
      // Process credit card payment
    } else if (payment.getMethod() == PaymentMethod.PAYPAL) {
      // Process PayPal payment
    } else if (payment.getMethod() == PaymentMethod.BITCOIN) {
      // Process Bitcoin payment
    }
  }
}
// After refactoring

public interface PaymentMethodProcessor {
  void processPayment(Payment payment);
}

public class CreditCardProcessor implements PaymentMethodProcessor {
  public void processPayment(Payment payment) {
    // Process credit card payment
  }
}

public class PayPalProcessor implements PaymentMethodProcessor {
  public void processPayment(Payment payment) {
    // Process PayPal payment
  }
}

public class BitcoinProcessor implements PaymentMethodProcessor {
  public void processPayment(Payment payment) {
    // Process Bitcoin payment
  }
}

public class PaymentProcessor {
  private Map<PaymentMethod, PaymentMethodProcessor> processors = new HashMap<>();
  
  public PaymentProcessor() {
    processors.put(PaymentMethod.CREDIT_CARD, new CreditCardProcessor());
    processors.put(PaymentMethod.PAYPAL, new PayPalProcessor());
    processors.put(PaymentMethod.BITCOIN, new BitcoinProcessor());
  }
  
  public void processPayment(Payment payment) {
    PaymentMethodProcessor processor = processors.get(payment.getMethod());
    if (processor != null) {
      processor.processPayment(payment);
    }
  }
}

Теперь мы можем добавить класс Stripe Payment Processor, не изменяя существующие коды.

3. Принцип замещения Лисков (LSP)

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

Допустим, у вас есть класс Rectangle, который вычисляет площадь прямоугольника. Вот пример того, как вы можете реорганизовать этот класс, чтобы он следовал LSP:

// Before refactoring

public class Rectangle {
  private int width;
  private int 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;
  }
}

public class Square extends Rectangle {
  public void setWidth(int width) {
    super.setWidth(width);
    super.setHeight(width);
  }
  
  public void setHeight(int height) {
    super.setWidth(height);
    super.setHeight(height);
  }
}

// Test code
Rectangle rect = new Square();
rect.setWidth(5);
rect.setHeight(10);
System.out.println(rect.getArea()); // Expected output: 50

В этом примере класс Square наследуется от класса Rectangle, но нарушает LSP, поскольку изменяет поведение суперкласса. Класс Square устанавливает для ширины и высоты одно и то же значение, чего нельзя ожидать от прямоугольника. Чтобы исправить это, вы можете изменить дизайн, чтобы использовать отдельные классы Square и Rectangle.

// After refactoring

public class Rectangle {
  protected int width;
  protected int height;
  
  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;
  }
}

public class Square extends Rectangle {
  public Square(int size) {
    super(size, size);
  }
}

// Test code
Rectangle rect = new Rectangle(5, 10);
System.out.println(rect.getArea()); // Expected output: 50

Square square = new Square(5);
System.out.println(square.getArea()); // Expected output: 25

4. Принцип разделения интерфейсов (ISP)

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

Допустим, у вас есть интерфейс Printer с двумя методами: print() и scan(). Вот пример того, как вы можете реорганизовать этот интерфейс, чтобы он соответствовал интернет-провайдеру:

// Before refactoring

public interface Printer {
  void print();
  void scan();
}

public class LaserPrinter implements Printer {
  public void print() {
    // Print document
  }
  
  public void scan() {
    // Scan document
  }
}

public class InkjetPrinter implements Printer {
  public void print() {
    // Print document
  }
  
  public void scan() {
    // Do nothing
  }
}
// After refactoring

public interface Printable {
  void print();
}

public interface Scanable {
  void scan();
}

public class LaserPrinter implements Printable, Scanable {
  public void print() {
    // Print document
  }

public void scan() {
  // Scan document
  }
}

public class InkjetPrinter implements Printable {
  public void print() {
    // Print document
  }
}

// Test code
Printable laserPrinter = new LaserPrinter();
laserPrinter.print();
((Scanable)laserPrinter).scan();

Printable inkjetPrinter = new InkjetPrinter();
inkjetPrinter.print();

В этом примере интерфейс Printer имеет два метода: print() и scan(). Классы LaserPrinter и InkjetPrinter реализуют интерфейс Printer, но класс InkjetPrinter не использует метод scan(), что нарушает ISP. Чтобы исправить это, вы можете создать отдельные интерфейсы Printable и Scanable, которые проще реализовать и использовать.

5. Принцип инверсии зависимостей (DIP)

Принцип инверсии зависимостей (DIP) гласит, что модули высокого уровня не должны зависеть от модулей низкого уровня. Вместо этого оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций. Другими словами, ваш код должен зависеть от интерфейсов или абстрактных классов, а не от конкретных реализаций.

Допустим, у вас есть класс Book, которому необходимо получить данные из класса Database. Вот пример того, как вы можете реорганизовать этот код, чтобы он следовал DIP:

// Before refactoring

public class Book {
 private Database database;
 
 public Book() {
 this.database = new Database();
 }
 
 public String getTitle(int id) {
 return database.getTitle(id);
 }
}
public class Database {
 public String getTitle(int id) {
 // Retrieve title from database
 return "Title";
 }
}
// After refactoring

public interface Database {
 String getTitle(int id);
}
public class Book {
 private Database database;
 
 public Book(Database database) {
 this.database = database;
 }
 
 public String getTitle(int id) {
 return database.getTitle(id);
 }
}
public class DatabaseImpl implements Database {
 public String getTitle(int id) {
 // Retrieve title from database
 return "Title";
 }
}
// Test code
Database database = new DatabaseImpl();
Book book = new Book(database);
System.out.println(book.getTitle(123)); // Expected output: "Title"

В этом примере класс Book зависит от класса Database, что нарушает DIP. Чтобы исправить это, вы можете создать интерфейс Database, от которого зависит класс Book, и создать отдельный класс DatabaseImpl, реализующий этот интерфейс. Таким образом, класс Book зависит от абстракции, а не от конкретной реализации. Следование DIP делает код подключаемым. Как? В приведенном выше примере мы можем переключиться на другую реализацию базы данных (например, перейти на PgSQL с MySql), и система не сломается.

Заключение

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

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

Подводя итог, принципы SOLID можно резюмировать следующим образом:

  • SRP: Класс должен иметь только одну единственную цель.
  • OCP: объекты программного обеспечения должны быть открыты для расширения, но закрыты для модификации.
  • LSP: Подтипы должны быть взаимозаменяемыми для своих базовых типов.
  • Интернет-провайдер: Клиенты не должны зависеть от интерфейсов, которые они не используют.
  • DIP: Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.

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

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

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

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

Спасибо за чтение и удачного кодирования!

Want to Connect? If you have any feedback, 
please ping me on my LinkedIn: https://linkedin.com/in/shuhanmirza/