🧹 Принципы чистого кода

DRY, KISS, YAGNI, Composition over Inheritance, Law of Demeter

Принцип 1
DRY — Don't Repeat Yourself
Не повторяйся

Суть: каждый кусок знания (логика, правило, формула) должен существовать в системе в единственном представлении. Если одну и ту же логику приходится менять в нескольких местах — это нарушение DRY.

Проблема:

Формула расчёта скидки продублирована в двух методах. Если правило скидки изменится, легко забыть поправить одно из мест — и получить рассинхронизацию.

❌ Плохо

class OrderService {
    double calculateWebPrice(Order order) {
        double price = order.getBasePrice();
        if (order.getQuantity() > 10) {
            price = price * 0.9; // скидка 10%
        }
        return price;
    }

    double calculateInvoicePrice(Order order) {
        double price = order.getBasePrice();
        if (order.getQuantity() > 10) {
            price = price * 0.9; // та же логика скопирована
        }
        return price;
    }
}
                        
✅ Хорошо

class OrderService {
    double calculateWebPrice(Order order) {
        return applyBulkDiscount(order);
    }

    double calculateInvoicePrice(Order order) {
        return applyBulkDiscount(order);
    }

    private double applyBulkDiscount(Order order) {
        double price = order.getBasePrice();
        if (order.getQuantity() > 10) {
            price = price * 0.9;
        }
        return price;
    }
}
                        
💡 Важно: DRY — это про дублирование знания, а не про дублирование текста. Два одинаковых на вид куска кода, которые меняются по разным причинам — это не нарушение DRY, и объединять их в один метод не нужно.
Принцип 2
KISS — Keep It Simple, Stupid
Делай проще

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

Проблема:

Простая проверка чётности числа реализована через громоздкую конструкцию с побитовыми операциями и тернарными выражениями — вместо очевидного оператора остатка от деления.

❌ Плохо

boolean isEven(int number) {
    return ((number & 1) == 0)
        ? true
        : (number % 2 == 0)
            ? true
            : false;
}
                        
✅ Хорошо

boolean isEven(int number) {
    return number % 2 == 0;
}
                        
💡 Важно: простота — это не примитивность. KISS не запрещает паттерны проектирования или абстракции, если они реально снижают сложность, а не добавляют её ради "гибкости на будущее".
Принцип 3
YAGNI — You Aren't Gonna Need It
Тебе это не понадобится

Суть: не добавляй функциональность, пока она реально не понадобится. Код "про запас" увеличивает сложность и стоимость поддержки, а часто вообще никогда не используется.

Проблема:

Сервис уведомлений с самого начала поддерживает Email, SMS, push и "универсальный" механизм плагинов для будущих каналов — хотя на практике используется только Email.

❌ Плохо

interface NotificationChannel {
    void send(String message);
}

class NotificationService {
    private List<NotificationChannel> channels = new ArrayList<>();
    private Map<String, Object> pluginRegistry = new HashMap<>();

    void registerPlugin(String name, Object plugin) {
        // "на будущее", никто пока не использует
        pluginRegistry.put(name, plugin);
    }

    void notifyUser(String message) {
        for (NotificationChannel channel : channels) {
            channel.send(message);
        }
    }
}
                        
✅ Хорошо

class NotificationService {
    void notifyUser(String email, String message) {
        sendEmail(email, message);
    }

    private void sendEmail(String email, String message) {
        // отправка email
    }
}

// Абстракцию под несколько каналов
// добавим, когда появится вторая реальная
// потребность — не раньше
                        
💡 Важно: YAGNI не отменяет проектирование наперёд там, где риск переделки высок (например, выбор формата хранения данных). Речь про ненужный код и абстракции, добавленные "на всякий случай".
Принцип 4
Composition over Inheritance
Композиция вместо наследования

Суть: вместо жёсткой иерархии наследования лучше собирать поведение объекта из независимых компонентов, переданных ему (обычно через конструктор). Это снижает связанность и упрощает изменение поведения без переписывания иерархии классов.

Проблема:

Поведение "летать" и "крякать" зашито в базовый класс Duck через наследование. Когда появляется вид утки, который не умеет летать, приходится переопределять метод и городить исключения в иерархии.

❌ Плохо — наследование

class Duck {
    void fly() {
        System.out.println("Летит");
    }
}

class RubberDuck extends Duck {
    @Override
    void fly() {
        // резиновая утка не летает,
        // но вынуждена переопределять метод
        throw new UnsupportedOperationException();
    }
}
                        
✅ Хорошо — композиция

interface FlyBehavior {
    void fly();
}

class FlyWithWings implements FlyBehavior {
    public void fly() {
        System.out.println("Летит");
    }
}

class CannotFly implements FlyBehavior {
    public void fly() {
        System.out.println("Не летает");
    }
}

class Duck {
    private final FlyBehavior flyBehavior;

    Duck(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

    void performFly() {
        flyBehavior.fly();
    }
}

Duck rubberDuck = new Duck(new CannotFly());
Duck realDuck = new Duck(new FlyWithWings());
                        
Сравнение подходов:
Аспект ❌ Наследование ✅ Композиция
Смена поведения Требует переопределения методов или новой ветки иерархии Достаточно передать другой объект-поведение
Связанность Жёсткая (compile-time) Слабая, поведение можно менять в рантайме
Переиспользование Только через цепочку родителей Любой объект может использовать любое поведение
Тестирование Нужно поднимать всю иерархию Легко подставить mock-поведение
💡 Важно: принцип не отменяет наследование полностью — он про то, что наследование стоит использовать для отношения "является" (is-a), а не как способ переиспользовать код.
Принцип 5
Law of Demeter
Принцип наименьшего знания

Суть: объект должен общаться только со своими "непосредственными знакомыми" — не вызывать методы через цепочку чужих объектов (a.getB().getC().doSomething()). Такие цепочки жёстко привязывают код к внутренней структуре чужих классов.

Проблема:

Чтобы узнать город клиента, код "пролезает" через три чужих объекта. Если поменяется структура Address или Wallet — сломается код, который вообще не должен был знать об их существовании.

❌ Плохо — цепочка вызовов

class OrderProcessor {
    void printCity(Customer customer) {
        String city = customer
            .getAddress()
            .getCity()
            .getName();
        System.out.println(city);
    }
}
                        
✅ Хорошо — делегирование

class Customer {
    private Address address;

    String getCityName() {
        return address.getCityName();
    }
}

class Address {
    private City city;

    String getCityName() {
        return city.getName();
    }
}

class OrderProcessor {
    void printCity(Customer customer) {
        System.out.println(customer.getCityName());
    }
}
                        
💡 Важно: формально принцип разрешает вызывать методы: у самого объекта, у его полей, у объектов, переданных в параметрах метода, и у объектов, созданных внутри метода. Всё остальное — "чужая земля", в которую лучше не заходить напрямую.