
Данная статья является более расширенной версией моей научной статьи, на примере разработанного ПО для спортсменов-биатлонистов c абстрактными примерами, соответствующих тематики.
Далее будут приведены и рассмотрены различные аналогии с принципами разработки, с помощью соответствующих теме изображений, сравнительных таблиц и примеров кода на Java
. Несмотря на то, что тема данной работы является достаточно заезженной, и по ней написано немало других статей, хотелось сделать работу, которая будет не только легко восприниматься человеком, не знакомым с Java
, за счет абстракций и различных изображений, но также понятна и для разработчика, который сможет рассмотреть каждый из принципов на конкретном кодовом примере.
Будут рассмотрены следущие приципы разработки ПО:
Ну что ж, давайте начнем!
1. SOLID
SOLID представляет собой набор практических рекомендаций и принципов, направленных на создание гибкого, расширяемого и устойчивого кода, способного эффективно адаптироваться к изменениям и удовлетворять постоянно меняющимся требованиям бизнеса и пользователей. К принципам SOLID относятся:
S (Single Responsibility Principle) – принцип единственной ответственности;
O (Open/Сlosed Principle) – принцип открытости‑закрытости;
L (Liskov Substitution Principle) – принцип подстановки Барбары Лисков;
I (Interface Segregation Principle) – принцип разделения интерфейса;
D (Dependency Inversion Principle) – принцип инверсии зависимостей.
Рассмотри каждый из них более подробно.
S – Single Responsibility Principle (Принцип единственной ответственности)
Каждый класс или модуль должен иметь только одну причину для изменения. Это означает, что класс должен выполнять только одну конкретную функцию или задачу, что облегчает его понимание, тестирование и поддержку.
Цели применения принципа единственной ответственности приведены в таблице ниже:
Название |
Описание |
---|---|
Упрощенная сопровождаемость |
Если класс выполняет одну функцию, изменения в ней не затрагивают другие части системы |
Уменьшение сложности |
Классы с единой ответственностью проще и понятнее, что снижает сложность кода и облегчает его анализ |
Повышение гибкости |
Изменения класса с единой ответственностью предсказуемы и не влияют на другие части системы |
Повторное использование |
Классы с четко определенной ответственностью подходят для повторного использования в других системах |
Легкость в тестировании |
Когда класс отвечает только за одну функциональность, его тестирование может быть более изолированным и понятным |
Читаемость и воспринимаемость |
Классы с узкой ответственностью более понятны, так как они представляют собой логически независимые модули |
Таким образом, основная идея принципа S состоит в том, что каждый класс должен иметь только одну зону ответственности.

Рассмотрим пример реализации принципа S при разработке архитектуры системы.
Допустим, имеется класс Biathlete
, который содержит в себе методы для описания тренировок, соревнований и травм. Рассмотрим метод train()
, содержащий описание тренировки спортсмена. Пусть стандартный функционал этого метода подразумевает только спринтовые тренировки на лыжах, тогда если заказчик программно-информационной системы захочет добавить, к примеру, силовые тренировки с помощью тренажеров или же стрелковые тренировки, разработчику системы будет необходимо изменять и дописывать данный метод. Аналогично с методами compete()
и handleInjury()
, где функционал можно расширять только с помощью изменения содержимого этих методов.
Неправильная реализация
public class Biathlete {
public void train() {
// Код тренировки
}
public void compete() {
// Код соревнования
}
public void handleInjury() {
// Код обработки травмы
}
}
Таким образом, данный класс имеет сразу несколько зон ответственности, что нарушает принцип S, так как класс должен быть отвечать только за одну логическую абстракцию.
В этом случае необходимо разделить класс Biathlete
на несколько классов и, следуя принципу S, позволить каждому из них отвечать только за одно действие. Правильное разделение на классы представлено ниже, где в классе Biathlete
остался только метод train()
, а ответственности за участие спортсмена в соревнованиях и описание его травм были вынесены в отдельные классы Competitor
и InjuryHandler
. Такой подход соответствует принципу единственной ответственности и делает каждый класс более фокусированным и поддерживаемым.
Правильная реализация
public class Competitor {
public void compete() {
// код соревнования
}
}
public class InjuryHandler {
public void handleInjury() {
// код обработки травмы
}
}
public class Biathlete {
public void train() {
// код тренировки
}
}
O — Open-Closed Principle (Принцип открытости-закрытости)
Данный принцип гласит, что программные сущности, такие как классы, модули и функции, должны быть открыты для расширения и закрыты для модификации. Другими словами, когда требуется изменить поведение системы, это должно происходить без изменения кода существующих компонентов, путем добавления новых компонентов или расширения существующих.
Цели применения этого принципа приведены в таблице ниже:
Название |
Описание |
Сопровождаемость |
Добавление функциональности без изменения старого кода снижает риск ошибок и способствует минимальным изменениям |
Расширяемость |
Принцип позволяет добавлять новые функции, не затрагивая существующий функционал, что упрощает разработку |
Устойчивость к изменениям |
Если при изменениях приходится править старый код, растёт риск ошибок. Принцип помогает избежать этого |
Повторное использование |
Существующие компоненты можно повторно использовать без изменений, что способствует уменьшению дублирования кода |
Таким образом, основная идея принципа O состоит в том, что классы должны иметь возможность для дальнейшего расширения, сохраняя при этом недоступность для модификации.

Допустим, у нас есть класс BiathleteTraining
, содержащий метод train()
, в котором описываются тренировки в зависимости от возрастной категории спортсменов (юниоры, взрослые). Возрастные категории определяются в перечислении Category
, а в классе Biathlete
задаются характеристики спортсмена-биатлониста с помощью полей этого класса. Такая реализация нарушает принцип открытости/закрытости, так как при добавлении новой возрастной категории в перечисление Category
потребуется изменять код метода train()
.
Неправильная реализация
public class BiathleteTraining {
public void train(Biathlete biathlete) {
if (biathlete.getCategory() == Category.JUNIOR) {
// Тренировки для юниоров
} else if (biathlete.getCategory() == Category.ADULT) {
// Тренировки для взрослых
}
}
}
public class Biathlete extends User {
// Наследуется от класса User
private long id;
private FullName fio;
private LocalDate date;
private Gender gender;
private String email;
private String login;
private String password;
// Свойства класса Biathlete
private Category category;
private Rank rank;
private Role status;
// Геттеры и сеттеры
}
public enum Category {
JUNIOR, ADULT
}
Продемонстрируем исправленный вариант реализации класса BiathleteTraining
с соблюдением принципа открытости/закрытости, для этого создадим интерфейс TrainingStrategy
, который представляет стратегию тренировки для различных возрастных категорий биатлонистов. Далее создадим классы JuniorTrainingStrategy
и AdultTrainingStrategy
, которые реализуют этот интерфейс для соответствующих категорий. Класс BiathleteTraining
теперь получает стратегию тренировки через конструктор и вызывает метод train()
для выбранной стратегии. Это позволяет добавлять новые возрастные категории спортсменов-биатлонистов и соответствующие стратегии тренировок без изменения существующего кода.
Правильная реализация
public interface TrainingStrategy {
void train(Biathlete biathlete);
}
public class JuniorTrainingStrategy implements TrainingStrategy {
@Override
public void train(Biathlete biathlete) {
// Тренировки для юниоров
}
}
public class AdultTrainingStrategy implements TrainingStrategy {
@Override
public void train(Biathlete biathlete) {
// Тренировки для взрослых
}
}
public class BiathleteTraining {
private final TrainingStrategy trainingStrategy;
public BiathleteTraining(TrainingStrategy trainingStrategy) {
this.trainingStrategy = trainingStrategy;
}
public void train(Biathlete biathlete) {
trainingStrategy.train(biathlete);
}
}
L — Liskov Substitution Principle (Принцип подстановки Барбары Лисков)
Данный принцип акцентирует важность того, чтобы объекты подклассов могут быть использованы в коде программы вместо объектов их базовых классов, не вызывая непредвиденных проблем или нарушений функциональности. Объекты подклассов должны быть подставляемы вместо объектов базовых классов без нарушения корректности выполнения программы.
Цели применения принципа единственной ответственности приведены в таблице ниже:
Название |
Описание |
Поддержание совместимости |
Принцип подстановки делает код гибким: новые подклассы не ломают существующую логику |
Упрощение использования наследования |
Принцип подстановки позволяет использовать подклассы вместо базового класса без изменения кода |
Избежание непредвиденных ошибок |
Если соблюдён принцип подстановки, замена базового класса на подкласс не вызывает ошибок |
Создание более гибких архитектур |
Принцип подстановки позволяет добавлять подклассы без переписывания старого кода |
Обеспечение предсказуемости |
Принцип подстановки помогает предсказать поведение кода с разными объектами и сделать систему надежнее |
Таким образом, основная идея принципа L состоит в следующем: в случае, когда тип П предстает в роли подтипа Т, допускается безопасная подмена объектов типа Т объектами типа П в программе без вреда для ее функциональности.

Допустим, имеются классы Biathlete
, JuniorBiathlete
, AdultBiathlete
, TrainingManager
. Классы JuniorBiathlete
и AdultBiathlete
являются производными от класса Biathlete
, они изменяют поведение метода train()
этого класса. Это нарушает принцип подстановки Барбары Лисков, так как мы не можем безопасно заменить базовый класс на его производные классы в методе trainBiathlete()
класса TrainingManager
.
Неправильная реализация
public class Biathlete {
public void train() {
// Тренировки для биатлонистов
}
}
public class JuniorBiathlete extends Biathlete {
@Override
public void train() {
// Тренировки для юниоров
}
}
public class AdultBiathlete extends Biathlete {
@Override
public void train() {
// Тренировки для взрослых
}
}
public class TrainingManager {
public void trainBiathlete(Biathlete biathlete) {
biathlete.train();
}
}
Правильный вариант реализации с соблюдением принципа подстановки Барбары Лисков показан ниже. Для этого был создан интерфейс Training
, который определяет метод train()
. Все классы Biathlete
, JuniorBiathlete
, AdultBiathlete
теперь реализуют данный интерфейс.
Правильная реализация
public interface Training {
void train();
}
public class Biathlete implements Training {
@Override
public void train() {
// Тренировки для биатлонистов
}
}
public class JuniorBiathlete implements Training {
@Override
public void train() {
// Тренировки для юниоров
}
}
public class AdultBiathlete implements Training {
@Override
public void train() {
// Тренировки для взрослых
}
}
public class TrainingManager {
public void trainBiathlete(Training biathlete) {
biathlete.train();
}
}
Таким образом, все классы соблюдают принцип подстановки Барбары Лисков, и мы можем безопасно заменять объекты производных классов на объекты базового класса в контексте тренировок.
I — Interface Segregation Principle (Принцип разделения интерфейсов)
Данный принцип уделяет внимание разделению больших, общих интерфейсов на более мелкие и специфичные, чтобы клиенты могли зависеть только от тех методов, которые им действительно нужны.
Цели применения принципа разделения интерфейсов приведены в таблице ниже:
Название |
Описание |
Снижение зависимостей |
Разбивая общие интерфейсы на более мелкие, можно уменьшить количество методов, от которых зависят клиенты. Это позволяет уменьшить связность кода и упростить его сопровождение |
Повышение гибкости |
Принцип разделения интерфейсов позволяет сделать архитектуру гибкой — клиенты зависят только от нужных им частей, что позволяет менять или добавлять функции без влияния на других |
Улучшение читаемости и воспринимаемости |
Код становится более понятным и читаемым, так как он описывает только те взаимодействия, которые действительно имеют значение для клиента |
Повторное использование |
Малые и узкоспециализированные интерфейсы облегчают повторное использование компонентов в системе |
Таким образом, основная идея принципа I состоит в следующем: необходимо избегать ситуации, при которой клиент привязывается к методам, не предъявляемым им к использованию.

Допустим у нас есть интерфейс Biathlete
, содержащий методы, как для тренировок, так и для соревнований. Так же есть классы JuniorBiathlete
и AdultBiathlete
, которые реализуют интерфейс Biathlete
, но могут иметь разное поведение в методах для тренировок и соревнований.
Это нарушает принцип разделения интерфейсов, так как классы вынуждены реализовывать методы, которые им не нужны.
Неправильная реализация
public interface Biathlete {
void train();
void compete();
}
public class JuniorBiathlete implements Biathlete {
@Override
public void train() {
// Тренировка для юниоров
}
@Override
public void compete() {
// Участие в соревнованиях юниоров
}
}
public class AdultBiathlete implements Biathlete {
@Override
public void train() {
// Тренировка для взрослых
}
@Override
public void compete() {
// Участие в соревнованиях взрослых
}
}
Разобьем интерфейс Biathlete
на два интерфейса Trainable
и Competitor
, каждый из которых предоставляет только соответствующие методы. Классы JuniorBiathlete
и AdultBiathlete
реализуют эти интерфейсы в соответствии с их функциональностью. Такое разделение интерфейсов позволяет классам иметь только те методы, которые им действительно нужны, и при этом соблюдать принцип разделения интерфейсов, что способствует более чистой и гибкой архитектуре программно-информационной системы.
Правильная реализация
public interface Trainable {
void train();
}
public interface Competitor {
void compete();
}
public class JuniorBiathlete implements Trainable, Competitor {
@Override
public void train() {
// Тренировка для юниоров
}
@Override
public void compete() {
// Участие в соревнованиях юниоров
}
}
public class AdultBiathlete implements Training, Competitor {
@Override
public void train() {
// Тренировка для взрослых
}
@Override
public void compete() {
// Участие в соревнованиях взрослых
}
}
D — Dependency Inversion Principle (Принцип инверсии зависимостей)
Данный принцип заключается в том, что зависимости между модулями и компонентами программы должны быть организованы таким образом, чтобы более низкоуровневые компоненты зависели от абстракций, а не наоборот.
Цели применения принципа инверсий приведены в таблице ниже:
Название |
Описание |
Снижение связности |
Высокоуровневые компоненты (абстракции) не зависят напрямую от низкоуровневых деталей, что позволяет легче менять детали без воздействия на абстракции |
Гибкость и масштабируемость |
Принцип позволяет создавать более гибкие и масштабируемые архитектуры, так как изменение деталей в низкоуровневых компонентах не должно приводить к изменению высокоуровневых компонентов |
Обратимость зависимостей |
Инвертирование зависимостей обеспечивает возможность повторного использования компонентов и модулей в разных контекстах, так как высокоуровневые модули могут зависеть от одних и тех же абстракций в разных сценариях |
Тестирование |
Принцип позволяет легче тестировать компоненты, так как высокоуровневые компоненты могут быть заменены на имитации для тестирования низкоуровневых деталей |
Расширяемость |
Принцип инверсии зависимостей позволяет добавлять новые детали или компоненты без изменения высокоуровневых абстракций |
Таким образом, основная идея принципа D состоит в следующем: модули верхнего уровня не должны зависеть от модулей нижнего уровня; модули верхнего и нижнего уровней должны зависеть от абстракций, которые, в свою очередь, не должны зависеть от деталей; детали должны зависеть от абстракций.

Допустим, у нас есть класс TrainingManager
, который прямо зависит от конкретной реализации класса Biathlete
, содержащего метод train()
. Это нарушает принцип инверсии зависимостей, так как высокоуровневый модуль (TrainingManager
) зависит от низкоуровневого модуля (Biathlete
), что делает систему менее гибкой и труднорасширяемой.
Неправильная реализация
public class Biathlete {
public void train() {
// Тренировки биатлонистов
}
}
public class TrainingManager {
private final Biathlete biathlete;
public TrainingManager() {
this.biathlete = new Biathlete();
}
public void trainBiathlete() {
biathlete.train();
}
}
Продемонстрируем исправленный вариант с соблюдением принципа инверсии зависимостей, для этого создадим интерфейс Training
, который определяет метод train()
. Класс TrainingManager
теперь принимает зависимость Training
через конструктор, что соответствует принципу инверсии зависимостей.
Правильная реализация
public interface Training {
void train();
}
public class Biathlete implements Training {
@Override
public void train() {
// Тренировки биатлонистов
}
}
public class TrainingManager {
private final Training training;
public TrainingManager(Training training) {
this.training = training;
}
public void trainBiathlete() {
training.Train();
}
}
Таким образом, мы рассмотрели все пять принципов SOLID, которые составляют важный фундамент объектно-ориентированного программирования и проектирования. В сочетании эти принципы позволяют разработчикам создавать программные системы, которые легче сопровождать, расширять и адаптировать к изменениям требований.
2. DRY — Don't repeat yourself ("Не повторяйся")
«Каждая единица знания должна иметь единственное, непротиворечивое и авторитетное представление в рамках системы» («Every piece of knowledge must have a single, unambiguous, authoritative representation within a system»)

Допустим, у нас есть класс Biathlete
, который реализует разные виды тренировок. В каждом методе повторяется логика стрельбы. Это нарушает принцип DRY.
Неправильная реализация
public class Biathlete {
public void trainShooting() {
System.out.println("Занять позицию");
System.out.println("Прицелиться");
System.out.println("Сделать 5 выстрелов");
}
public void trainCombined() {
System.out.println("Пробежать 5 км");
// Дублирование
System.out.println("Занять позицию");
System.out.println("Прицелиться");
System.out.println("Сделать 5 выстрелов");
}
public void trainInterval() {
System.out.println("Спринт 1 км");
// Дублирование
System.out.println("Занять позицию");
System.out.println("Прицелиться");
System.out.println("Сделать 5 выстрелов");
}
}
Исправим реализацию, для этого вынесем код стрельбы (повторяющийся код) в отдельный метод shoot()
. После этого будем вызывать shoot()
в методах trainShooting()
, trainCombined()
, trainInterval()
, тем самым избегая дублирование одного и того же функционала.
Правильная реализация
public class Biathlete {
private void shoot() {
System.out.println("Занять позицию");
System.out.println("Прицелиться");
System.out.println("Сделать 5 выстрелов");
}
public void trainShooting() {
shoot();
}
public void trainCombined() {
System.out.println("Пробежать 5 км");
shoot();
}
public void trainInterval() {
System.out.println("Спринт 1 км");
shoot();
}
}
Так, повторяющийся код (shoot()
) вынесен в отдельный метод, что соответствует принципу DRY и упрощает сопровождение и масштабирование.
3. KISS — Keep it simple, stupid («Делай проще, тупица»)
Принцип KISS подразумевает, что программное решение должно быть максимально простым и понятным.

Допустим, мы пишем класс для подсчёта результатов стрельбы. Плохая версия делает слишком много проверок и использует сложную логику.
Неправильная реализация
public class OvercomplicatedShootingScore {
public int calculateScore(boolean[] hits, int seriesNumber, int windSpeed, boolean isStanding) {
int score = 0;
double windFactor = 1.0;
// Учёт ветра (избыточная сложность для примера)
if (windSpeed > 5) {
windFactor = 0.9;
} else if (windSpeed > 10) {
windFactor = 0.8;
} else if (windSpeed > 15) {
windFactor = 0.7;
}
// Учёт положения (стоя/лёжа)
if (isStanding) {
windFactor *= 0.95;
}
// Подсчёт попаданий с "корректировками"
for (boolean hit : hits) {
if (hit) {
score += 10 * windFactor;
} else {
score -= 5 * (1.0 / seriesNumber);
}
}
// Дополнительная "награда" за серию
if (seriesNumber > 3) {
score += 2 * seriesNumber;
}
return (int) Math.round(score);
}
}
Проблемы:
Слишком много параметров (
windSpeed
,isStanding
,seriesNumber
), хотя в биатлоне обычно считается просто «попал/не попал».Неочевидные формулы (зачем
windFactor * 0.95
для стойки?).Трудно тестировать — много зависимостей.
Нарушает KISS — реально в биатлоне штрафуют просто за промахи, без таких сложных расчётов.
Упростим систему подсчёта: в биатлоне промах = штрафной круг (или +1 минута), а попадание = 0 штрафа.
Правильная реализация
public class SimpleShootingScore {
/**
* Считает штрафные круги по результатам стрельбы.
* @param hits Массив попаданий (true = попадание, false = промах).
* @return Количество штрафных кругов.
*/
public int calculatePenalty(boolean[] hits) {
int misses = 0;
for (boolean hit : hits) {
if (!hit) misses++;
}
return misses; // 1 промах = 1 штрафной круг
}
}
4. YAGNI — You Ain't Gonna Need It ("Вам это не понадобится")
Это принцип разработки, согласно которому не стоит реализовывать функциональность, которая возможно пригодится в будущем, но не требуется сейчас. Он помогает избегать лишнего кода и сосредоточиться на актуальных задачах

Допустим у нас есть класс Biathlete
, который содержит поля name, age
, skiBrand
, rifleModel
и методы shoot()
, ski()
, cleanRifle()
, adjustSkiBindings()
. Но данный функционал сильно перегружен, поскольку вероятнее всего поля skiBrand
и rifleModel
, а также методы cleanRifle()
и adjustSkiBindings()
могут и не понадобиться при дальнейшей разработке.
Неправильная реализация
// Биатлонист с кучей ненужных методов
class Biathlete {
private String name;
private int age;
private String skiBrand; // Пока не нужно
private String rifleModel; // Пока не нужно
void shoot() { /* ... */ }
void ski() { /* ... */ }
void cleanRifle() {} // Не используется
void adjustSkiBindings() {} // Не используется
}
Проблемы:
Содержит неиспользуемые поля (информация о снаряжении)
Реализованы методы без текущей необходимости
Усложняет поддержку из-за избыточного кода
Здесь нарушается принцип YAGNI, поскольку добавлен лишний функционал, который на данный момент не обязателен. Доработаем код, убрав лишний функционал, оставив лишь основной.
Правильная реализация
// Только основная функциональность
class Biathlete {
private String name;
private int age;
void shoot() {
System.out.println("Выстрел");
}
void ski() {
System.out.println("Лыжный ход");
}
}
5. APO — Avoid Premature Optimization ("Избегайте преждевременной оптимизации")
Это принцип разработки, согласно которому не стоит оптимизировать код до тех пор, пока в этом действительно не возникнет необходимость. Ранняя оптимизация усложняет систему и может тратить ресурсы на нерешающие задачи.

Допустим, есть класс Biathlete
, который уже на начальном этапе разработки включает в себя кэширование, логирование и оптимизацию. Однако в этом нет необходимости, поскольку неизвестно, понадобятся ли эти механизмы в дальнейшем — как в процессе разработки, так и в финальном продукте для клиента.
Неправильная реализация
public class Biathlete {
private String name;
private int age;
private StringBuilder trainingLog = new StringBuilder();
public Biathlete(String name, int age) {
this.name = name;
this.age = age;
}
public void train() {
// Сложная логика с кэшированием и оптимизацией строк
trainingLog.append("Тренировка началась\n");
trainingLog.append("Выполняются упражнения\n");
trainingLog.append("Тренировка завершена\n");
System.out.println(trainingLog.toString());
}
public void compete() {
// Аналогичная сложная оптимизация
trainingLog.append("Соревнование началось\n");
trainingLog.append("Участие в гонке\n");
trainingLog.append("Соревнование завершено\n");
System.out.println(trainingLog.toString());
}
}
Здесь нарушается принцип APO — оптимизация сделана заранее, усложняя код без реальной необходимости. Уберем излишние оптимизации, так, исправленный код будет представлен ниже.
Правильная реализация
public class Biathlete {
private String name;
private int age;
public Biathlete(String name, int age) {
this.name = name;
this.age = age;
}
public void train() {
System.out.println(name + " тренируется");
}
public void compete() {
System.out.println(name + " участвует в соревнованиях");
}
}
6. BDUF — Big Design Up Front ("Глобальное проектирование прежде всего")
Это принцип разработки, согласно которому вся архитектура и детали системы тщательно продумываются до начала разработки. Принцип предупреждает, что чрезмерное планирование на ранних этапах может привести к ненужной сложности и плохой адаптации к изменениям.

Допустим у нас есть класса CompetitionSystem
, архитектура которого разрабатывается заранее и в деталях, задолго до начала реального программирования. В нем учитываются все возможные сценарии и роли: спортсмены, судьи, репортёры, фанаты, медики, трансляции, статистика, VIP-зона — и всё это внедрено в одну монолитную систему, что уже нарушает множество приницпов разработки, включая BDUF.
Неправильная реализация
// Слишком раннее и избыточное проектирование
public class CompetitionSystem {
private List<Athlete> athletes;
private List<Judge> judges;
private List<Reporter> reporters;
private List<Fan> fans;
private List<MedicalTeam> medics;
private LiveStream stream;
private StatisticSystem stats;
private VIPLounge vipLounge;
public void startCompetition() {
authenticateAllUsers();
initMedicalProtocols();
checkVIPAccess();
launchLiveStream();
notifyPress();
beginRace();
}
private void authenticateAllUsers() {
// Сложная логика аутентификации
}
private void initMedicalProtocols() {
// Подключение медперсонала
}
private void checkVIPAccess() {
// Работа с VIP-гостями
}
private void launchLiveStream() {
// Старт видеотрансляции
}
private void notifyPress() {
// Информирование репортёров
}
private void beginRace() {
// Наконец-то гонка начинается
}
}
Проблемы:
Система перегружена ненужным на первом этапе функционалом.
Проблемы при изменении требований (например, если VIP-доступ или видеотрансляции больше не нужны).
Нет ценности на ранней стадии — гонка не запускается без всей этой обвязки.
Трудно тестировать, трудно развивать.
Исправленная реализация представлена ниже:
Правильная реализация
// Первая рабочая версия — минимально необходимый функционал
public class CompetitionSystem {
private List<Athlete> athletes;
public CompetitionSystem(List<Athlete> athletes) {
this.athletes = athletes;
}
public void startRace() {
System.out.println("Гонка началась!");
for (Athlete athlete : athletes) {
athlete.run();
}
}
}
public class Athlete {
private String name;
public Athlete(String name) {
this.name = name;
}
public void run() {
System.out.println(name + " бежит дистанцию");
}
}
Не нужно проектировать "всю вселенную" заранее. Начать лучше с простого и жизнеспособного ядра, развивая систему по мере появления требований.
7. Бритва О́ккама ("Не следует множить сущее без необходимости")
Это принцип разработки, который гласит: «Не следует множить сущности без необходимости» (или «Среди конкурирующих гипотез следует выбирать самую простую, если нет веских доказательств обратного»).
Проще — лучше: выбирай самое простое решение, если нет веских причин делать сложнее. Не создавай лишних сущностей, классов, абстракций, переменных, уровней логики, если без них можно обойтись.

Допустим у нас есть два интерфейса (Trainable
, Competitor
) с единственным методом в каждом из них, хотя в проекте имеется лишь одна реализация — класс Biathlete
, что делает абстракции избыточными. Дополнительно, классы TrainingService
и CompetitionService
выполняют минимальную логику и используются только внутри Biathlete
, не предоставляя самостоятельной ценности. Всё это усложняет архитектуру без объективной необходимости и не даёт преимуществ в расширяемости или повторном использовании, что нарушает принцип бритвы Оккама
Неправильная реализация
public interface Trainable {
void executeTrainingRoutine();
}
public interface Competitor {
void executeCompetitionRoutine();
}
public class TrainingService {
public void prepare() {
System.out.println("Подготовка к тренировке");
}
}
public class CompetitionService {
public void prepare() {
System.out.println("Подготовка к соревнованию");
}
}
public class Biathlete implements Trainable, Competitor {
private TrainingService trainingService = new TrainingService();
private CompetitionService competitionService = new CompetitionService();
@Override
public void executeTrainingRoutine() {
trainingService.prepare();
System.out.println("Биатлонист тренируется");
}
@Override
public void executeCompetitionRoutine() {
competitionService.prepare();
System.out.println("Биатлонист участвует в соревнованиях");
}
}
Проблемы:
Слишком много сущностей: интерфейсы, сервисы, лишние уровни абстракции.
Всё это — без необходимости, ведь логика простая.
Злоупотребление ООП и «архитектурностью» ради самой архитектуры.
Исправленная реализация представлена ниже:
Правильная реализация
public class Biathlete {
private String name;
public Biathlete(String name) {
this.name = name;
}
public void train() {
System.out.println(name + " тренируется");
}
public void compete() {
System.out.println(name + " участвует в соревнованиях");
}
}
Заключение
Процесс разработки ПО подразумевает под собой придерживание различных принципов, которые для начинающего программиста первоначально могут вызывать затруднения. Но за каждым из них стоит практический смысл: облегчить поддержку, сделать код чище, понятнее и устойчивее к изменениям.
Не стоит стремиться применять все принципы сразу и везде — как и в жизни, здесь важно чувство меры. DRY не означает, что любой повтор кода — зло, а YAGNI не призывает игнорировать архитектуру. Главное — понимать «зачем», а не просто следовать правилам по списку.
Начните с простого, пробуйте, ошибайтесь, и постепенно каждое из этих загадочных слов станет вашим надёжным инструментом.
Спасибо, что прочитали мою первую работу, надеюсь, данная статья принесёт много пользы и станет хорошим стартом для погружения в мир чистого кода и эффективной разработки.
Комментарии (10)
Licemery
05.07.2025 09:41Ну в общем-то по картинкам и особенно "выЙграл" уже понятно всё становится о знаниях автора. Особенно обожаю во всём этом карго-культе алфавитного супа это требование неукоснительного соблюдения взаимоисключающих параметров. Значит, плодить сущности нельзя, но классы мы будем разбивать до гранулярности песчинок, пока их количество не разрастётся до того, что разобраться в этом будет невозможно, противореча DRY. Словно никто не слышал о перенормализации в базах данных, где тоже надо знать меру.
Описание BDUF вообще само себе противоречит, надо думать сразу обо всём заранее, но нельзя погрязать в планировании, чтобы не получилось YAGNI. Лучше забейте на оптимизацию, юзер заплатит за новое железо - это объясняет, почему вокруг столько bloatware.
Хорошо придумывать принципы, которые не тебе использовать в реальном проекте.
GrigoryevArtem Автор
05.07.2025 09:41Спасибо за замечание, но сомневаюсь, что иллюстративные примеры, которые помогут разобраться людям из других сфер IT, не связанных с разработкой, или даже орфографическая ошибка могут позволить судить о знаниях автора.
А по поводу принципов разработки: как и написано в конце статьи, все принципы необходимо использовать в меру и с умом.
«Не стоит стремиться применять все принципы сразу и везде — как и в жизни, здесь важно чувство меры. DRY не означает, что любой повтор кода — зло, а YAGNI не призывает игнорировать архитектуру. Главное — понимать, «зачем», а не просто следовать правилам по списку».
whoisking
05.07.2025 09:41Боже, как же уже надоело читать про этот солид, просто дичайший карго-культ без какого-либо углубления в суть, запудривание мозгов себе и окружающим, просто рак мира разработки...
Zloten
05.07.2025 09:41Эх, вам надоело. А представьте какого поддерживать такой (SOLID-based) 'Академический код' когда всё расщеплено буквально на атомы и всё дерево сущностей и их взаимодействий не помещается в ОЗУ головного мозга (ну, кроме мозга автора). Вообще, я пришёл к выводу, что человека, который пишет грязный/неудобный/неоптимальный код проще научить писать лучше, чем донести мысль о здравом! использовании SOLID его фанату.
Полагаю есть три типа:
Интуитивщики - применяющие данные принципы исходя из многолетнего опыта наломанных дров;
Академики - ну это про них: ООП-(SOLID) головного мозга;
-
Адекваты - воспринимающие все эти 'правила/принципы' как рекомендации.
Первым нехватает систематизации (упорядоченных знаний). Вторые, в основном, зубрилки (шаблонные програмисты) без творческой жилки, либо фанатики. Третьи рулят, ну или страдают (как я) в компании первых двух :)
apevzner
05.07.2025 09:41Проблема в том, что все эти прекрасные принцыпы - это мысли зрелых разработчиков, адресованные зрелым разработчикам.
Вот взять, к примеру, програмку, которая делает что-нибудь полезное по HTTP (и вовсе не обязательно это веб-бровсер). Где тут проводить границы single responsibility?
Вот я бы, наверное, провёл их по линии, очерченной соответствующим набором RFC, вынеся MIME и IDA в отдельные сущности, по границам, очерченным тамошними RFC. Но можно ведь всё в одну сущность затолкать, а можно и наоборот, помельче разрезать. Какое тут правильное решение? На этот вопрос нет простого однозначного ответа.
Или, например, идея распилить класс
Biathlete
на три, каждый из которых имеет свою реализацию методаtrain()
. Нередко это приводит к тому, что класс, который так бы уместился в 200 простых и понятных строк превращается в иерархию классов, общим объёмом строк на тысячу, которые рано или поздно обрастают дублирующимся кодом и которые надо все параллельно поддерживать.А так ли уж по сути отличаются тренировки для юниоров, зрелых и престарелых? Может, они в целом примерно одинаковые, но отличаются набором параметров? А что, если "думательную" часть класса сделать общую, и кормить ее табличкой с параметрами, которые специализируют её под контретное применение?
Ну и т.д. и т.п.
И, к сожалению, все эти прекрасные принципы, примененные без соответствующего опыта, пораждают, скорее, не хороший дизайн, а некоторый карго-культ...
danilovmy
Я правильно понял, то что иллюстрации на бритву оккама нет - это и есть иллюстрация?
@GrigoryevArtem спасибо, иллюстрации прикольные: биатлонист в валенках сделал мой день!
GrigoryevArtem Автор
Спасибо, вероятно не прогрузилось изображение, иллюстрация на пример бритвы Оккама "с влиянием гороскопа" присутствует в статье)