Найти в Дзене
JavAKnazzz

Java. Принципы SOLID

SOLID - это аббревиатура, используемая для описания пяти основных принципов объектно-ориентированного программирования (ООП), которые помогают разработчикам создавать более поддерживаемый и расширяемый код. Примеры применения этих принципов: Что такое Single Responsibility Principle (Принцип единственной ответственности)? Принцип единственной ответственности (Single responsibility principle) - это принцип объектно-ориентированного программирования, который утверждает, что класс должен иметь только одну причину для изменения, то есть должен быть ответственным только за одну функциональность. Если класс имеет несколько функциональностей, то изменение одной из них может привести к ошибкам в работе других функциональностей, что увеличивает сложность кода и усложняет его поддержку. Данный принцип является частью SOLID-принципов, которые были предложены Робертом Мартином в книге "Чистый код". Цель этих принципов заключается в том, чтобы улучшить качество кода, сделать его более читаемым, п
Оглавление

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

  • Принцип единственной ответственности (Single Responsibility Principle, SRP) - класс должен иметь только одну ответственность. Например, класс, отвечающий за работу с базой данных, не должен также заниматься обработкой пользовательского ввода или выводом на экран.
  • Принцип открытости/закрытости (Open/Closed Principle, OCP) - классы должны быть открыты для расширения, но закрыты для модификации. Это означает, что новый функционал должен добавляться через добавление новых классов или методов, а не изменение существующих.
  • Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP) - объекты одного класса могут быть заменены объектами другого класса, производного от него, не нарушая работоспособность программы. Например, класс "фрукт" может быть заменен производными классами "яблоко", "груша", "апельсин" и т. д.
  • Принцип разделения интерфейса (Interface Segregation Principle, ISP) - клиенты не должны зависеть от интерфейсов, которые они не используют. Интерфейсы должны быть маленькими и специфическими для конкретных задач.
  • Принцип инверсии зависимостей (Dependency Inversion Principle, DIP) - модули верхнего уровня не должны зависеть от модулей нижнего уровня. Их зависимости должны быть инвертированы через абстракции. Например, класс, который использует базу данных, должен зависеть от абстрактного интерфейса базы данных, а не от конкретной реализации базы данных.

Примеры применения этих принципов:

Что такое Single Responsibility Principle (Принцип единственной ответственности)?

Принцип единственной ответственности (Single responsibility principle) - это принцип объектно-ориентированного программирования, который утверждает, что класс должен иметь только одну причину для изменения, то есть должен быть ответственным только за одну функциональность. Если класс имеет несколько функциональностей, то изменение одной из них может привести к ошибкам в работе других функциональностей, что увеличивает сложность кода и усложняет его поддержку. Данный принцип является частью SOLID-принципов, которые были предложены Робертом Мартином в книге "Чистый код". Цель этих принципов заключается в том, чтобы улучшить качество кода, сделать его более читаемым, поддерживаемым и расширяемым.

Принцип единственной ответственности (SRP) - это принцип объектно-ориентированного проектирования, который гласит, что каждый объект должен иметь только одну ответственность и все его сервисы должны быть направлены исключительно на обеспечение этой ответственности.

Вот несколько примеров использования SRP в Java:

  • Класс Customer может иметь только одну ответственность, например, хранить данные о клиенте и предоставлять методы для работы с этими данными. Класс должен быть разделен на две части: одна для хранения информации о клиенте, а другая для обработки ее.

public class Customer {
private int id;
private String name;
private String address;

// methods for getting and setting customer information ...
}

public class CustomerRepository {
// methods for saving, updating, and deleting customer data ...
}

  • Класс Employee также может иметь только одну ответственность - чтобы содержать информацию о работнике и методы для работы с этой информацией. Этот класс также может быть разделен на две части - одна для хранения информации, а другая для обработки.

public class Employee {
private int id;
private String name;
private String address;
private String position;

// methods for getting and setting employee information ...
}

public class EmployeeRepository {
// methods for saving, updating, and deleting employee data ...
}

  • Класс FileReader может иметь только одну ответственность - чтение данных из файла. Этот класс не должен использоваться для трансформации или обработки данных, он должен выполнять только одну задачу - чтение данных из файла.

public class FileReader {
public List<String> readFile(String filename) {...}
}

Все вышеупомянутые классы имеют только одну ответственность

Что такое Open Closed Principle (Принцип открытости/закрытости)?

Принцип открытости/закрытости (Open/Closed Principle, OCP) - классы должны быть открыты для расширения, но закрыты для модификации. Иными словами, вы должны иметь возможность добавлять новую функциональность без изменения старого кода.

Принцип открытости/закрытости (Open Closed Principle, OCP) в объектно-ориентированном программировании означает, что сущность должна быть открыта для расширения, но закрыта для модификации. Суть заключается в том, что при добавлении новой функциональности к системе не следует изменять существующий рабочий код, вместо этого следует добавлять новый код. Это помогает сделать код более гибким и способствует улучшению его качества и поддерживаемости.

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

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

Изучение и применение принципа OCP в своих проектах может помочь сделать код более гибким и снизить уровень зависимости между различными частями системы.

Пример на Java:

// Плохой пример нарушает OCP public class Shape {
private String type;

public void draw() {
if (type.equalsIgnoreCase("circle")) {
drawCircle();
} else if (type.equalsIgnoreCase("square")) {
drawSquare();
}
}

private void drawCircle() {
// логика рисования круга }

private void drawSquare() {
// логика рисования квадрата }
}

// Хороший пример OCP public abstract class Shape {
public abstract void draw();
}

public class Circle extends Shape {
@Override public void draw() {
// логика рисования круга }
}

public class Square extends Shape {
@Override public void draw() {
// логика рисования квадрата }
}

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

Классы Circle и Square следуют принципу OCP, так как они наследуются от абстрактного класса Shape и имеют свою собственную реализацию метода draw(). Если мы захотим добавить новый тип фигуры, нам просто нужно будет создать новый класс, наследуемый от Shape

Что такое Liskov’s Substitution Principle (Принцип подстановки Барбары Лисков)?

Принцип подстановки Барбары Лисков (Liskov's Substitution Principle, LSP) - это принцип SOLID-архитектуры, который гласит, что объекты в программе должны быть заменяемыми их наследниками без изменения корректности программы.

Пример на Java:

class Bird {
public void fly() {
// выполнение полета }
}

class Duck extends Bird {
public void swim() {
// выполнение плавания }
}

class Ostrich extends Bird {
public void run() {
// выполнение бега }
}

public class Main {
public static void main(String[] args) {
Bird duck = new Duck();
duck.fly(); // вызывает метод лета у объекта Duck
Bird ostrich = new Ostrich();
ostrich.fly(); // ошибка компиляции, т.к. страус не умеет летать }
}

Здесь подклассы Bird - это наследники класса Bird, который содержит метод fly(). Однако, Ostrich не умеет летать, так что вызов метода fly() приводит к ошибке. Таким образом, Ostrich не является заменяемым на Bird без нарушения принципа LSP.

Пример, который следует принципу LSP:

class Bird {
public void move() {
// выполнение движения }
}

class Duck extends Bird {
public void move() {
// выполнение полета или плавания }
}

class Ostrich extends Bird {
public void move() {
// выполнение бега }
}

public class Main {
public static void main(String[] args) {
Bird duck = new Duck();
duck.move(); // вызывает метод move() у объекта Duck, это может быть полет или плавание Bird ostrich = new Ostrich();
ostrich.move(); // вызывает метод move() у объекта Ostrich, это бег }
}

Что такое Interface Segregation Principle (Принцип разделения интерфейса)?

Принцип разделения интерфейса (Interface Segregation Principle, ISP) является одним из пяти принципов SOLID для объектно-ориентированного программирования. Он заключается в том, что клиенты не должны зависеть от методов, которые они не используют.

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

Вот пример реализации ISP на Java:

interface Vehicle {
void startEngine();
void stopEngine();
void speedUp();
void slowDown();
}

interface Car extends Vehicle {
void turnOnAC();
void turnOffAC();
}

interface Motorcycle extends Vehicle {
void putHelmetOn();
}

В данном примере интерфейс Vehicle содержит четыре метода, которые должны быть реализованы всеми транспортными средствами. Затем мы создаем два специализированных интерфейса - Car и Motorcycle - которые содержат только те методы, которые соответствуют конкретному типу транспортного средства. Это позволяет клиентам использовать только те методы, которые им нужны, вместо того, чтобы иметь доступ к всем методам в одном интерфейсе.

Например, если у нас есть объект car типа Car, то мы можем использовать методы turnOnAC() и turnOffAC() для управления кондиционером, но не можем использовать методы putHelmetOn(), которые присутствуют только в интерфейсе Motorcycle.

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

Еще пример, который демонстрирует принцип разделения интерфейса в Java:

public interface Printer {
void print();
}

public interface Scanner {
void scan();
}

public interface Fax {
void fax();
}

public class AllInOnePrinter implements Printer, Scanner, Fax {
public void print() {
// код для печати }

public void scan() {
// код для сканирования }

public void fax() {
// код для отправки факса }
}

public class SimplePrinter implements Printer {
public void print() {
// код для печати }
}

Здесь мы определили три интерфейса: Printer, Scanner и Fax, каждый из которых имеет один метод. После этого мы определили два класса: AllInOnePrinter, который реализует все три интерфейса, и SimplePrinter, который реализует только Printer.

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

Теперь, если у клиента возникнет потребность только в печати документов, ему можно будет использовать класс SimplePrinter без необходимости создавать экземпляр класса AllInOnePrinter.

Что такое Dependency Inversion Principle (Принцип инверсии зависимостей)?

Dependency Inversion Principle (Принцип инверсии зависимостей) - это принцип SOLID, который гласит, что абстракции не должны зависеть от деталей, а детали должны зависеть от абстракций. То есть, высокоуровневые модули не должны зависеть от низкоуровневых, а должны зависеть от абстракций, которые могут быть реализованы как в низкоуровневых, так и в высокоуровневых модулях.

Пример на Java:

public interface MessageSender {
void sendMessage(String message);
}

public class EmailMessageSender implements MessageSender {
public void sendMessage(String message) {
// sending email message }
}

public class SmsMessageSender implements MessageSender {
public void sendMessage(String message) {
// sending SMS message }
}

public class NotificationService {
private MessageSender messageSender;
public NotificationService(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void sendNotification(String message) {
messageSender.sendMessage(message);
}
}

public class MyApp {
public static void main(String[] args) {
MessageSender messageSender = new EmailMessageSender();
NotificationService notificationService = new NotificationService(messageSender);
notificationService.sendNotification("Hello World!");
}
}

В этом примере зависимость между NotificationService и MessageSender инвертирована. Мы создаем экземпляр MessageSender вне NotificationService и передаем его через конструктор. Таким образом, NotificationService не зависит от конкретной реализации MessageSender, а зависит только от абстракции MessageSender. Это позволяет нам легко заменять конкретные реализации MessageSender, добавлять новые реализации и тестировать NotificationService независимо от реализации MessageSender.