SOLID на котиках: коротко и по делу

—

от автора

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

В этой статье рассмотрим, как внедрять эти принципы с умом, и да, будет немного котиков — куда без них.

Что такое SOLID и зачем это нужно?

SOLID — это пять принципов проектирования, которые помогают писать читаемый, сопровождаемый и расширяемый код.

  • S (Single Responsibility): один класс — одна ответственность.

  • O (Open/Closed): открыт для расширения, закрыт для изменения.

  • L (Liskov Substitution): дочерние классы заменяют родительские без сюрпризов.

  • I (Interface Segregation): узкие интерфейсы лучше широких.

  • D (Dependency Inversion): зависимость от абстракций, а не реализаций.

Зачем это нужно?

  • Изменение одной части приложения ломает всё (нарушение S).

  • Добавление новой фичи требует переписывать старый код (нарушение O).

  • Наследники ведут себя непредсказуемо (нарушение L).

  • Перегруженные интерфейсы заставляют писать лишний код (нарушение I).

  • Зависимость от конкретных реализаций делает код негибким (нарушение D).

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

Теперь разберем каждый принцип отдельно.

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

Каждый класс должен отвечать за одну-единственную задачу. Т.е он должен быть о чём-то одном.

Вы наверняка видели такой код:

public class Cat {     private String name;      public Cat(String name) {         this.name = name;     }      public void eat() {         System.out.println(name + " ест.");     }      public void sleep() {         System.out.println(name + " спит.");     }      public void cleanLitterBox() {         System.out.println("Убираем лоток для " + name + ".");     } }

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

Разделим всё на отдельные классы:

public class Cat {     private final String name;      public Cat(String name) {         this.name = name;     }      public void eat() {         System.out.println(name + " ест.");     }      public void sleep() {         System.out.println(name + " спит.");     } }  public class LitterBoxService {     public void cleanLitterBox(String animalName) {         System.out.println("Убираем лоток для " + animalName + ".");     } }

Теперь Cat занимается только своим поведением, а уборка делегирована сервису. Хотите добавить собачку? Это получится легко.

O: принцип открытости/закрытости

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

Допустим, нужно добавить новых котов и пишем код вот так:

public class CatService {     public void makeSound(String catType) {         if (catType.equals("домашний")) {             System.out.println("Мяу!");         } else if (catType.equals("дикий")) {             System.out.println("Ррр!");         } else {             throw new IllegalArgumentException("Неизвестный тип кота.");         }     } }

Каждый новый тип кота — новая ветка в if-else. Этот код плохо тестируется, и его сложно поддерживать.

Используем интерфейсы и создаём классы для каждого типа кота:

public interface Cat {     void makeSound(); }  public class DomesticCat implements Cat {     @Override     public void makeSound() {         System.out.println("Мяу!");     } }  public class WildCat implements Cat {     @Override     public void makeSound() {         System.out.println("Ррр!");     } }

А теперь сервис:

public class CatService {     public void playWithCat(Cat cat) {         cat.makeSound();     } }

При добавлении нового кота мы просто пишем новый класс. Сервис остаётся неизменным. Это и есть принцип открытости/закрытости.

L: принцип подстановки Барбары Лисков

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

Пример:

public class Cat {     public void eat() {         System.out.println("Кот ест.");     } }  public class ToyCat extends Cat {     @Override     public void eat() {         throw new UnsupportedOperationException("Игрушечный кот не ест.");     } }

Если метод работает с Cat, то передача ToyCat вызовет ошибку.

Используем интерфейс для общего поведения:

public interface Cat {     void makeSound(); }  public class RealCat implements Cat {     @Override     public void makeSound() {         System.out.println("Мяу!");     } }  public class ToyCat implements Cat {     @Override     public void makeSound() {         System.out.println("Пи-пи!");     } }

Теперь ToyCat никогда не вызовет UnsupportedOperationException.

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

Большие интерфейсы — зло. Лучше несколько маленьких, чем один огромный.

Раздутый интерфейс:

public interface Cat {     void eat();     void sleep();     void climbTree(); }

Не все коты лазают по деревьям. Если HouseCat имплементирует этот интерфейс, ему придётся писать пустую реализацию climbTree.

Разделим интерфейсы по ролям:

public interface BasicCat {     void eat();     void sleep(); }  public interface TreeClimbingCat {     void climbTree(); }

Теперь классы реализуют только то, что им нужно:

public class HouseCat implements BasicCat {     @Override     public void eat() {         System.out.println("Мяу, я ем.");     }      @Override     public void sleep() {         System.out.println("Я дремлю.");     } }  public class ForestCat implements BasicCat, TreeClimbingCat {     @Override     public void eat() {         System.out.println("Я ем мышей.");     }      @Override     public void sleep() {         System.out.println("Дремлю на дереве.");     }      @Override     public void climbTree() {         System.out.println("Лазаю по деревьям.");     } }

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

Высокоуровневые модули не должны зависеть от низкоуровневых. Всё должно зависеть от абстракций.

Прямые зависимости:

public class Cat {     private final DryFood food = new DryFood();      public void eat() {         food.consume();     } }  public class DryFood {     public void consume() {         System.out.println("Кот ест сухой корм.");     } }

Теперь Cat жёстко завязан на DryFood.

Внедрение зависимостей:

public class Cat {     private final Food food;      public Cat(Food food) {         this.food = food;     }      public void eat() {         food.consume();     } }  public interface Food {     void consume(); }  public class DryFood implements Food {     @Override     public void consume() {         System.out.println("Кот ест сухой корм.");     } }  public class WetFood implements Food {     @Override     public void consume() {         System.out.println("Кот ест влажный корм.");     } }

Теперь вы можно передавать любой тип еды, а Cat останется неизменным.


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

Код, построенный на принципах SOLID, переживёт не только первый релиз, но и бесконечные «давайте добавим ещё вот это» от бизнеса. Он станет тем проектом, который другие разработчики будут вспоминать с теплотой, а не с дрожью.

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

Статья подготовлена в рамках специализации «Java-разработчик». Прочитать полную программу и посмотреть записи открытых уроков можно на странице курса.


ссылка на оригинал статьи https://habr.com/ru/articles/874104/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *