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 три фундаментальных абстракции:
-
Entity — объект с уникальной идентичностью, которая сохраняется в течение жизненного цикла (даже если атрибуты меняются).
Интерфейс может задаватьgetId(),equals()по ID. -
Value Object — объект, идентифицируемый только по значениям полей. Не имеет ID, неизменяем, может быть использован как аргумент или возвращаемое значение.
Примеры:Money(amount, currency),Address(street, city, zip).
Интерфейс часто не нужен — достаточно соглашения, но в строгих системах может бытьValueObject<T>сequals(),hashCode(). -
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; - обработка исключений (перехват
ServiceException→400 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(например,OrderService→InventoryService), но циклические зависимости — сигнал тревоги.
Пример:
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 перехватывает исключения на уровне приложения и преобразует их в понятные клиенту ответы. Он:
- изолирует детали реализации (стек-трейс не должен уходить клиенту);
- различает типы ошибок:
ValidationException→400 Bad Request,EntityNotFoundException→404 Not Found,UnauthorizedException→401 Unauthorized,ServiceUnavailableException→503 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 — безопасность
Эти классы образуют цепочку аутентификации и авторизации:
-
User — доменная сущность, содержащая:
- идентификатор,
- учётные данные (хэш пароля, не сам пароль),
- права (
roles,permissions), - атрибуты безопасности (
lastLogin,isLocked).
-
Authenticator — проверяет подлинность:
authenticate(username, password)→ либоUser, либо исключение;- использует
PasswordHasherдля сравнения хэшей.
-
Authorizer — проверяет полномочия:
isAuthorized(user, resource, action)→true/false;- может опираться на RBAC (роли), ABAC (атрибуты), ACL.
-
AuthService — оркестратор:
- вызывает
Authenticator, - генерирует сессию/токен через
TokenManager, - записывает аудит-лог.
- вызывает
-
TokenManager — управляет жизненным циклом токенов:
issue(user) → token,revoke(token),refresh(token),- хранит токены в Redis/БД для отзывов.
-
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) |
| ViewModel | UI | Данные, адаптированные под отображение (в MVVM) |
| Presenter | UI | Логика взаимодействия View и Model (в MVP) |
| View | UI | Отображение (часто интерфейс) |
| Component | UI | Самостоятельный 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.
- C#:
-
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).