Каждый разработчик знает, каково это — увидеть код, который страшно трогать. В нём всё ломается, стоит добавить пару строк. Чтобы такого не было, мир придумал SOLID — набор из пяти принципов, которые делают ваш код понятным, надёжным и лёгким в поддержке.
В этой статье рассмотрим, как внедрять эти принципы с умом, и да, будет немного котиков — куда без них.
Теперь разберем каждый принцип отдельно.
Вы наверняка видели такой код:
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 занимается только своим поведением, а уборка делегирована сервису. Хотите добавить собачку? Это получится легко.
Допустим, нужно добавить новых котов и пишем код вот так:
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();
}
}
При добавлении нового кота мы просто пишем новый класс. Сервис остаётся неизменным. Это и есть принцип открытости/закрытости.
Пример:
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.
Раздутый интерфейс:
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("Лазаю по деревьям.");
}
}
Прямые зависимости:
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. Не потому, что это красиво звучит, а потому что это спасёт вас от десятков бессонных ночей.
В этой статье рассмотрим, как внедрять эти принципы с умом, и да, будет немного котиков — куда без них.
Что такое SOLID и зачем это нужно?
SOLID — это пять принципов проектирования, которые помогают писать читаемый, сопровождаемый и расширяемый код.- S (Single Responsibility): один класс — одна ответственность.
- O (Open/Closed): открыт для расширения, закрыт для изменения.
- L (Liskov Substitution): дочерние классы заменяют родительские без сюрпризов.
- I (Interface Segregation): узкие интерфейсы лучше широких.
- D (Dependency Inversion): зависимость от абстракций, а не реализаций.
- Изменение одной части приложения ломает всё (нарушение S).
- Добавление новой фичи требует переписывать старый код (нарушение O).
- Наследники ведут себя непредсказуемо (нарушение L).
- Перегруженные интерфейсы заставляют писать лишний код (нарушение I).
- Зависимость от конкретных реализаций делает код негибким (нарушение D).
Теперь разберем каждый принцип отдельно.
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. Не потому, что это красиво звучит, а потому что это спасёт вас от десятков бессонных ночей.
SOLID на котиках: коротко и по делу
Каждый разработчик знает, каково это — увидеть код, который страшно трогать. В нём всё ломается, стоит добавить пару строк. Чтобы такого не было, мир придумал SOLID — набор из пяти принципов, которые...
habr.com