Оглавление
Введение
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
}
}
Что демонстрирует этот пример наследования:
Наследование структуры — все дочерние классы технически содержат поле
name
от родителяTransport
, но не имеют к нему прямого доступа из-за модификатораprivate
. Взаимодействие с этим полем происходит через унаследованные публичные методы родителя.Наследование методов - все классы могут использовать
move()
иstop()
от родителяРасширение функциональности - каждый дочерний класс добавил свои уникальные методы:
Car:
honk()
- сигналитBicycle:
ringBell()
- звонит в звонокAirplane:
takeOff()
- взлетает
Вызов конструктора родителя - каждый класс вызывает конструктор родителя через
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 интерфейсы
Критерий | Полиморфизм через наследование | Полиморфизм через интерфейсы |
---|---|---|
Связность | Жёсткая связь с родительским классом | Слабая связь, только контракт |
Гибкость | Один родитель, нельзя наследовать от нескольких классов | Можно реализовать несколько интерфейсов |
Переиспользование | Наследуется всё: и методы, и поля, и поведение | Реализуется только контракт, внутренняя реализация свободна |
Расширяемость | Для добавления функциональности нужно менять иерархию | Новые интерфейсы добавляются без изменения существующих классов |
Пример в коде |
|
|
Когда использовать | Когда есть чёткая иерархия "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) — самое простое решение часто оказывается лучшим.