Оглавление

Введение
1. Классы и Объекты
2. Инкапсуляция
3. Наследование
4. Полиморфизм
5. Абстракция
Проблемы ООП
Заключение

Введение

Привет! Эта статья написана для улучшения понимания принципов объектно-ориентированного программирования для начинающих автоматизаторов. Эта тема является важной и часто её спрашивают на собеседованиях. Понимание ООП поможет тебе писать более структурированный, поддерживаемый и переиспользуемый код.

Когда я впервые столкнулся с ООП, меня встретили сухие определения: "инкапсуляция", "наследование", "полиморфизм". Звучало сложно и оторвано от реал��ности. Но на самом деле ООП — это способ мышления, который отражает то, как мы воспринимаем мир вокруг себя.

Фруктовый магазин - введение в ООП
Фруктовый магазин - введение в ООП

Представьте, что вы заходите в фруктовый магазин. Вы видите яблоки, бананы, апельсины. Каждый фрукт:

  • Имеет свои свойства (цвет, вес, спелость)

  • Имеет поведение (может созревать, может портиться)

  • Относится к какой-то категории (цитрусовые, косточковые)

Именно так работает ООП! Давайте разберёмся на конкретных примерах.

1. Классы и Объекты: Чертёж и реальная вещь

Теория:
Класс — это чертёж, описание того, какими свойствами и поведением будет обладать объект. Объект — это конкретный экземпляр, созданный по этому чертежу.

Главная мысль: класс — это описание, объект — это конкретный экземпляр с собственным состоянием.

Класс и объект - чертёж и автомобиль
Класс и объект - чертёж и автомобиль

Жизненный пример:
Представьте, что у вас есть чертёж "Автомобиль" с описанием: 4 колеса, руль, двигатель, возможность ехать и сигналить. Это класс. А конкретная машина во дворе — красная Toyota с номером А123ВС — это объект, созданный по этому чертежу.

Термин:
Процесс создания объекта из класса называется инстанцированием (instantiation).
Когда мы пишем new Car("Toyota", "красный", "А123ВС"), — мы инстанцируем класс Car, то есть создаём конкретный экземпляр на его основе.

Код на Java:

// Класс - чертёж для автомобиля
class Car {
    // Состояние (поля) - что автомобиль "имеет"
    private String brand;
    private String color;
    private String licensePlate;
    private boolean isEngineOn;
    private int currentSpeed;
    
    // Конструктор - специальный метод для создания объектов
    public Car(String brand, String color, String licensePlate) {
        this.brand = brand;
        this.color = color;
        this.licensePlate = licensePlate;
        this.isEngineOn = false;
        this.currentSpeed = 0;
    }
    
    // Поведение (методы) - что автомобиль "умеет делать"
    public void startEngine() {
        this.isEngineOn = true;
        System.out.println("Двигатель автомобиля " + brand + " завёлся!");
    }
    
    public void stopEngine() {
        this.isEngineOn = false;
        this.currentSpeed = 0;
        System.out.println("Двигатель автомобиля " + brand + " заглушен");
    }
    
    public void accelerate(int speed) {
        if (isEngineOn) {
            this.currentSpeed += speed;
            System.out.println(brand + " разгоняется до " + currentSpeed + " км/ч");
        } else {
            System.out.println("Сначала заведите двигатель!");
        }
    }
    
    public void honk() {
        System.out.println("Автомобиль " + brand + " сигналит: Би-бип!");
    }
    
    // Геттеры - чтобы узнать состояние
    public String getBrand() {
        return brand;
    }
    
    public String getColor() {
        return color;
    }
    
    public int getCurrentSpeed() {
        return currentSpeed;
    }
    
    public boolean isEngineRunning() {
        return isEngineOn;
    }
}

// Использование в программе
public class CarDemo {
    public static void main(String[] args) {
        // Создаём ОБЪЕКТЫ по ЧЕРТЕЖУ Car
        Car toyota = new Car("Toyota", "красный", "А123ВС");
        Car bmw = new Car("BMW", "чёрный", "В777ХХ");
        
        // Вызываем поведение объектов
        toyota.startEngine();    // Заводим Toyota
        toyota.accelerate(50);   // Разгоняемся
        toyota.honk();           // Сигналим
        
        System.out.println("Текущая скорость Toyota: " + toyota.getCurrentSpeed() + " км/ч");
        
        // BMW пока не заведён
        System.out.println("Двигатель BMW заведён: " + bmw.isEngineRunning());
        bmw.accelerate(30); // Не сработает - двигатель заглушен
    }
}

Что происходит в коде:

  • Car - это класс (чертёж)

  • toyota и bmw - это объекты (конкретные машины)

  • Каждый объект имеет своё состояние: разный цвет, разные номера, разная скорость

  • Поведение (методы) одинаковое для всех объектов, но результат зависит от их состояния

2. Инкапсуляция: Контроль доступа к состоянию

Теория:
Инкапсуляция — это принцип, согласно которому внутреннее состояние объекта защищено от прямого доступа извне. Взаимодействие происходит только через публичные методы.
Коротко
Инкапсуляция — сокрытие реализации.

Инкапсуляция - банкомат
Инкапсуляция - банкомат

Жизненный пример:
Представьте банкомат. У него есть внутреннее состояние: количество денег, журнал операций. Но вы не можете просто открыть его и взять деньги. Вы взаимодействуете через интерфейс: вставляете карту, вводите PIN, выбираете операцию. Банкомат сам проверяет корректность запроса и выдает деньги, если всё правильно.

Реализация в Java:

  • Модификаторы доступа: private, protected, public для контроля видимости полей и методов

  • Геттеры: методы для безопасного чтения приватных полей

  • Сеттеры: методы для безопасного изменения полей с валидацией

  • Конструкторы: обеспечение корректного начального состояния объекта

Код на Java:

class BankAccount {
    // private - значит, напрямую извне к этим полям обратиться нельзя
    private String ownerName;
    private double balance;
    private String accountNumber;
    
    public BankAccount(String ownerName, double initialBalance, String accountNumber) {
        this.ownerName = ownerName;
        this.balance = initialBalance;
        this.accountNumber = accountNumber;
    }
    
    // Публичные методы - наш "интерфейс" для взаи��одействия
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Пополнение на " + amount + ". Новый баланс: " + balance);
        } else {
            System.out.println("Сумма пополнения должна быть положительной");
        }
    }
    
    public boolean withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Снято " + amount + ". Остаток: " + balance);
            return true;
        } else {
            System.out.println("Недостаточно средств или неверная сумма");
            return false;
        }
    }
    
    // Геттеры - только для чтения информации
    public double getBalance() {
        return balance;
    }
    
    public String getOwnerName() {
        return ownerName;
    }
}

// Использование
public class BankDemo {
    public static void main(String[] args) {
        BankAccount account = new BankAccount("Иван Иванов", 1000.0, "123456789");
        
        // Так нельзя! Будет ошибка компиляции:
        // account.balance = 1000000; // ОШИБКА! Поле private
        
        // А так можно - через контролируемые методы
        account.deposit(500.0);    // Пополняем
        account.withdraw(200.0);   // Снимаем
        account.withdraw(2000.0);  // Не получится - недостаточно средств
        
        System.out.println("Текущий баланс: " + account.getBalance());
    }
}

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

3. Наследование

Теория:
Наследование позволяет создавать новый класс на основе существующего, перенимая его свойства и поведение, и добавляя что-то новое.
Коротко
Наследование — создание нов��го на основе существующего.

Наследование - семейное дерево транспорта
Наследование - семейное дерево транспорта

Жизненный пример:
Представьте транспортные средства. Есть общее понятие "Транспорт" - он может ехать и останавливаться. "Автомобиль" является транспортом, но добавляет возможность сигналить. "Велосипед" тоже является транспортом, но добавляет возможность крутить педали. "Самолёт" является транспортом, но добавляет возможность летать.

Реализация в Java:

  • extends: создание иерархии классов (один родитель)

  • super: обращение к конструкторам и методам родительского класса

  • Наследование методов: дочерние классы получают функциональность родителя

  • Доступ к protected: наследники видят protected-поля и методы

Код на Java:

// Базовый класс - Транспорт
class Transport {
    private String name;
    
    public Transport(String name) {
        this.name = name;
    }
    
    public void move() {
        System.out.println(name + " начинает движение");
    }
    
    public void stop() {
        System.out.println(name + " остановился");
    }
}

// Класс Car наследует от Transport
class Car extends Transport {
    public Car() {
        super("Автомобиль"); // Вызываем конструктор родителя
    }
    
    // Новый метод, специфичный для автомобилей
    public void honk() {
        System.out.println("Би-бип! Сигналит автомобиль");
    }
}

// Класс Bicycle наследует от Transport
class Bicycle extends Transport {
    public Bicycle() {
        super("Велосипед");
    }
    
    // Новый метод, специфичный для велосипедов
    public void ringBell() {
        System.out.println("Дзинь-дзинь! Звонит велосипед");
    }
}

// Класс Airplane наследует от Transport
class Airplane extends Transport {
    public Airplane() {
        super("Самолёт");
    }
    
    // Новый метод, специфичный для самолётов
    public void takeOff() {
        System.out.println("Самолёт взлетает в небо");
    }
}

// Использование
public class InheritanceDemo {
    public static void main(String[] args) {
        // Создаём объекты
        Car car = new Car();
        Bicycle bike = new Bicycle();
        Airplane plane = new Airplane();
        
        // Все объекты имеют унаследованные методы
        System.out.println("=== Унаследованные методы ===");
        car.move();     // Унаследовано от Transport
        bike.move();    // Унаследовано от Transport
        plane.move();   // Унаследовано от Transport
        
        System.out.println("\n=== Уникальные методы ===");
        // Каждый класс имеет свои уникальные методы
        car.honk();     // Только у Car
        bike.ringBell(); // Только у Bicycle
        plane.takeOff(); // Только у Airplane
        
        System.out.println("\n=== Общие действия ===");
        car.stop();     // Унаследовано от Transport
        bike.stop();    // Унаследовано от Transport
        plane.stop();   // Унаследовано от Transport
    }
}

Что демонстрирует этот пример наследования:

  1. Наследование структуры — все дочерние классы технически содержат поле name от родителя Transport, но не имеют к нему прямого доступа из-за модификатора private. Взаимодействие с этим полем происходит через унаследованные публичные методы родителя.

  2. Наследование методов - все классы могут использовать move() и stop() от родителя

  3. Расширение функциональности - каждый дочерний класс добавил свои уникальные методы:

    • Car: honk() - сигналит

    • Bicycle: ringBell() - звонит в звонок

    • Airplane: takeOff() - взлетает

  4. Вызов конструктора родителя - каждый класс вызывает конструктор родителя через super(), передавая своё название

4. Полиморфизм: Одно действие - разные реализации

Теория:
Полиморфизм позволяет объектам разных классов отвечать на один и тот же вызов метода по-разному.
Коротко
Полиморфизм — одно действие, разные реализации.

Полиморфизм - один пульт для всех устройств
Полиморфизм - один пульт для всех устройств

Жизненный пример:
Представьте кнопки на пульте управления. Есть кнопка "Включить":

  • На телевизоре она включает экран

  • На кондиционере - запускает охлаждение

  • На кофеварке - начинает готовить кофе

Одна и та же команда "включить" выполняется по-разному в зависимости от устройства.

Реализация в Java:

  • Перегрузка (overloading): один класс, разные параметры (статический полиморфизм)

  • Переопределение (overriding): наследование + одинаковые сигнатуры (динамический полиморфизм)

  • Интерфейсы (interface): контракты для реализации разными классами

  • Абстрактные классы: частичная реализация + обязательные для переопределения методы

Аннотация @Override явно указывает, что мы переопределяем метод родителя —
это переопределение (overriding) является основой полиморфизма.

Код на Java:

Полиморфизм через наследование:

// Базовый класс для устройств
class Device {
    protected String name;
    
    public Device(String name) {
        this.name = name;
    }
    
    // Этот метод будет по-разному работать для каждого устройства
    public void turnOn() {
        System.out.println("Включаем " + name);
    }
}

// Конкретные устройства
class TV extends Device {
    public TV() {
        super("телевизор");
    }
    
    @Override
    public void turnOn() {
        System.out.println("Телевизор: загорается экран, показывается программа");
    }
}

class AC extends Device {
    public AC() {
        super("кондиционер");
    }
    
    @Override
    public void turnOn() {
        System.out.println("Кондиционер: запускается вентилятор, начинает дуть холодный воздух");
    }
}

class CoffeeMachine extends Device {
    public CoffeeMachine() {
        super("кофеварка");
    }
    
    @Override
    public void turnOn() {
        System.out.println("Кофеварка: начинает готовить кофе, слышно шипение");
    }
}

// Демонстрация полиморфизма
public class PolymorphismDemo {
    public static void main(String[] args) {
        // Создаём устройства
        Device tv = new TV();
        Device ac = new AC();
        Device coffee = new CoffeeMachine();
        
        // Массив устройств
        Device[] devices = {tv, ac, coffee};
        
        System.out.println("Нажимаем кнопку 'Включить' на всех устройствах:");
        
        // ПОЛИМОРФИЗМ: один метод, разное поведение!
        for (Device device : devices) {
            device.turnOn(); // Каждое устройство включается по-своему
        }
    }
}

Полиморфизм через интерфейсы:

// интерфейс вместо базового класса
interface Switchable {
    void turnOn();  // Контракт - все реализации должны иметь этот метод
}

// Конкретные устройства реализуют интерфейс
class TV implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("Телевизор: загорается экран, показывается программа");
    }
}

class AC implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("Кондиционер: запускается вентилятор, начинает дуть холодный воздух");
    }
}

class CoffeeMachine implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("Кофеварка: начинает готовить кофе, слышно шипение");
    }
}

// Демонстрация полиморфизма через интерфейсы
public class InterfacePolymorphismDemo {
    public static void main(String[] args) {
        // Создаём устройства через интерфейсный тип
        Switchable tv = new TV();
        Switchable ac = new AC();
        Switchable coffee = new CoffeeMachine();
        
        // Массив устройств
        Switchable[] devices = {tv, ac, coffee};
        
        System.out.println("Нажимаем кнопку 'Включить' на всех устройствах:");
        
        // ПОЛИМОРФИЗМ ЧЕРЕЗ ИНТЕРФЕЙСЫ: один метод, разное поведение!
        for (Switchable device : devices) {
            device.turnOn(); // Каждое устройство включается по-своему
        }
    }
}

Сравнение подходов: наследование vs интерфейсы

Критерий

Полиморфизм через наследование

Полиморфизм через интерфейсы

Связность

Жёсткая связь с родительским классом

Слабая связь, только контракт

Гибкость

Один родитель, нельзя наследовать от нескольких классов

Можно реализовать несколько интерфейсов

Переиспользование

Наследуется всё: и методы, и поля, и поведение

Реализуется только контракт, внутренняя реализация свободна

Расширяемость

Для добавления функциональности нужно менять иерархию

Новые интерфейсы добавляются без изменения существующих классов

Пример в коде

class TV extends Device

class TV implements Switchable

Когда использовать

Когда есть чёткая иерархия "is-a" и общее поведение

Когда нужна гибкость и разные объекты должны поддерживать одинаковые операции

5. Абстракция: Скрытие сложности

Теория:
Абстракция позволяет скрыть сложную реализацию и показать только необходимые детали, работая на уровне концепций, а не конкретной реализации.
Коротко
Абстракция — выделение главного, сокрытие деталей реализации.

Абстракция - водитель за рулём
Абстракция - водитель за рулём

Жизненный пример:
Когда вы ведете автомобиль, вам не нужно знать как работает двигатель, система впрыска топлива или электроника. Вы используете руль, педали и рычаги - это абстракция, скрывающая сложность механизмов под капотом.

Реализация в Java:

  • Интерфейсы (interface): определение ЧТО должно делать, без реализации КАК

  • Абстрактные классы (abstract class): общая логика + абстрактные методы для реализации

  • Абстрактные методы: методы без реализации, которые должны быть реализованы в наследниках

  • Сокрытие сложности: публичные методы скрывают внутреннюю реализацию

Код на Java:

// Абстракция - определяем ЧТО должно делать, без реализации
interface Vehicle {
    void start();       // Абстрактный метод - без реализации
    void stop();        // Абстрактный метод - без реализации
    int getMaxSpeed();  // Абстрактный метод - без реализации
}

// Конкретная реализация - скрываем сложность двигателя
class Car implements Vehicle {
    private String engineType;
    private int currentSpeed;
    
    public Car(String engineType) {
        this.engineType = engineType;
        this.currentSpeed = 0;
    }
    
    @Override
    public void start() {
        // Сложная логика запуска двигателя скрыта от пользователя
        System.out.println("Запускаем " + engineType + " двигатель");
        System.out.println("Проверяем системы...");
        System.out.println("Двигатель запущен!");
    }
    
    @Override
    public void stop() {
        // Сложная логика остановки скрыта
        System.out.println("Останавливаем двигатель...");
        this.currentSpeed = 0;
        System.out.println("Транспорт остановлен");
    }
    
    @Override
    public int getMaxSpeed() {
        return 220; // Конкретная реализация
    }
}

// Другая реализация - скрываем сложность электрической системы
class ElectricScooter implements Vehicle {
    private int batteryLevel;
    
    public ElectricScooter() {
        this.batteryLevel = 100;
    }
    
    @Override
    public void start() {
        // Сложность электрической системы скрыта
        System.out.println("Активируем батарею...");
        System.out.println("Запускаем электромотор...");
        System.out.println("Самокат готов к поездке!");
    }
    
    @Override
    public void stop() {
        // Сложность остановки электромотора скрыта
        System.out.println("Отключаем электромотор...");
        System.out.println("Самокат остановлен");
    }
    
    @Override
    public int getMaxSpeed() {
        return 25; // Конкретная реализация
    }
}

// Использование абстракции
public class AbstractionDemo {
    public static void main(String[] args) {
        // Работаем через абстракцию, не зная деталей реализации
        Vehicle car = new Car("бензиновый");
        Vehicle scooter = new ElectricScooter();
        
        System.out.println("=== Используем транспорт через абстракцию ===");
        
        // Нам не важно КАК именно запускается транспорт - важно ЧТО он запускается
        car.start();        // Сложность запуска ДВС скрыта
        scooter.start();    // Сложность запуска электромотора скрыта
        
        System.out.println("\nМаксимальная скорость:");
        System.out.println("Автомобиль: " + car.getMaxSpeed() + " км/ч");
        System.out.println("Самокат: " + scooter.getMaxSpeed() + " км/ч");
        
        System.out.println("\nОстанавливаем транспорт:");
        car.stop();         // Сложность остановки скрыта
        scooter.stop();     // Сложность остановки скрыта
    }
}

Преимущество абстракции: Мы можем работать с транспортом на высоком уровне, не зная деталей реализации. Если завтра мы добавим новый вид транспорта (например, электромобиль), код, использующий абстракцию Vehicle, не нужно будет менять - мы просто реализуем интерфейс в новом классе.

Проблемы ООП

Проблема наследования: Хрупкая базовая функциональность

Проблемы ООП - запутанная система
Проблемы ООП - запутанная система

Представьте, что мы хотим добавить всем транспортным средствам возможность рассчитывать стоимость поездки:

class Transport {
    // ... остальные методы ...
    
    public double calculateTripCost(int distance) {
        // Базовая реализация
        return 0;
    }
}

// Теперь ВСЕ классы-наследники должны переопределить этот метод
class Car extends Transport {
    @Override
    public double calculateTripCost(int distance) {
        return distance * 0.15; // 15 рублей за км
    }
}

class Bicycle extends Transport {
    @Override
    public double calculateTripCost(int distance) {
        return 0; // На велосипеде бесплатно
    }
}

class Airplane extends Transport {
    @Override
    public double calculateTripCost(int distance) {
        return distance * 5.0; // 5 рублей за км (условно)
    }
}

// И так для КАЖДОГО транспорта!

Чем больше классов-наследников, тем сложнее вносить изменения в базовый класс.

Проблема выражения: Добавление нового поведения

Допустим, мы хотим добавить операцию "помыть транспорт". В ООП-стиле нам придётся добавить метод wash() в базовый класс Transport и реализовать его во всех наследниках:

class Transport {
    public void wash() {
        // Базовая реализация - может быть не подходит для всех
    }
}

class Car extends Transport {
    @Override
    public void wash() {
        System.out.println("Моем автомобиль на автомойке");
    }
}

class Bicycle extends Transport {
    @Override
    public void wash() {
        System.out.println("Моем велосипед из шланга");
    }
}

class Airplane extends Transport {
    @Override
    public void wash() {
        System.out.println("Самолёт моет специальная бригада с подъёмниками"); // Сложная реализация!
    }
}

Опасный пример: Нарушение инкапсуляции через возвращение mutable-объектов

Вот как можно незаметно "выстрелить себе в ногу" в ООП:

class ShoppingCart {
    private List<String> items;
    
    public ShoppingCart() {
        this.items = new ArrayList<>();
    }
    
    // ОПАСНОСТЬ: возвращаем mutable-коллекцию!
    public List<String> getItems() {
        return items; // Теперь внешний код может изменить нашу внутреннюю коллекцию!
    }
    
    public void addItem(String item) {
        items.add(item);
        System.out.println("Добавлен товар: " + item);
    }
}

public class EncapsulationDangerDemo {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        cart.addItem("Книга");
        cart.addItem("Ноутбук");
        
        // Получаем "защищённую" коллекцию
        List<String> items = cart.getItems();
        
        System.out.println("Товаров в корзине: " + items.size()); // 2
        
        // НЕОЖИДАННОСТЬ: внешний код меняет внутреннее состояние!
        items.clear(); // Удалили все товары!
        items.add("Взломанный товар"); // Добавили что-то без валидации
        
        System.out.println("Товаров в корзине после 'взлома': " + items.size()); // 1
        System.out.println("Корзина сломана: " + cart.getItems()); // [Взломанный товар]
    }
}

Почему это проблема: Мы думали, что защитили данные через private, но вернули mutable-объект, который позволяет обойти все защиты.

Как исправить:

// Защищённые геттеры:
public List<String> getItems() {
    return new ArrayList<>(items); // Возвращаем копию
}

// Или лучше - возвращаем неизменяемую коллекцию:
public List<String> getItems() {
    return Collections.unmodifiableList(items);
}

// Или предоставляем только read-only операции:
public int getItemsCount() { return items.size(); }
public boolean containsItem(String item) { return items.contains(item); }

Наследование вместо композиции (нарушение LSP)

class Rectangle {
    protected int width;
    protected int height;
    
    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
    
    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    public int getArea() { return width * height; }
}

// ОПАСНОСТЬ: Square "является" Rectangle? Не всегда!
class Square extends Rectangle {
    public Square(int size) {
        super(size, size);
    }
    
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width); // Ломаем контракт Rectangle!
    }
    
    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(height); // Ломаем контракт Rectangle!
    }
}

public class LSPViolationDemo {
    static void resizeRectangle(Rectangle rectangle) {
        rectangle.setWidth(5);
        rectangle.setHeight(4);
        System.out.println("Ожидаемая площадь: 20, фактическая: " + rectangle.getArea());
    }
    
    public static void main(String[] args) {
        Rectangle rect = new Rectangle(2, 3);
        resizeRectangle(rect); // Ожидаемая площадь: 20, фактическая: 20 ✅
        
        Rectangle square = new Square(2);
        resizeRectangle(square); // Ожидаемая площадь: 20, фактическая: 16 ❌
        
        // Нарушение принципа подстановки Лисков: 
        // Square не может быть заменой Rectangle!
    }
}

Почему это проблема: Наследование должно сохранять поведение базового класса. Если наследник нарушает контракт родителя - это приводит к тонким багам.

Как исправить:

// Лучше использовать композицию:
class Square {
    private Rectangle rectangle;
    
    public Square(int size) {
        this.rectangle = new Rectangle(size, size);
    }
    
    public void setSize(int size) {
        // Используем делегирование, а не наследование
        rectangle = new Rectangle(size, size);
    }
    
    public int getArea() {
        return rectangle.getArea();
    }
}

Скрытые зависимости и побочные эффекты

class UserService {
    private UserRepository repository;
    private EmailService emailService;
    private Logger logger;
    
    public UserService(UserRepository repository, EmailService emailService) {
        this.repository = repository;
        this.emailService = emailService;
        this.logger = Logger.getLogger(); // СКРЫТАЯ ЗАВИСИМОСТЬ!
    }
    
    public void registerUser(String email, String password) {
        // Валидация...
        User user = new User(email, password);
       
        // Ожидаемые действия:
        repository.save(user);
        emailService.sendWelcomeEmail(email); // Это логично и предсказуемо

        // А ЭТО — СКРЫТЫЕ ПОБОЧНЫЕ ЭФФЕКТЫ:
        logger.log("User registered: " + email); // Логирование не заявлено в контракте
        Metrics.incrementCounter("registrations"); // Метрики не очевидны из названия
        
        // И ещё хуже:
        if (email.endsWith("@admin.com")) {
            SecurityNotifier.notifyAdmins(); // Глобальное состояние!
        }
    }
}

public class HiddenDependenciesDemo {
    public static void main(String[] args) {
        // Тестируем регистрацию пользователя
        UserRepository repo = new TestUserRepository();
        EmailService email = new MockEmailService();
        
        UserService service = new UserService(repo, email);
        service.registerUser("test@example.com", "password");
        
        // Внезапно: наш тест падает из-за:
        // - глобального логгера, который не инициализирован
        // - метрик, которые требуют конфигурации
        // - SecurityNotifier, который пытается отправить реальные уведомления!
    }
}

Почему это проблема: Метод делает больше, чем заявлено в его контракте. Это усложняет тестирование и приводит к неожиданному поведению.

Как исправить:

// Явные зависимости через конструктор:
class UserService {
    private final UserRepository repository;
    private final EmailService emailService;
    private final Logger logger;
    private final Metrics metrics;
    
    // Все зависимости явные и обязательные
    public UserService(UserRepository repository, EmailService emailService, 
                      Logger logger, Metrics metrics) {
        this.repository = repository;
        this.emailService = emailService;
        this.logger = logger;
        this.metrics = metrics;
    }
    
    // Метод дела��т только то, что заявлено:
    public RegistrationResult registerUser(String email, String password) {
        User user = new User(email, password);
        repository.save(user);
        emailService.sendWelcomeEmail(email);
        logger.log("User registered: " + email);
        metrics.incrementCounter("registrations");
        
        return new RegistrationResult(user, SUCCESS);
    }
}

Заключение

ООП — это мощный инструмент, который помогает организовать сложный код, но требует понимания и осторожности:

Сильные стороны:

  • Естественное моделирование предметной области

  • Инкапсуляция защищает целостность данных

  • Полиморфизм делает код расширяемым

Опасности:

  • Слишком глубокие иерархии наследования

  • Скрытые побочные эффекты в методах

  • Сложность добавления нового поведения

Ключевой вывод: используйте ООП там, где оно действительно упрощает код, а не усложняет его. Комбинируйте ООП с другими подходами, и всегда помните о принципе KISS (Keep It Simple, Stupid) — самое простое решение часто оказывается лучшим.