Перейти к основному содержимому

6.11. Типы классов

Разработчику Архитектору Аналитику

Типы классов

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

Имена классов — это сигналы. Они несут семантическую нагрузку. Название UserLoginHandler сразу сообщает разработчику, что перед ним класс, отвечающий за обработку события входа пользователя, а не за хранение данных о пользователе или управление сессией. Такие имена работают как договорённости внутри команды — неформальные, но эффективные.

Эти договорённости складываются из опыта, из наблюдения за тем, какие структуры повторяются в разных проектах и как они помогают справиться с типовыми проблемами: инкапсуляция данных, разделение забот, отложённое создание объектов, реакция на события, управление зависимостями и так далее.

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


Абстрактные типы классов

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

Такие типы не реализуют поведения (или реализуют его частично), но задают форму взаимодействия. Они особенно важны в крупных системах, где разные модули разрабатываются параллельно, и где необходимо чётко отделить интерфейс от реализации.

Интерфейс (Interface)

Интерфейс — это строгий контракт, определяющий набор методов, которые класс-реализатор обязан предоставить. Он не содержит состояния и не включает реализацию. В языках, где интерфейсы поддерживают дефолтные методы (например, Java 8+), они всё равно не используются для хранения данных — только для реализации общего поведения «по умолчанию», которое может быть переопределено.

Интерфейсы служат основой для полиморфизма и для внедрения зависимостей. Например, если сервис авторизации зависит от UserRepository, и UserRepository объявлен как интерфейс, то реальная реализация может быть подменена в тестах на заглушку без изменения самого сервиса.

Интерфейсы часто появляются в паттернах:

  • Strategy — набор взаимозаменяемых алгоритмов (CompressionStrategy, SortingStrategy);
  • Observer — подписчики на события реализуют Observer, а издатель работает с List<Observer>;
  • Factory Method — фабрика возвращает объект по интерфейсу (Document), а не по конкретному классу (PDFDocument).

Замечание: в некоторых языках (например, Go) интерфейсы определяются неявно — по набору методов. Это не отменяет их роли как контракта, но смещает акцент с объявления интерфейса на соблюдение его поведения.

Абстрактный класс (Abstract Class)

Абстрактный класс отличается от интерфейса тем, что может содержать как абстрактные методы (без реализации), так и конкретные — с реализацией по умолчанию. Он может хранить состояние и инициализировать его в конструкторе. Однако напрямую создать экземпляр абстрактного класса нельзя — только его подклассов.

Абстрактные классы применяются там, где существует общая часть поведения, которую логично вынести, но при этом остаётся вариативная часть, которую каждый наследник должен определить самостоятельно.

Характерный пример — паттерн Template Method:

AbstractReportGenerator
├─ generate() { // шаблонный метод
│ prepareData()
│ formatHeader()
│ renderBody() ← абстрактный
│ formatFooter()
│ }
├─ formatHeader() { ... } // конкретная реализация
└─ abstract renderBody()

Здесь AbstractReportGenerator задаёт общую структуру отчёта, но делегирует его содержательную часть (renderBody) подклассам (PDFReportGenerator, HTMLReportGenerator). Такой подход предпочтительнее чистых интерфейсов, когда есть не только контракт, но и общая логика.

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

Контракт (Contract)

Термин «контракт» часто используется в методологиях, где акцент сделан на доменную модель и разделение слоёв, например, в Domain-Driven Design (DDD) и Clean Architecture. Контракт — это более широкое понятие, чем интерфейс в языковом смысле. Он включает:

  • сигнатуры методов,
  • предусловия и постусловия (что должно быть истинным до и после вызова),
  • инварианты (что всегда остаётся неизменным),
  • семантические обязательства (например, «метод не должен вызывать внешние API»).

Контракт может быть задан интерфейсом, но может также сопровождаться документацией, аннотациями (например, @PreAuthorize, @Valid), тестами (contract tests), или быть выраженным в виде спецификаций (например, OpenAPI для REST).

Например, OrderProcessingService может иметь контракт:

  • принимает OrderDTO,
  • не выбрасывает исключений при валидных данных,
  • гарантирует атомарность: либо создаётся заказ и резервируются товары, либо ничего не происходит.

Реализация может быть разной — через базу данных, через распределённую транзакцию, через компенсирующие операции — но контракт остаётся неизменным.

Service Interface

Интерфейс сервиса описывает бизнес-операции, которые могут быть выполнены в рамках предметной области. Он не связан напрямую с UI, HTTP, базой данных или конкретной платформой. Это уровень доменной логики.

Типичные признаки:

  • методы выражены в терминах предметной области: placeOrder, cancelSubscription, assignRole;
  • параметры — доменные объекты (User, Order) или DTO (CreateOrderRequest);
  • возвращаемые значения — результат операции (OrderConfirmation, ValidationResult);
  • не зависит от фреймворков (например, не принимает HttpServletRequest).

В архитектурах MVC/MVP/MVVM сервисный интерфейс используется как зависимость контроллера/презентера — таким образом UI-слой не привязывается к реализации бизнес-логики.

В DDD такие интерфейсы часто называются Domain Service Interface — особенно если операция не принадлежит ни одной сущности (например, проверка уникальности имени пользователя требует обращения к репозиторию, что нарушает принцип «сущность не должна знать о репозитории»).

Repository Interface

Репозиторий — это абстракция над хранилищем данных, которая скрывает детали доступа (SQL, NoSQL, файлы, кэш) и предоставляет коллекцию-подобный интерфейс для работы с агрегатами.

Интерфейс репозитория обычно включает:

  • findById(id),
  • save(entity),
  • delete(entity),
  • findAll(),
  • специфичные методы поиска: findByEmail(email), findActiveOrders(userId).

Ключевой принцип: репозиторий работает с агрегатами, а не с таблицами. Это означает, что он возвращает целостные объекты (например, Order со всеми OrderLines), а не частичные данные.

От DAO (см. ниже) репозиторий отличается уровнем абстракции: DAO ближе к данным (маппинг «таблица → объект»), репозиторий ближе к домену (маппинг «агрегат → состояние»).

В Clean Architecture и Hexagonal Architecture репозиторий — это порт (port), направленный внутрь, а его реализация (PostgresOrderRepository) — адаптер, направленный наружу.

DAO (Data Access Object Interface)

DAO — более техническая абстракция, возникшая в эпоху Java EE. Её цель — изолировать бизнес-логику от конкретного способа доступа к данным (JDBC, JPA, MyBatis).

Интерфейс DAO обычно:

  • ориентирован на сущность (UserDAO, ProductDAO);
  • содержит методы CRUD (create, read, update, delete);
  • может включать постраничный поиск, фильтрацию;
  • часто возвращает «плоские» объекты (UserRecord), а не богатые доменные модели.

DAO не запрещает работы с несколькими таблицами (например, OrderDAO может делать JOIN с order_items), но не объединяет разнородные сущности в один агрегат — это выходит за рамки его ответственности.

Разница между Repository и DAO часто размывается на практике, но семантически:

  • DAO — это технический слой между бизнес-логикой и БД;
  • Repository — это концептуальный слой между доменом и инфраструктурой.

Выбор термина зависит от архитектурного стиля: в DDD предпочтителен Repository, в традиционных CRUD-приложениях — DAO.

Фабрика (Factory Interface)

Фабрика — это интерфейс, ответственный за создание объектов. Её преимущество перед конструктором — скрытие логики инстанцирования: выбор конкретного типа, инициализация зависимостей, кэширование, валидация параметров.

Интерфейс фабрики может выглядеть как:

DocumentFactory
├─ createDocument(type: DocumentType, params: Map<String, Object>): Document

Реализации могут:

  • возвращать PDFDocument или WordDocument в зависимости от type;
  • проверять, что params содержит обязательные поля;
  • инжектировать зависимости в создаваемый объект (например, TemplateEngine);
  • кэшировать тяжёлые ресурсы (шрифты, конфигурации).

Фабрика часто сочетается с DI-контейнерами: сама фабрика инжектится как зависимость, и создаёт объекты с учётом контекста (например, UserContextAwareDocumentFactory).

Паттерны: Factory Method, Abstract Factory (когда фабрика сама является продуктом другой фабрики — например, ReportBuilderFactory возвращает PDFReportBuilder или HTMLReportBuilder).

Билдер (Builder Interface)

Билдер применяется при создании сложных объектов с множеством опциональных параметров. Интерфейс билдера задаёт пошаговый процесс конструирования, где каждый шаг возвращает либо самого себя (fluent interface), либо следующий интерфейс в цепочке (пошаговый билдер).

Пример интерфейса:

ReportBuilder
├─ withTitle(title: String): ReportBuilder
├─ addSection(name: String, content: String): ReportBuilder
├─ setTheme(theme: Theme): ReportBuilder
└─ build(): Report

Билдер отличается от фабрики тем, что:

  • фокусируется на конфигурации одного типа объекта;
  • поддерживает fluent-синтаксис;
  • часто используется, когда конструктор с множеством параметров становится нечитаемым («anti-pattern telescoping constructor»).

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

  • разделить логику построения и использования;
  • поддерживать неизменяемость итогового объекта (build() возвращает новый Report);
  • валидировать состояние перед созданием.

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

Стратегия — это интерфейс, инкапсулирующий алгоритм, который может меняться в зависимости от контекста. Класс, использующий стратегию, не знает деталей её реализации — он лишь вызывает метод (execute(), apply() и т.п.).

Примеры:

  • PaymentStrategy (CreditCardStrategy, PayPalStrategy);
  • SortingStrategy (QuickSort, MergeSort);
  • NotificationStrategy (EmailNotification, PushNotification).

Преимущества:

  • избегается множественная условная логика (if (type == "email") ... else if (type == "sms"));
  • новые алгоритмы добавляются без изменения существующего кода (OCP);
  • алгоритмы тестируются независимо.

Стратегия особенно эффективна в сочетании с DI: контейнер может инжектить нужную реализацию на основе конфигурации или runtime-условий.

Команда (Command Interface)

Команда представляет операцию как самостоятельный объект. Интерфейс обычно содержит один метод — execute() — и может включать undo(), redo(), canExecute().

Применения:

  • реализация отмены/повтора действий (редакторы, игры);
  • очередь задач (отложить выполнение, сериализовать, выполнить в фоне);
  • логирование операций (каждая команда — запись в лог);
  • ограничение прав доступа («пользователь X может выполнить команду Y?»).

Команда инкапсулирует:

  • что нужно сделать,
  • с какими данными,
  • в каком контексте.

Пример:
TransferMoneyCommand содержит fromAccount, toAccount, amount; при execute() проверяет баланс, списывает, зачисляет, логирует; при undo() — возвращает деньги.

Обратите внимание: не путать с паттерном Command в CQRS, где Command — это сообщение без поведения, а обработка делегируется CommandHandler. Там команда — это данные, а не объект с методом execute().

Обработчик (Handler Interface)

Обработчик — это обобщённый интерфейс для компонентов, которые реагируют на запросы, события или ошибки. Он часто используется в middleware-цепочках, где каждый обработчик может:

  • обработать запрос и завершить цепочку,
  • модифицировать запрос и передать дальше,
  • перехватить ошибку и преобразовать её.

Примеры интерфейсов:

  • RequestHandler.handle(request) → response;
  • ErrorHandler.handle(exception) → response;
  • ValidationHandler.handle(data) → ValidationResult.

Паттерн Цепочка обязанностей (Chain of Responsibility) строится именно на таких интерфейсах: каждый обработчик имеет ссылку на следующего, и решение о передаче принимается внутри.

В веб-фреймворках:

  • ASP.NET Core — IRequestHandler<TRequest, TResponse>;
  • MediatR — IRequestHandler;
  • Java Servlet — Filter (аналог обработчика в цепочке).

Прослушиватель (Listener Interface) и Наблюдатель (Observer Interface)

Эти интерфейсы тесно связаны и часто реализуются вместе.

Listener — это реактивный интерфейс: он вызывается, когда происходит внешнее событие (клик мыши, получение сообщения, завершение задачи). Он не опрашивает систему — он ждёт уведомления.
Пример: WebSocketConnectionListener.onOpen(session).

Observer — это подписочный интерфейс: объект-наблюдатель регистрируется у источника (subject), и при изменении состояния subject вызывает update() у всех подписчиков.
Пример: OrderStatusObserver.update(order).

Разница в топологии:

  • Listener — обычно один-к-одному или один-к-многим с явной регистрацией (.addEventListener());
  • Observer — многие-ко-многим, с управлением подпиской через subscribe()/unsubscribe().

В современных системах оба подхода объединяются в Event Bus или Reactive Streams (RxJava, Project Reactor), где события распространяются через потоки (Observable, Flow).

Адаптер (Adapter Interface)

Адаптер — это интерфейс, который преобразует один API в другой, делая несовместимые интерфейсы совместимыми.

Существует два вида:

  • Объектный адаптер — содержит ссылку на адаптируемый объект и реализует целевой интерфейс;
  • Классовый адаптер — наследуется от адаптируемого класса и реализует целевой интерфейс (требует множественного наследования или композиции через extends + implements).

Пример:
У вас есть сторонняя библиотека LegacyPaymentGateway с методом processPayment(amount, currencyCode), а в вашем приложении нужен интерфейс PaymentService.process(order: Order).
Тогда создаётся LegacyPaymentGatewayAdapter implements PaymentService, который внутри вызывает legacyGateway.processPayment(order.amount, order.currency.isoCode).

Адаптеры критически важны при интеграции с legacy-системами, внешними API, или при миграции между версиями.

Модель (Model Interface), Презентер (Presenter Interface), Представление (View Interface)

Эти интерфейсы формируют основу архитектурных паттернов представления:

  • Model — абстракция данных и бизнес-логики. Может быть интерфейсом (UserModel), если требуется подменяемость (например, локальная vs облачная модель).
  • View — интерфейс для UI: showLoading(), displayUser(User), hideError(). Это позволяет тестировать логику без GUI и использовать один и тот же презентер в разных UI (веб, мобильное приложение, CLI).
  • Presenter — посредник: получает события от View (onLoginClicked()), вызывает Model (userService.login()), обновляет View (view.displayUser()). Интерфейс Presenter нужен, если презентер может быть заменён (редко), но чаще его реализация внедряется в View.

В MVP (Model-View-Presenter) View и Presenter связаны двусторонне: View знает о Presenter’е, Presenter — о View (через интерфейс). Это отличает MVP от MVC, где Controller не знает о View.

Юзкейс (Use Case Interface)

Интерфейс юзкейса — это высокоуровневая операция, выраженная в терминах пользователя: PlaceOrderUseCase, RegisterUserUseCase.

Он:

  • принимает простые входные данные (DTO, примитивы);
  • возвращает результат (успех/ошибка, данные);
  • не зависит от фреймворков, UI, БД;
  • может использовать несколько сервисов, репозиториев, внешних вызовов.

В Clean Architecture юзкейсы — это ядро приложения. Их интерфейсы размещаются во внутреннем слое (Use Cases), а реализации — в том же слое или в Application Services.

Пример сигнатуры:
PlaceOrderUseCase.execute(request: PlaceOrderRequest): Result<OrderConfirmation>

Итератор (Iterator Interface)

Итератор предоставляет стандартизированный способ последовательного обхода элементов коллекции без раскрытия её внутренней структуры.

Интерфейс обычно содержит:

  • hasNext(): boolean;
  • next(): T.

Преимущества:

  • скрытие реализации коллекции (связный список, дерево, файл на диске);
  • поддержка нескольких параллельных обходов (каждый итератор — независимое состояние);
  • возможность ленивой загрузки (например, итератор по базе данных подгружает страницы по мере next()).

В языках с поддержкой foreach (C#, Java, Python) итераторы используются неявно, но при создании кастомных коллекций реализация Iterable<T> (Java) или IEnumerable<T> (C#) обязательна.

enum / Enum

Перечисление — это специализированный тип, представляющий фиксированный набор именованных значений.

Хотя enum не является классом в полной мере (в Java — это final class extends Enum; в C# — struct), он часто используется как доменный примитив:

  • OrderStatus.DRAFT, OrderStatus.CONFIRMED, OrderStatus.SHIPPED;
  • UserRole.ADMIN, UserRole.USER;
  • PaymentMethod.CREDIT_CARD, PaymentMethod.CRYPTO.

Преимущества перед строками и числами:

  • компилятор проверяет исчерпаемость (switch без default может быть безопасным);
  • имена значений говорят сами за себя;
  • можно добавлять поведение (status.isFinal()), параметры (paymentMethod.feePercent), методы.

В DDD enum — это частный случай Value Object, когда значение дискретно и небольшое.

Specification (Интерфейс спецификации)

Спецификация — это объект, инкапсулирующий логическое правило для проверки объекта. Интерфейс обычно выглядит как:

Specification<T>
├─ isSatisfiedBy(candidate: T): boolean

Примеры:

  • ActiveUserSpecification.isSatisfiedBy(user)user.isActive && user.emailConfirmed;
  • OrderTotalOverSpecification(1000).isSatisfiedBy(order)order.total > 1000.

Спецификации можно:

  • комбинировать: and(spec1, spec2), or(spec1, spec2), not(spec);
  • сериализовать (например, в JSON для поиска);
  • передавать как параметр («найти всех, кто удовлетворяет спецификации X»).

Паттерн активно используется в DDD для построения гибких фильтров и правил валидации.

Specification Validator

Это обёртка над спецификацией, которая проверяет isSatisfiedBy и возвращает пояснения, почему правило не выполнено.

Интерфейс:

SpecificationValidator<T>
├─ validate(candidate: T): ValidationResult

ValidationResult содержит:

  • isValid: boolean;
  • errors: List<String>.

Такой подход позволяет накапливать ошибки, а не останавливаться на первой. Особенно ценен при валидации форм.

Port (Порт)

В Hexagonal Architecture (архитектура «портов и адаптеров») порт — это интерфейс, направленный внутрь приложения, описывающий, как приложение хочет взаимодействовать с внешним миром.

Примеры портов:

  • UserRegistrationPort (как приложение хочет регистрировать пользователей);
  • PaymentPort (как приложение хочет проводить платежи);
  • NotificationPort (как приложение хочет уведомлять).

Порт — это абстракция потребности. Его реализация — адаптер (EmailNotificationAdapter, StripePaymentAdapter) — подключается снаружи и переводит потребность в конкретный протокол.

Таким образом, ядро приложения зависит только от портов, а не от инфраструктуры.

Subject и Observer Interface — повторно, для полноты

Subject — это издатель событий. Его интерфейс:

Subject
├─ register(observer: Observer)
├─ unregister(observer: Observer)
└─ notifyObservers()

Observer — подписчик:

Observer
└─ update(subject: Subject)

Это классическая реализация паттерна Observer (GoF). В современных системах вместо update() часто передаётся конкретное событие (OrderCreatedEvent), а не сам subject, чтобы снизить связанность.

Aggregate Root Interface, Entity Interface, Value Object Interface — DDD-триада

В Domain-Driven Design три фундаментальных абстракции:

  1. Entity — объект с уникальной идентичностью, которая сохраняется в течение жизненного цикла (даже если атрибуты меняются).
    Интерфейс может задавать getId(), equals() по ID.

  2. Value Object — объект, идентифицируемый только по значениям полей. Не имеет ID, неизменяем, может быть использован как аргумент или возвращаемое значение.
    Примеры: Money(amount, currency), Address(street, city, zip).
    Интерфейс часто не нужен — достаточно соглашения, но в строгих системах может быть ValueObject<T> с equals(), hashCode().

  3. Aggregate Root — корневая сущность агрегата, через которую должен происходить весь доступ к внутренним объектам. Гарантирует целостность агрегата как единого целого.
    Интерфейс может включать getVersion() для оптимистичной блокировки, или validate().

Эти интерфейсы редко объявляются явно — чаще они задаются соглашениями и базовыми классами. Но понимание их роли критично для проектирования.

Mapper Interface

Mapper — это интерфейс для преобразования данных между слоями. Типичные направления:

  • Entity → DTO (для отправки клиенту);
  • DTO → Entity (для сохранения в БД);
  • Domain Model → API Model.

Сигнатура:

UserMapper
├─ toDto(user: UserEntity): UserDto
├─ toEntity(dto: UserDto): UserEntity

Важно: маппер не должен содержать бизнес-логику. Его задача — чистое копирование и приведение типов. Если нужно преобразование с вычислениями (fullName = firstName + " " + lastName), это лучше вынести в доменную модель или в отдельный Transformer.


Main — точка входа

Main — это стартовый класс приложения, содержащий метод, с которого начинается выполнение (main() в Java/C#/C++, if __name__ == "__main__": в Python).

Хотя его роль кажется тривиальной, он несёт важную семантическую нагрузку:

  • это граница между операционной системой и приложением;
  • здесь происходит инициализация среды выполнения: загрузка конфигурации, создание DI-контейнера, подключение логгера, регистрация сигналов (для graceful shutdown);
  • в нём часто запускается Bootstrapper или ApplicationRunner.

Рекомендация: не помещать в Main бизнес-логику. Его задача — запустить систему, а не выполнять её функции. Даже CLI-параметры лучше передавать в отдельный CommandLineParser, а не обрабатывать напрямую в main().

В микросервисах Main может быть минимальным — просто вызовом SpringApplication.run() или Host.CreateDefaultBuilder().Build().Run() — и это корректно: фреймворк берёт на себя оркестрацию.


Controller — маршрутизатор запросов

Контроллер — это компонент уровня представления (Presentation Layer), принимающий входящие запросы (HTTP, gRPC, WebSocket) и координирующий их обработку. Он делегирует логику сервисам и преобразует результаты в формат ответа.

Типичные обязанности:

  • десериализация входных данных (JSON → DTO);
  • валидация (через аннотации или вызов Validator);
  • вызов соответствующего Service или UseCase;
  • обработка исключений (перехват ServiceException400 Bad Request);
  • сериализация результата (DTO → JSON).

Пример сигнатуры в REST:

[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
{
var result = await _userService.RegisterAsync(request);
return result.Match(
success => CreatedAtAction(nameof(GetUser), new { id = success.Id }, success),
failure => BadRequest(failure.Errors)
);
}

Антипаттерны:

  • логика валидации внутри контроллера (следует выносить в Validator или Specification);
  • прямой вызов Repository из контроллера (нарушение слоистой архитектуры);
  • возврат сущностей напрямую (UserEntity), а не DTO (смешение слоёв, утечка деталей).

В MVC/MVVM/MVP контроллер — это координатор. Его имя должно отражать сущность (UserController), а не действие (LoginController — избыточно, если внутри только login() и logout(); лучше AuthController).


ServiceImpl — реализация бизнес-логики

ServiceImpl (или просто Service, если интерфейс не выделяется явно) — это ядро бизнес-логики. Он инкапсулирует правила предметной области, оркестрирует взаимодействие сущностей, репозиториев, внешних сервисов и обеспечивает согласованность операций.

Особенности:

  • работает с доменными объектами (User, Order), а не с DTO;
  • транзакционность: методы часто помечаются как @Transactional (Spring) или оборачиваются в Unit of Work;
  • не зависит от UI, HTTP, базы данных — только от интерфейсов (UserRepository, NotificationPort);
  • может вызывать другие Service (например, OrderServiceInventoryService), но циклические зависимости — сигнал тревоги.

Пример:

public OrderConfirmation placeOrder(CreateOrderCommand cmd) {
User user = userRepository.findById(cmd.userId())
.orElseThrow(() -> new UserNotFoundException(cmd.userId()));

if (!user.isActive())
throw new UserNotActiveException();

Order order = Order.create(cmd.items(), user);
orderRepository.save(order);

notificationService.sendOrderPlaced(user, order);
return new OrderConfirmation(order.getId(), order.getTotal());
}

ServiceImpl — не хранилище утилит. Если метод не выражает бизнес-операцию, а лишь «делает что-то полезное» (calculateDiscount(), formatAddress()), это признак того, что логика должна быть частью сущности или Value Object.


RepositoryImpl — реализация доступа к данным

RepositoryImpl — конкретная реализация Repository Interface, отвечающая за персистентность агрегатов. Она знает, как сохранить/загрузить объект в конкретном хранилище (PostgreSQL, MongoDB, файловая система).

Ключевые принципы:

  • работает с агрегатами целиком, а не с отдельными полями;
  • не возвращает прокси или lazy-сущности — объект должен быть готов к использованию после findById();
  • не содержит бизнес-логики («найти всех активных пользователей» — допустимо; «найти пользователей, у которых баланс > 1000 и статус = ACTIVE» — переносится в Specification или Service);
  • не зависит от фреймворков ORM напрямую — лучше обернуть JpaRepository или DbContext в собственный класс.

Пример (на псевдокоде):

public class PostgresUserRepository : IUserRepository
{
private readonly IDbConnection _db;

public User? FindById(Guid id)
{
// Запрос JOIN users, user_profiles, user_settings
// Маппинг в User (с Address, Preferences как Value Objects)
// Возврат полного агрегата
}

public void Save(User user)
{
// BEGIN TX
// INSERT/UPDATE users
// INSERT/UPDATE user_profiles
// INSERT/UPDATE user_settings
// COMMIT
}
}

Антипаттерн: «репозиторий как обёртка над ORM» с методами GetAll(), GetByField(), DeleteById() — это DAO, а не репозиторий. Репозиторий должен говорить на языке домена.


DAOImpl — реализация технического доступа

DAOImpl ближе к инфраструктуре, чем RepositoryImpl. Он фокусируется на маппинге таблица ↔ объект и часто:

  • работает с «плоскими» DTO (UserRecord);
  • содержит SQL-запросы или вызовы ORM напрямую;
  • может быть сгенерирован (например, MyBatis Generator, JPA Metamodel);
  • используется в legacy-системах или в микросервисах с простой моделью.

В современных архитектурах DAO часто скрывается внутри RepositoryImpl как приватное поле:
PostgresUserRepository содержит UserRecordDao, но клиенты видят только IUserRepository.


Validator — проверка корректности

Validator — класс, ответственный за проверку входных данных на соответствие правилам. Он может работать на разных уровнях:

  • DTO-валидация (поля не null, email формат, длина строки);
  • бизнес-валидация («пользователь с таким email уже существует», «баланс недостаточен»);
  • контекстная валидация («если тип оплаты = CRYPTO, то кошелёк обязателен»).

Реализации:

  • статический валидатор (EmailValidator.isValid(email)) — для простых правил;
  • инстанцируемый валидатор (OrderValidator.validate(order)) — для правил, требующих зависимостей (userRepository);
  • спецификации (new UniqueEmailSpecification(userRepository).isSatisfiedBy(email)) — для составных и переиспользуемых правил.

Валидация — это не исключение. Validator должен возвращать ValidationResult с коллекцией ошибок, а не выбрасывать ValidationException при первой проблеме. Это позволяет клиенту увидеть все нарушения сразу.

Пример:

public class OrderValidator {
private final InventoryService inventory;

public ValidationResult validate(Order order) {
var errors = new ArrayList<String>();

if (order.getItems().isEmpty())
errors.add("Order must contain at least one item");

for (var item : order.getItems()) {
var stock = inventory.getStock(item.productId());
if (stock < item.quantity())
errors.add("Insufficient stock for " + item.productId());
}

return new ValidationResult(errors.isEmpty(), errors);
}
}

Logger — запись событий выполнения

Logger — фасад над системой логгирования (Log4j, Serilog, Winston). Его задача — обеспечить единый способ записи сообщений с уровнями (DEBUG, INFO, WARN, ERROR), контекстом (идентификатор запроса, пользователь) и структурированными данными.

Хороший логгер:

  • не пишет в stdout напрямую, а использует абстракцию (ILogger);
  • поддерживает структурное логгирование (JSON-поля: {"userId":"...", "orderId":"...", "durationMs":123});
  • избегает чувствительных данных (password, cardNumber);
  • позволяет фильтровать по уровню и источнику.

Пример:

_logger.LogInformation("Order {OrderId} created for user {UserId} in {DurationMs}ms", 
order.Id, user.Id, stopwatch.ElapsedMilliseconds);

Антипаттерн: Console.WriteLine("Step 1 done") — это не логгирование, это отладочный вывод, не подлежащий настройке и фильтрации.


ExceptionHandler — централизованная обработка ошибок

ExceptionHandler перехватывает исключения на уровне приложения и преобразует их в понятные клиенту ответы. Он:

  • изолирует детали реализации (стек-трейс не должен уходить клиенту);
  • различает типы ошибок:
    • ValidationException400 Bad Request,
    • EntityNotFoundException404 Not Found,
    • UnauthorizedException401 Unauthorized,
    • ServiceUnavailableException503 Service Unavailable;
  • логирует ошибки с контекстом (запрос, пользователь, окружение).

В Spring — @ControllerAdvice; в ASP.NET Core — IExceptionHandler; в Node.js — middleware app.use((err, req, res, next) => ...).

Пример:

@ExceptionHandler(InsufficientFundsException.class)
public ResponseEntity<ApiError> handleInsufficientFunds(InsufficientFundsException ex) {
logger.warn("Payment failed: insufficient funds for user {}", ex.getUserId());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ApiError("INSUFFICIENT_FUNDS", "Balance too low"));
}

ErrorHandler — системная обработка сбоев

В отличие от ExceptionHandler, который работает на уровне приложения, ErrorHandler отвечает за инфраструктурные ошибки:

  • сбои в инициализации DI-контейнера,
  • ошибки подключения к БД при старте,
  • исчерпание памяти,
  • аварийное завершение (SIGTERM, SIGINT).

Он запускается до или вне основного жизненного цикла и обеспечивает graceful shutdown:

  • остановка приёма новых запросов,
  • завершение текущих операций,
  • закрытие соединений,
  • запись диагностической информации.

В Java — обработчики Runtime.getRuntime().addShutdownHook(); в .NET — IHostApplicationLifetime.ApplicationStopping.


Configurator / Config — управление настройками

Configurator (или Configuration) загружает параметры из внешних источников (файлы, переменные окружения, Vault, Consul) и предоставляет их приложению в виде типизированного объекта.

Принципы:

  • неизменяемость после инициализации;
  • проверка обязательных параметров на старте («если DATABASE_URL не задан — падать сразу»);
  • поддержка профилей (dev, prod);
  • отказоустойчивость (значения по умолчанию, fallback-источники).

Пример:

public class AppConfig
{
public string DatabaseUrl { get; }
public int CacheTtlSeconds { get; } = 300; // значение по умолчанию

public AppConfig(IConfiguration config)
{
DatabaseUrl = config["DATABASE_URL"]
?? throw new InvalidOperationException("DATABASE_URL is required");
CacheTtlSeconds = config.GetValue<int>("CACHE_TTL_SECONDS", 300);
}
}

Антипаттерн: Environment.GetEnvironmentVariable("KEY") внутри бизнес-логики — это нарушение инверсии зависимостей.


Router — маршрутизация запросов

Router определяет, какой обработчик должен получить запрос в зависимости от пути, метода, заголовков. Он:

  • строит дерево маршрутов (часто trie для эффективности);
  • поддерживает параметры (/users/{id});
  • может включать middleware (аутентификация, логгирование);
  • изолирует логику маршрутизации от контроллеров.

В Express.js — app.get('/users', controller.list); в ASP.NET Core — endpoints.MapControllers(); в Spring — @RequestMapping.

Хороший роутер — декларативный: правила задаются в одном месте, без императивной логики (if (path == "/login") handleLogin()).


RequestHandler и ResponseBuilder — обработка запроса и формирование ответа

RequestHandler — это обобщённый компонент, который:

  • парсит запрос (тело, заголовки, query-параметры);
  • проверяет подлинность и права;
  • передаёт управление Controller или Service;
  • перехватывает исключения.

ResponseBuilder, напротив — отвечает за формирование ответа:

  • установку статус-кода;
  • заголовков (Content-Type, Cache-Control);
  • сериализацию тела;
  • добавление кросс-доменных заголовков (CORS).

В REST API они часто интегрированы во фреймворк, но при построении custom-протоколов (например, бинарный TCP-протокол) их выделяют явно.


User, AuthService, Authenticator, Authorizer, TokenManager, JwtProvider — безопасность

Эти классы образуют цепочку аутентификации и авторизации:

  1. User — доменная сущность, содержащая:

    • идентификатор,
    • учётные данные (хэш пароля, не сам пароль),
    • права (roles, permissions),
    • атрибуты безопасности (lastLogin, isLocked).
  2. Authenticator — проверяет подлинность:

    • authenticate(username, password) → либо User, либо исключение;
    • использует PasswordHasher для сравнения хэшей.
  3. Authorizer — проверяет полномочия:

    • isAuthorized(user, resource, action)true/false;
    • может опираться на RBAC (роли), ABAC (атрибуты), ACL.
  4. AuthService — оркестратор:

    • вызывает Authenticator,
    • генерирует сессию/токен через TokenManager,
    • записывает аудит-лог.
  5. TokenManager — управляет жизненным циклом токенов:

    • issue(user) → token,
    • revoke(token),
    • refresh(token),
    • хранит токены в Redis/БД для отзывов.
  6. JwtProvider — конкретная реализация для JWT:

    • подписывает токены секретом или приватным ключом,
    • проверяет подпись и срок действия,
    • извлекает claims (sub, roles).

Важно: User не должен знать о JWT или сессиях — это инфраструктурные детали. Аутентификация и авторизация — сквозная задача, вынесенная в middleware.


DatabaseSeeder, Migrator — управление схемой и данными

  • DatabaseSeeder заполняет БД справочными или тестовыми данными:

    • страны, валюты, статусы заказов;
    • демо-пользователи;
    • запускается при развёртывании или в тестах.
  • Migrator управляет эволюцией схемы:

    • up() — применяет изменения (CREATE TABLE, ALTER COLUMN);
    • down() — откатывает;
    • отслеживает применённые миграции в служебной таблице (schema_migrations).

Инструменты: Flyway, Liquibase, Entity Framework Migrations.
Правило: миграции должны быть идемпотентными и атомарными.


Scheduler, TaskRunner — фоновые задачи

  • Scheduler запускает задачи по расписанию (cron):

    • ежедневная агрегация метрик,
    • очистка кэша,
    • отправка отчётов.
  • TaskRunner выполняет асинхронные операции:

    • обработка очереди (RabbitMQ, Kafka),
    • тяжёлые вычисления (рендер PDF),
    • повторные попытки (retry with backoff).

Ключевые требования:

  • идемпотентность (задача может быть запущена дважды),
  • трекинг состояния (в процессе / завершена / ошибка),
  • приоритезация.

Notifier, Mailer, SmsSender — уведомления

Notifier — фасад над каналами уведомлений:

  • notify(user, event) → выбирает канал (email, push, SMS) по предпочтениям пользователя.

Mailer и SmsSender — конкретные реализации:

  • интеграция с SendGrid, AWS SES, Twilio;
  • шаблонизация (Handlebars, Jinja2);
  • обработка ошибок («если email не дошёл — логировать, но не падать»).

Антипаттерн: отправка уведомлений синхронно в бизнес-методе — это замедляет ответ и снижает отказоустойчивость. Лучше публиковать событие UserRegisteredEvent, а отправку делать в фоне.


EventDispatcher, EventListener, MessageProducer, MessageConsumer — событийная архитектура

  • EventDispatcher — центральный шина:

    • dispatch(event) → находит всех подписчиков и вызывает их.
  • EventListener — реакция на событие:

    • @EventListener OrderPaidListener.handle(OrderPaidEvent e).
  • MessageProducer — отправка в внешнюю очередь:

    • kafkaTemplate.send("orders", event).
  • MessageConsumer — получение и обработка:

    • @KafkaListener(topics = "orders") void onOrderPaid(OrderPaidEvent e).

Разница:

  • EventDispatcher/EventListenerвнутрипроцессные события (в памяти);
  • MessageProducer/Consumerмежпроцессные (через брокер).

Важно: события должны быть иммутабельными и сериализуемыми.


CacheManager, HttpClient, Integrator — интеграции

  • CacheManager — абстракция над кэшем (Redis, Memcached):

    • get(key), set(key, value, ttl), invalidate(key);
    • стратегии: cache-aside, write-through.
  • HttpClient — обёртка над HTTP-клиентом с:

    • retry-логикой,
    • таймаутами,
    • логгированием запросов,
    • трассировкой (trace-id propagation).
  • Integrator — компонент, объединяющий несколько вызовов внешних API в одну операцию:

    • «получить курс валют из ЦБ + конвертировать сумму»;
    • изолирует детали интеграции от бизнес-логики.

Parser, Formatter, Serializer, Deserializer, Transformer, Mapper, Converter — преобразование данных

Эти классы образуют конвейер преобразований:

КлассНазначениеПример
ParserРазбор «сырых» данных → структурированныхJsonParser.parse(jsonString) → JsonObject
FormatterПодготовка данных для выводаDateTimeFormatter.format(date) → "2025-11-20"
SerializerОбъект → сериализуемый форматJsonSerializer.serialize(user) → "{...}"
DeserializerСериализуемый формат → объектJsonDeserializer.deserialize(json, User.class)
TransformerИзменение структуры/семантикиOrder → OrderSummary (удалить детали платёжной системы)
MapperКопирование между слоямиUserEntity → UserDto (точное соответствие полей)
ConverterКонвертация форматовCsvConverter.toJson(csvString)

Ключевое различие:

  • Mapper — «один к одному», без логики;
  • Transformer — может включать вычисления (fullName = firstName + " " + lastName);
  • Converter — работает с форматами, а не с объектами.

Loader, Saver, Exporter, Importer, Aggregator, Analyzer, Calculator, Processor — работа с данными

  • Loader — загрузка из источника: файл, БД, API.
  • Saver — сохранение (не обязательно сразу — может буферизовать).
  • Exporter — выгрузка в формат (PDF, Excel) с учётом требований (шаблоны, стили).
  • Importer — импорт с валидацией и маппингом.
  • Aggregator — сбор данных из нескольких источников (например, для дашборда).
  • Analyzer — извлечение инсайтов («пик продаж в 18:00»).
  • Calculator — вычисления (налоги, скидки, рейтинги).
  • Processor — обработка потока («обработать 1000 платежей»).

Принцип: каждый класс должен делать одну вещь, но делать её хорошо. PaymentProcessor не должен одновременно и считать комиссию, и отправлять уведомление — лучше разделить на CommissionCalculator и NotificationService.


Scanner, ScannerWorker, Monitor, Watcher, Poller — наблюдение

  • Scanner — однократный анализ («просканировать файл на вирусы»).
  • ScannerWorker — фоновая версия (пул потоков для параллельной проверки).
  • Monitor — проверка здоровья системы («доступна ли БД?»).
  • Watcher — реакция на изменения («файл изменился → перезагрузить конфиг»).
  • Poller — периодический опрос («раз в 5 секунд проверять статус задачи»).

Важно: Watcher использует события ОС (inotify, kqueue), а Poller — таймеры. Первый эффективнее, но не везде доступен.


Bootstrapper, Registry, Locator, Container, Injector — управление зависимостями

  • Bootstrapper — инициализация приложения: конфиг, логгер, DI.
  • Registry — карта «имя → экземпляр» (устаревший паттерн, лучше DI).
  • Locator — Service Locator (антипаттерн в большинстве случаев — нарушает инверсию зависимостей).
  • Container — DI-контейнер (Spring, Autofac, Dagger). Управляет жизненным циклом (singleton, scoped, transient).
  • Injector — механизм внедрения (чаще всего часть контейнера).

Рекомендация: использовать конструкторное внедрение, а не ServiceLocator.GetService(). Это делает зависимости явными и упрощает тестирование.


Factory, SimpleFactory, Singleton, Proxy, Decorator, Adapter, Facade — структурные и порождающие паттерны

  • Factory — создаёт объекты с логикой (выбор типа, инициализация).
  • SimpleFactory — статический метод (DocumentFactory.create(type)), но нарушает OCP.
  • Singleton — гарантирует один экземпляр. Осторожно: глобальное состояние усложняет тестирование.
  • Proxy — заместитель (lazy loading, access control, logging).
  • Decorator — добавление функциональности («зашифровать поток»: EncryptingOutputStream вокруг FileOutputStream).
  • Adapter — совместимость интерфейсов (уже рассмотрен в абстрактной части).
  • Facade — упрощение сложной подсистемы (PaymentFacade.process(order) вместо ручного вызова auth → capture → notify).

UnitOfWork, Entity, ValueObject, AggregateRoot — DDD-реализации

  • UnitOfWork — отслеживает изменения в объектах и сохраняет их атомарно:

    • registerDirty(entity),
    • commit() → вызывает Repository.save() для всех изменённых.
  • Entity, ValueObject, AggregateRoot — реализации доменных концепций (см. абстрактную часть).
    Пример ValueObject:

    public readonly record struct Money(decimal Amount, string Currency)
    {
    public static Money operator +(Money a, Money b)
    => a.Currency == b.Currency
    ? new Money(a.Amount + b.Amount, a.Currency)
    : throw new InvalidOperationException("Currencies must match");
    }

DTO, VO, POJO, BO, DAO, Model, ViewModel, Presenter, View, Component — слои представления

ТипУровеньНазначение
DTOПередачаДанные между процессами (сервер ↔ клиент)
VOДоменНеизменяемые значения (Money, Address)
POJO/Plain ObjectОбщийПростой объект без фреймворк-зависимостей
BOБизнесОбъект с логикой (устаревший термин, лучше Entity/ValueObject)
DAOДанныеТехнический доступ к БД
ModelДомен/UIДанные предметной области (в MVC) или UI-состояние (в MVVM)
ViewModelUIДанные, адаптированные под отображение (в MVVM)
PresenterUIЛогика взаимодействия View и Model (в MVP)
ViewUIОтображение (часто интерфейс)
ComponentUIСамостоятельный UI-блок (React/Vue/Angular)

Важно: не смешивать DTO и Entity. DTO — для передачи, Entity — для логики. Их структура может отличаться.


Helper, Utils, Extension, Wrapper — вспомогательные классы

  • Helper/Utils — статические методы общего назначения:

    • StringUtils.isNullOrEmpty(),
    • DateUtils.addDays().
      Но избегайте «мусорных» утилит (MagicHelper.doEverything()).
  • Extension — расширение функциональности без наследования:

    • C#: public static string ToTitleCase(this string s),
    • Kotlin: extension functions.
  • Wrapper — инкапсуляция стороннего объекта для:

    • упрощения интерфейса,
    • добавления логирования/валидации,
    • тестирования (mock wrapper, а не сторонний класс).

Правило: если метод можно выразить через существующие — не создавайте утилиту. Например, list.Any(x => x.IsActive) лучше ListUtils.hasActive(list).


Starter, Stopper, TestRunner, Mocker, Spy, Stub, Fixture, Spec, Scenario — жизненный цикл и тестирование

  • Starter/Stopper — управление запуском/остановкой (особенно в CLI-приложениях).
  • TestRunner — запуск тестов (JUnit, NUnit, pytest).
  • Mocker/Spy/Stub — заглушки:
    • Stub — возвращает фиксированные значения,
    • Mock — проверяет, как вызывался метод,
    • Spy — оборачивает реальный объект и логирует вызовы.
  • Fixture — подготовленные данные для тестов («пользователь с ролью ADMIN»).
  • Spec/Scenario — BDD-стиль: Feature: User registration, Scenario: Email already taken.

Driver, Client, Server, ConnectionManager, Pool, Buffer — низкоуровневые компоненты

  • Driver — управление устройством/протоколом (драйвер БД, драйвер GPIO).
  • Client/Server — сетевые компоненты.
  • ConnectionManager — управление подключениями (пулы, таймауты, reconnect).
  • Pool — повторное использование ресурсов (соединения, потоки, объекты).
  • Buffer — промежуточное хранение при IO (чтение блоками).

Interceptor — сквозная функциональность

Interceptor перехватывает вызовы для добавления:

  • логгирования (@LogExecutionTime),
  • безопасности (@PreAuthorize),
  • кэширования (@Cacheable),
  • метрик (@Timed).

Реализации: AOP (AspectJ, Spring AOP), middleware (ASP.NET, Express), декораторы (Python).