Каждый разработчик знает, каково это — увидеть код, который страшно трогать. В нём всё ломается, стоит добавить пару строк. Чтобы такого не было, мир придумал 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/
Добавить комментарий