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

SOLID — это аббревиатура, и каждый символ этой аббревиатуры относится к принципу OOD, описанному ниже.

S — принцип единой ответственности

У одного класса должна быть только одна обязанность. Иногда вы можете увидеть этот принцип, записанный как «Один класс должен иметь только одну причину для изменения». Все это означает, что класс должен выполнять только одну работу, и делать это правильно!

Давайте рассмотрим небольшой пример кода.

Скажем, у нас есть класс, отвечающий за управление нашими пользователями, код которого можно увидеть ниже.

public class User {
 private String name;
 private int age;
 public User(int name, int age){
  this.name = name;
  this.age = age;
 }
 public void saveUserToDatabase(){
  //implementation detail
 }
 
  //getters and setters
  …
}

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

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

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

public class User {
 private String name;
 private int age;
 private InfoPersistenseProvider database;
 public User(int name, int age, InfoPersistenseProvider database){
  this.name = name;
  this.age = age;
  this.database = database;
 }
 public void saveUserToDatabase(){
  this.database.saveToDatabase();
 }
 
  //getters and setters
  …
}
public class InfoPersistenseProvider{
 public InfoPersistenseProvider(){}
 public void saveToDatabase(User user){
  //implementation detail
 }
 //getters and setters
 …
}

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

O— открытый/закрытый принцип

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

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

public class Video{
 private String type;
//…
 public double computeUserInterest(){
  if(this.type.equals(“Movie”)){
   //compute movie interest
  }else if(this.type.equals(“TVShow”)){
   //compute TVShow interest
  }
 }
}

Что произойдет, если вы захотите добавить 10 новых типов видео на нашу платформу? Наше «если-иначе» станет гигантским. Этот пример, где мы постоянно добавляем блоки if, представляет собой классический пример нарушения принципа Open/Closed.

Как мы можем следовать этому принципу?
В приведенном ниже блоке кода показано, как это сделать.

public interface Video{
 public double computeUserInterest();
}
public class Movie implements Video{
 public double computeUserInterest(){
  //implementation detail
 }
}
public class TVShow implements Video{
 public double computeUserInterest(){
  //implementation detail
 }
}

Что произойдет, если вы захотите добавить новый тип видео на нашу платформу? Вам просто нужно заставить класс реализовать наш интерфейс. Это делает наш код чище и, что более важно, подчиняется принципу Open/Close.

L — Принцип замещения Лискова

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

Давайте проанализируем предстоящий блок кода, чтобы лучше понять эту концепцию!

public class Movie{
 public void play(){
  //implementation detail
 }
 public void increaseVolume(){
  //implementation detail
 }}
public class Joker extends Movie{
}
public class ASilentMovie extends Movie{
 @Override
 public void increaseVolume(){}
}

Допустим, у нас есть суперкласс Movie, у которого есть два основных действия: play (запускает фильм) иintensionVolume(увеличивает громкость фильма). Эта логика будет работать в большинстве современных фильмов, но есть рыночная ниша для немых фильмов, которые, как мы все знаем, не имеют большого объема.

Будет ли у нас правильное поведение, если вдруг в нашем коде мы заменим объект Movie другим объектом, представляющим немой фильм? Ответ - нет. Нет, из-за действия по увеличению громкости. Неправильно говорить: «Я увеличу громкость немого кино», потому что в такого рода фильмах нет такого понятия, как громкость! Это означает, что мы не соблюдаем принцип подстановки Лисков.

I — принцип разделения интерфейса

В соответствии с этим принципом нельзя заставлять класс реализовывать методы, от которых он не зависит. Хорошее эмпирическое правило заключается в том, что если вы обнаружите, что создаете фиктивную/пустую реализацию метода (например, увеличение объема в классе ASilentMovie в предыдущем примере), вы нарушаете этот принцип. Цель здесь состоит в том, чтобы наши классы имели только те методы, которые им необходимы для выполнения своей работы.

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

public interface Movie{
 public void play();
 public void increaseVolume();
}
public class Joker implements Movie{
  public void play(){
   //implementation detail
  }
 public void increaseVolume(){
  //implementation detail
 }
}
public class ASilentMovie implements Movie{
  public void play(){
   //implementation detail
  }
 public void increaseVolume(){
  //implementation detail [PROBLEM HERE]
 }
}

Как мы видели из предыдущего примера Movie, возникла проблема, когда мы попытались увеличить громкость немого фильма. Мы нарушали принцип подстановки Лисков. Как вы видите, и как упоминалось ранее, мы также нарушаем принцип разделения интерфейса, потому что класс ASilentMovie зависит (реализуется) от метода, который не используется.

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

public interface MoviePlayManager{
 public void play();
}
public interface AudioManager{
 public void increaseVolume();
}
public class Joker implements MoviePlayManager,AudioManager{
  public void play(){
   //implementation detail
  }
  public void increaseVolume(){
   //implementation detail
  }
}
public class ASilentMovie implements MoviePlayManager{
  public void play(){
   //implementation detail
  }
}

Как видите, наш класс ASilentMovie зависит только от методов, которые он фактически использует!

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

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

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

public class ComedyCategory{
 
}
public class Movie{
 private String name;
 private ComedyCategory comedyCategory;
 public Movie(String name, ComedyCategory comedyCategory){
 }
 public ComedyCategory getCategory(){
  return this.comedyCategory;
 }
 public void setCategory(ComedyCategory comedyCategory){
  this.comedyCategory = comedyCategory;
 }
}

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

Что происходит с примером, который я только что привел, так это то, что наш класс фильма зависит от конкретной реализации категории (в данном случае категории комедии). Таким образом, мы создаем тесную связь между нашим классом Movie и классом ComedeCategory. Мы хотим избежать этих тесных связей.

Один из способов сделать это — создать абстракцию/интерфейс категории и наследовать/реализовать ее всеми существующими категориями.

Следующий рефакторинг пытается показать это на практике.

public interface Category{
}
public class ComedyCategory implements Category{
 
}
public class Movie{
 private String name;
 private Category comedyCategory;
 public Movie(String name, Category comedyCategory){
 }
 public ComedyCategory getCategory(){
  return this.comedyCategory;
 }
 public void setCategory(Category comedyCategory){
  this.comedyCategory = comedyCategory;
 }
}

Как видите, теперь наш класс Movie зависит только от абстракции категории, а не от конкретной реализации категории. Что это означает в практическом плане?

С нашим первым подходом, чтобы создать экземпляр класса Movie, у нас должно быть что-то вроде `Movie Joker = new Movie("Joker", new ComedyCategory())`. Если бы мы попытались сделать `Moviejoker = new Movie("Joker", new DramaCategory())`, то получили бы ошибку!

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

😁Надеюсь, это помогло!

Это пока все! Большое спасибо за чтение. Если вы более визуальный ученик, обратитесь к этой замечательной статье, которая объясняет предыдущие концепции более наглядно!

Если у вас есть какие-либо вопросы, пожалуйста, не стесняйтесь задавать их.
Подписывайтесь на меня, если хотите узнать больше об этих темах!

Ссылки на социальные сети:
LinkedIn
Twitter
Github