5 основных правил объектно-ориентированного дизайна
Суть: У класса должна быть только одна причина для изменения. Каждый класс должен отвечать за одну задачу.
Класс Invoice делает слишком много: считает сумму, сохраняет в БД и отправляет Email. Если изменится логика сохранения или формат письма — придется менять один класс.
class Invoice {
public void calculateTotal() {
/* ... */
}
public void saveToDatabase() {
/* сохранение в БД */
}
public void sendEmailNotification() {
/* отправка email */
}
}
class Invoice {
public double calculateTotal() {
return 100.0;
}
}
class InvoiceRepository {
public void save(Invoice invoice) {
/* БД */
}
}
class EmailService {
public void sendInvoice(Invoice invoice) {
/* email */
}
}
Суть: Программные сущности должны быть открыты для расширения, но закрыты для изменения.
Каждая новая фича заставляет переписывать существующий if/else или switch, что рискует сломать уже работающий код. Каждый раз при добавлении новой фигуры придется менять метод calculate().
class AreaCalculator {
public double calculate(Object shape) {
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return Math.PI * circle.radius *
circle.radius;
} else if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle) shape;
return rect.width * rect.height;
}
return 0;
}
}
⚠️ Проблемы:
interface Shape {
double area();
}
class Circle implements Shape {
private double radius;
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
private double width, height;
public double area() {
return width * height;
}
}
class AreaCalculator {
public double calculate(Shape shape) {
return shape.area();
}
}
✓ Преимущества:
class Triangle implements Shape {
private double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
public double area() {
return (base * height) / 2;
}
}
// Использование
AreaCalculator calculator = new AreaCalculator();
calculator.calculate(new Circle(5));
calculator.calculate(new Rectangle(4, 6));
calculator.calculate(new Triangle(3, 4)); // новая фигура!
Класс AreaCalculator больше никогда не нужно менять! Он "закрыт для модификации", но "открыт для расширения" через новые реализации интерфейса Shape.
Суть: Наследник должен дополнять поведение базового типа, а не нарушать его. Объекты подклассов должны заменять объекты базового класса без ошибок в работе программы.
Класс Ostrich (Страус) наследуется от Bird, но бросает исключение в fly(), меняя ожидаемое поведение. Это нарушает контракт базового класса!
class Bird {
public void fly() {
System.out.println("Лечу!");
}
}
class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException(
"Страусы не умеют летать!"
);
}
}
// Код сломается!
Bird bird = new Ostrich();
bird.fly(); // ❌ Исключение!
⚠️ Проблемы:
interface Bird {
void eat();
}
interface FlyingBird extends Bird {
void fly();
}
class Sparrow implements FlyingBird {
public void eat() {
System.out.println("Воробей клюёт");
}
public void fly() {
System.out.println("Воробей летит");
}
}
class Ostrich implements Bird {
public void eat() {
System.out.println("Страус ест");
}
public void run() {
System.out.println("Страус бежит");
}
}
✓ Преимущества:
// Функция ожидает летающую птицу
public void makeBirdFly(FlyingBird bird) {
bird.fly(); // Безопасно! Только FlyingBird
}
makeBirdFly(new Sparrow()); // ✅ OK
makeBirdFly(new Ostrich()); // ❌ Ошибка на этапе компиляции
// Функция работает с любой птицей
public void feedBird(Bird bird) {
bird.eat(); // Безопасно! Все птицы едят
}
feedBird(new Sparrow()); // ✅ OK
feedBird(new Ostrich()); // ✅ OK
"Если S является подтипом T, то объекты типа S могут безопасно использоваться везде, где ожидается объект типа T."
Иными словами: если Ostrich реализует Bird, то везде, где нужна Bird, можно передать Ostrich без риска ошибки.
Суть: Клиенты не должны зависеть от методов, которые они не используют. Лучше много специализированных интерфейсов, чем один универсальный.
Обычному рабочему принудительно заставляют реализовывать методы code(), хотя он не программист. Робот не может есть, но вынужден реализовывать eat().
interface Worker {
void work();
void eat();
void code(); // Не все работают программистами!
void design(); // Не все дизайнеры!
}
class Programmer implements Worker {
public void work() {
System.out.println("Пишу код");
}
public void eat() {
System.out.println("Обедаю");
}
public void code() {
System.out.println("Кодирую");
}
public void design() {
throw new Exception(
"Я не дизайнер!"
); // ❌ Нужно реализовать!
}
}
class RobotWorker implements Worker {
public void work() {
System.out.println("Робот работает");
}
public void eat() {
throw new Exception(
"Робот не ест!"
); // ❌ Ненужный метод!
}
public void code() {
System.out.println("Кодирую");
}
public void design() {
throw new Exception(
"Робот не дизайнит!"
); // ❌ Ненужный метод!
}
}
⚠️ Проблемы:
// Разбили на маленькие контекстные интерфейсы
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Codeable {
void code();
}
interface Designable {
void design();
}
class Programmer implements Workable,
Codeable,
Eatable {
public void work() {
System.out.println("Пишу код");
}
public void eat() {
System.out.println("Обедаю");
}
public void code() {
System.out.println("Кодирую");
}
}
class Designer implements Workable,
Designable,
Eatable {
public void work() {
System.out.println("Дизайню");
}
public void design() {
System.out.println("Создаю дизайн");
}
public void eat() {
System.out.println("Обедаю");
}
}
class RobotWorker implements Workable,
Codeable {
public void work() {
System.out.println("Робот работает");
}
public void code() {
System.out.println("Робот кодирует");
}
}
✓ Преимущества:
// Функция работает только с кодирующими
public void codeReview(Codeable codeable) {
codeable.code();
}
// Функция работает только с едящими
public void lunchBreak(Eatable eatable) {
eatable.eat();
}
// Функция работает с любым работником
public void assignTask(Workable worker) {
worker.work();
}
// Всё работает правильно!
codeReview(new Programmer()); // ✅ OK
codeReview(new RobotWorker()); // ✅ OK
codeReview(new Designer()); // ❌ Ошибка компиляции - хорошо!
lunchBreak(new Programmer()); // ✅ OK
lunchBreak(new RobotWorker()); // ❌ Ошибка - робот не ест!
"Лучше иметь много узкопрофильных интерфейсов, чем один толстый интерфейс, который никто полностью не использует."
Это делает код:
Суть: Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
Класс Car жестко завязан на конкретный класс GasEngine. Поменять его на ElectricEngine без переписывания Car не выйдет.
class GasEngine {
public void start() {
System.out.println("Запуск ДВС");
}
}
class Car {
// ❌ Жесткая зависимость!
private GasEngine engine =
new GasEngine();
public void start() {
engine.start();
}
}
// Проблема: как поменять двигатель?
Car car = new Car(); // Только бензиновый!
// car.engine = new ElectricEngine();
// ❌ Нельзя!
⚠️ Проблемы:
interface Engine {
void start();
}
class GasEngine implements Engine {
public void start() {
System.out.println("Запуск ДВС");
}
}
class ElectricEngine implements Engine {
public void start() {
System.out.println(
"Запуск электродвигателя"
);
}
}
class Car {
private final Engine engine;
// Dependency Injection
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
}
}
✓ Преимущества:
// Легко менять двигатели!
Car gasCar = new Car(new GasEngine());
gasCar.start();
// "Запуск ДВС"
Car electricCar = new Car(new ElectricEngine());
electricCar.start();
// "Запуск электродвигателя"
// Тестирование с mock двигателем
class MockEngine implements Engine {
public void start() {
System.out.println("Mock двигатель");
}
}
Car testCar = new Car(new MockEngine());
testCar.start(); // Тест с mock!
| Аспект | ❌ Плохо | ✅ Хорошо |
|---|---|---|
| Car зависит от | GasEngine (конкретный) | Engine (интерфейс) |
| Создание двигателя | Car создает сам | Внедряется через конструктор |
| Смена двигателя | Требует изменения Car | Достаточно другого аргумента |
| Тестирование | Невозможно mock | Легко MockEngine |
"Зависимости должны течь от конкретных классов к абстракциям, а не наоборот."
Это называется Dependency Injection — мощный паттерн для гибких и тестируемых приложений!