JavaRush /Курсы /JAVA 25 SELF /Интерфейсы в архитектуре Java, паттерны проектирования

Интерфейсы в архитектуре Java, паттерны проектирования

JAVA 25 SELF
20 уровень , 4 лекция
Открыта

1. Интерфейсы как контракт: фундамент архитектуры

В Java (и не только) интерфейс — это не просто набор методов. Это контракт: обещание, что любой класс, реализующий интерфейс, поддерживает определённое поведение. Интерфейс определяет, что должно быть реализовано, а не как.

Почему это важно?

  • Разделение кода на уровни. Благодаря интерфейсам мы можем отделить «что делает» от «как делает». Например, если у вас есть интерфейс PaymentService, то разные реализации могут обрабатывать оплату банковской картой, PayPal или криптовалютой, но код, который вызывает pay(), не заботится о деталях.
  • Гибкость и расширяемость. Вы можете добавить новую реализацию интерфейса, не меняя остальной код. Это особенно важно в больших командах и долгоживущих проектах.
  • Тестируемость. Благодаря интерфейсам легко подменять реализации на тестовые (моки), не трогая основной код.

Пример: сервисный слой и DAO

Рассмотрим классический пример из бизнес-приложений. Пусть у нас есть интерфейс для работы с пользователями:


public interface UserRepository {
    User findById(int id);
    void save(User user);
}

В разных ситуациях мы можем реализовать этот интерфейс по-разному:

  • DatabaseUserRepository — хранит пользователей в базе данных.
  • InMemoryUserRepository — хранит пользователей в памяти (удобно для тестов).
  • FileUserRepository — сохраняет пользователей в файле.

Код, который работает с пользователями, зависит только от интерфейса:

public class UserService {
    private final UserRepository userRepository;

    // Внедрение зависимости через конструктор
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void registerUser(User user) {
        userRepository.save(user);
    }
}

Теперь мы можем легко подменять реализацию UserRepository без изменения кода сервиса.

2. Dependency Injection (внедрение зависимостей) и роль интерфейсов

Dependency Injection (DI, внедрение зависимостей) — это архитектурный приём, при котором зависимости (например, реализации интерфейсов) передаются объекту извне, обычно через конструктор или сеттер. Это позволяет строить гибкие, легко тестируемые и расширяемые приложения.

Почему интерфейсы важны для DI?

Если бы мы жёстко прописывали реализацию в коде, заменить её было бы сложно. С помощью интерфейсов мы можем подставлять любую реализацию, не меняя основной код.

Пример с внедрением зависимости

public interface NotificationSender {
    void send(String message);
}

public class EmailNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("Отправка Email: " + message);
    }
}

public class SmsNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("Отправка SMS: " + message);
    }
}

// Класс, который использует NotificationSender
public class NotificationService {
    private final NotificationSender sender;

    public NotificationService(NotificationSender sender) {
        this.sender = sender;
    }

    public void notifyUser(String message) {
        sender.send(message);
    }
}

Теперь вы можете легко протестировать NotificationService, передав ему, например, «заглушку» вместо реального отправителя сообщений.

3. Паттерны проектирования и интерфейсы

Интерфейсы — это не только про архитектуру, но и про паттерны проектирования. Многие паттерны невозможно реализовать без интерфейсов. Рассмотрим самые популярные.

Observer (Наблюдатель)

Observer — паттерн, который позволяет объекту (наблюдаемому) оповещать другие объекты (наблюдателей) об изменениях своего состояния.

UML-диаграмма (упрощённо):

+------------------+         +------------------------+
|   Subject        |<------->|   Observer             |
+------------------+         +------------------------+
| +addObserver()   |         | +update()              |
| +removeObserver()|         +------------------------+
| +notifyObservers()|
+------------------+

Пример кода:

import java.util.ArrayList;
import java.util.List;

// Интерфейс наблюдателя
public interface Observer {
    void update(String event);
}

// Интерфейс субъекта
public interface Subject {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers(String event);
}

// Реализация субъекта
public class NewsAgency implements Subject {
    private List<Observer> observers = new ArrayList<>();

    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(String event) {
        for (Observer observer : observers) {
            observer.update(event);
        }
    }
}

// Реализация наблюдателя
public class NewsReader implements Observer {
    private String name;

    public NewsReader(String name) {
        this.name = name;
    }

    @Override
    public void update(String event) {
        System.out.println(name + " получил новость: " + event);
    }
}

// Главный класс для запуска примера
public class ObserverExample {
    public static void main(String[] args) {
        // Создаём "агентство новостей" (субъект)
        NewsAgency agency = new NewsAgency();

        // Создаём наблюдателей
        Observer alice = new NewsReader("Алиса");
        Observer bob = new NewsReader("Боб");

        // Подписываем наблюдателей на новости
        agency.addObserver(alice);
        agency.addObserver(bob);

        // Отправляем новость
        agency.notifyObservers("Вышла новая версия Java!");

        // Убираем одного наблюдателя и отправляем ещё одну новость
        agency.removeObserver(bob);
        agency.notifyObservers("Следующая новость для подписчиков");
    }
}

Результат:

Алиса получил новость: Вышла новая версия Java!
Боб получил новость: Вышла новая версия Java!

Strategy (Стратегия)

Strategy — паттерн, позволяющий выбрать алгоритм поведения во время выполнения, не меняя клиентский код.

UML-диаграмма (упрощённо):

+------------------+
|   Context        |
+------------------+
| -strategy: Strat.|
| +setStrategy()   |
| +execute()       |
+------------------+
         |
         v
+------------------+
|   Strategy       |<-------------------------+
+------------------+                          |
| +execute()       |                          |
+------------------+                          |
         ^                                    |
         |                                    |
+------------------+   +------------------+   |
|   ConcreteA      |   |   ConcreteB      |---+
+------------------+   +------------------+
| +execute()       |   | +execute()       |
+------------------+   +------------------+

Пример кода:

public interface PaymentStrategy {
    void pay(int amount);
}

public class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Оплата " + amount + " руб. банковской картой");
    }
}

public class PaypalPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Оплата " + amount + " руб. через PayPal");
    }
}

public class OnlineStore {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout(int amount) {
        paymentStrategy.pay(amount);
    }
}

// Использование:
OnlineStore store = new OnlineStore();
store.setPaymentStrategy(new CreditCardPayment());
store.checkout(1000);

store.setPaymentStrategy(new PaypalPayment());
store.checkout(500);

Результат:

Оплата 1000 руб. банковской картой
Оплата 500 руб. через PayPal

Command (Команда)

Command — паттерн, инкапсулирующий запрос как объект, позволяя передавать действия как параметры.

Пример кода:

public interface Command {
    void execute();
}

public class LightOnCommand implements Command {
    @Override
    public void execute() {
        System.out.println("Свет включён!");
    }
}

public class LightOffCommand implements Command {
    @Override
    public void execute() {
        System.out.println("Свет выключен!");
    }
}

public class RemoteControl {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        command.execute();
    }
}

// Использование:
RemoteControl remote = new RemoteControl();
remote.setCommand(new LightOnCommand());
remote.pressButton(); // Свет включён!

remote.setCommand(new LightOffCommand());
remote.pressButton(); // Свет выключен!

4. Преимущества использования интерфейсов в архитектуре

  • Слабое связывание (Low Coupling). Код зависит только от интерфейса, а не от конкретной реализации. Это облегчает замену, тестирование и расширение.
  • Тестируемость. Легко подменить настоящую реализацию на тестовую (mock/stub) при написании unit-тестов.
  • Расширяемость. Можно добавлять новые реализации интерфейса без изменения существующего кода — принцип открытости/закрытости (OCP).
  • Параллельная разработка. Несколько команд могут независимо реализовывать разные части системы, если у них есть общий интерфейс.
  • Гибкость архитектуры. Легко внедрять новые паттерны и подходы.

5. Типичные ошибки при использовании интерфейсов в архитектуре

Ошибка № 1: Жёсткая привязка к реализации.
Если вы везде используете конкретные классы, а не интерфейсы, то любое изменение реализации потребует переписывать код во многих местах. Всегда старайтесь программировать «на уровне интерфейсов».

Ошибка № 2: Слишком большие интерфейсы (God Interface).
Интерфейс должен быть компактным и отвечать за одну зону ответственности. Не стоит пихать в один интерфейс всё подряд — иначе реализация станет тяжёлой и запутанной.

Ошибка № 3: Игнорирование преимуществ тестируемости.
Если вы не используете интерфейсы для подмены зависимостей в тестах, ваши тесты могут стать медленными и ненадёжными, особенно если они работают с реальными базами данных или сетями.

Ошибка № 4: Множественная реализация, но отсутствие DI.
Если вы сделали несколько реализаций интерфейса, но жёстко прописали одну из них в коде, вы теряете всю гибкость архитектуры. Используйте внедрение зависимостей (DI)!

1
Задача
JAVA 25 SELF, 20 уровень, 4 лекция
Недоступна
Гибкая Система Журналирования: Консоль или Файл
Гибкая Система Журналирования: Консоль или Файл
1
Задача
JAVA 25 SELF, 20 уровень, 4 лекция
Недоступна
Уведомления Пользователям: Гибкий Способ Отправки
Уведомления Пользователям: Гибкий Способ Отправки
1
Задача
JAVA 25 SELF, 20 уровень, 4 лекция
Недоступна
Приветствия на Разный Лад: Приветливый или Формальный
Приветствия на Разный Лад: Приветливый или Формальный
1
Задача
JAVA 25 SELF, 20 уровень, 4 лекция
Недоступна
Новостной Канал: Издатель и Подписчики
Новостной Канал: Издатель и Подписчики
1
Опрос
Интерфейсы, 20 уровень, 4 лекция
Недоступен
Интерфейсы
Понятие интерфейса
Комментарии (11)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Xaxatumba Уровень 38
12 ноября 2025
Да подача знаний здесь чумовая. Людям которые только начали изучение сразу давать архитектуру не объяснив вообще как строиться проект, паттерны не объяснив что это такое, внедрение зависимостей не понимая вообще этого словосочетания. Это просто недопустимо. В лучшем случае конечный читатель просто проигнорирует данную лекцию, в худшем забьёт на дальнейшее обучение и разочаруется в языке.
Александр Уровень 35
6 ноября 2025
Выбрал первый вариант, но он оказался не правильным. Прямо на картинке мои комментарии. В чем ошибаюсь?
Roke Уровень 25
11 ноября 2025
Абстрактный класс может содержать конструктор, в отличии от интерфейса.
Александр Уровень 35
11 ноября 2025
Действительно... https://javarush.com/groups/posts/431-10-voprosov-po-abstraktnihm-klassam-i-interfeysam-s-sobesedovaniy-po-jazihku-java Спасибо
Shanechka Уровень 31
29 октября 2025
А почему на вопрос "Какой принцип объектно-ориентированного проектирования реализуется при использовании интерфейсов?" Ответ: Принцип открытости/закрытости не подходит?
Александр Уровень 35
6 ноября 2025
Второй вариант можно не пробовать, он тоже не подходит))))
Xaxatumba Уровень 38
12 ноября 2025
Ну раз пошла такая пляска третий тоже неверный 😌.
German Malykh Уровень 29
26 сентября 2025

// Интерфейс наблюдателя
public interface Observer {
    void update(String event);
}

// Интерфейс субъекта
public interface Subject {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers(String event);
}
Вроде не упоминалось в лекциях. В Java методы могут принимать параметры любого типа — в том числе интерфейсного. Это значит: «дай мне любой объект, который реализует этот интерфейс». Что тут происходит: * Observer — контракт «наблюдателя»: у него есть метод update(String event). * Subject — контракт «издателя/субъекта», который умеет: - addObserver(Observer o) — принять любой объект, реализующий Observer; - removeObserver(Observer o) — убрать такого наблюдателя; - notifyObservers(String event) — оповестить всех. Это не «передача интерфейса», а передача ссылки на объект, чей статический тип — Observer. В рантайме это будет экземпляр конкретного класса, реализующего Observer.
German Malykh Уровень 29
26 сентября 2025
# Роли (кто есть кто) * `Observer` — контракт наблюдателя: у него есть `update(event)`. * `Subject` — контракт издателя: умеет добавлять/удалять наблюдателей и оповещатьих. * `NewsAgency` — конкретный издатель (реализация `Subject`): хранит список наблюдателей и вызывает у каждого `update`. * `NewsReader` — конкретный наблюдатель (реализация `Observer`): печатает, что получил событие. # Что значит «интерфейс как параметр» Методы `addObserver(Observer o)`/`removeObserver(Observer o)` принимают любой объект, который реализует `Observer`. Т.е. в список `List<Observer>` кладутся ссылки на объекты любых классов, реализующих этот контракт (в твоём коде — `NewsReader`). # Как это работает пошагово (трассировка выполнения)

public class Demo {
    public static void main(String[] args) {
        NewsAgency agency = new NewsAgency();        // 1) создаём издателя

        NewsReader alice = new NewsReader("Алиса");  // 2) создаём наблюдателей
        NewsReader bob   = new NewsReader("Боб");

        agency.addObserver(alice);                   // 3) подписка: список = [Алиса, Боб]
        agency.addObserver(bob);

        agency.notifyObservers("Релиз 1.0");         // 4) оповещение:
        // внутри:
        //   for (Observer o : observers) o.update("Релиз 1.0");
        //   → вызовется alice.update("Релиз 1.0")
        //   → вызовется bob.update("Релиз 1.0")

        agency.removeObserver(bob);                  // 5) отписка Боба: список = [Алиса]

        agency.notifyObservers("Хотфикс 1.0.1");     // 6) оповещение получит только Алиса
    }
}
German Malykh Уровень 29
26 сентября 2025
Ожидаемый вывод:

Алиса получил новость: Релиз 1.0
Боб получил новость: Релиз 1.0
Алиса получил новость: Хотфикс 1.0.1
# Ментальная модель (очень просто) * `Subject` держит список слушателей. * При событии `Subject` идёт по списку и вызывает один и тот же метод `update(...)` у каждого. * Конкретные классы слушателей решают, что делать внутри `update`. # Почему это удобно * `Subject` не знает, какие именно классы подписаны — только что они умеют `update`. → низкая связность, легко добавлять новые типы слушателей (лог, email, метрики и т.д.). * Легко тестировать: можно подложить фейкового `Observer`, который просто записывает полученные события. # Частые ошибки * Забыли удалить наблюдателя → будет продолжать получать события. * Изменяют `observers` во время обхода → ConcurrentModificationException (если из `update` делать отписку; решается копией списка или безопасными коллекциями). * Длинные/падающие `update` у одного наблюдателя тормозят/валят оповещение остальных (решается асинхронностью, очередями).
nastya_zhadan Уровень 66
23 сентября 2025
Вот это, конечно, рановато: Ошибка № 3: Игнорирование преимуществ тестируемости. Если вы не используете интерфейсы для подмены зависимостей в тестах, ваши тесты могут стать медленными и ненадёжными (например, при работе с реальными БД или сетью). Используйте моки/стабы вместо реальных интеграций 🙃 А так, лекция хорошая)