Баллада о компоненте
24.10.2025В последнее время веду много архитектурных дискуссий, и в их рамках часто возникает необходимость определить некоторые элементы в системе и задать правила взаимодействия этих элементов. Чаще всего это делается через картинку с квадратиками и стрелочками между ними. Однако за этими квадратиками и стрелочками часто кроется недосказанность, которая потом проявляется при разработке или даже при эксплуатации системы.
Поэтому важно думать об этих квадратиках каким-то образом, чтобы эти недосказанности видеть в процессе проектирования или даже использовать их в проектировании. В своей работе я часто пользуюсь абстракцией компонента и предлагаю ее окружающим. В ходе этих предложений приходится про компонент рассказывать детально. Поэтому я решил написать эту статью, в которую поместить все те мысли, которые я произношу, чтобы был материал, во-первых, написанный, а во-вторых, доступный для чтения желающим.
Вот она, баллада о компоненте
Что такое компонент?
Для начала определим, что такое собственно компонент, обычно я даю такую картинку:

Если говорить нестрого, то компонент - это некоторая сущность, имеющая некоторое назначение и решающая какую-то одну задачу. Если задач несколько - то скорее всего мы имеем дело с несколькими компонентами и надо подумать над декомпозицией. К компоненту, уже по определению, применяется принцип SRP.
Для того, чтобы обсуждение было более наглядным, возьмем пример. Будем рассматривать такой компонент, как бариста.
У баристы есть назначение - он готовит кофе.
Однако, чтобы выполнить свое предназначение, любому компоненту нужны связи с внешним миром (с другими компонентами). Можно выделить четыре вида связи:
- Входные данные - набор входных условий, на основании которых компонент делает свою работу. Для нашего примера с баристой - входными данными являются заказы клиентов.
- Выходные данные - набор выходных артефактов, которые являются результатом деятельности компонента. Для примера с баристой - это чашка кофе по конкретному заказу клиента.
- Зависимости - ресурсы, артефакты, данные, которые использует компонент, чтобы преобразовывать входные данные в выходные. Для примера с баристой - это зерна кофе, вода, электричество, молоко и т.д.
- Конфигурация - специальные управляющие воздействия на то, как компонент делает свою работу. Для примера с баристой - это, например, меню от руководства, задающее доступные для заказа и приготовления напитки (т.е. например, даже если у нас есть молоко для капучино, мы можем намеренно его не готовить).
Для большего понимания, рассмотрим пример кода с баристой:
struct ClientAsk {
enum CoffeeKinds {
case americano
case cappuccino
// the rest ...
}
let coffeeKind: CoffeeKinds
let withSugar: Bool
}
final class Barista {
private let dependencies: BaristaDependencies
private let menu: Menu
init(dependencies: BaristaDependencies, menu: Menu) {
self.dependencies = dependencies
self.menu = menu
}
func makeCoffee(ask: ClientAsk) -> Result<Cup, BaristaErrors> {
// use dependencies and menu to process client ask and make a cup of coffee
// or report and error
}
}
В данном случае
- Входные данные: ClientAsk, сущность, описывающая заказ клиента. Это то, что принимает компонент для одной итерации своей работы.
- Выходные данные: Result<Cup, Error>, тут стоит отметить важный факт, что результатом работы компонента может быть:
- успешное выполнение своих обязанностей, и тогда результатом этого будет некоторый артефакт (в нашем случае Cup) этой работы
- или сигнал о том, что работа в обычном виде выполнена быть не может (в нашем случае BaristaErrors), артефакт, описывающий причины того, почему работа не может быть сделана или почему не удалось ее выполнить (важно не забывать про эту часть)
- Зависимости: Dependencies, тут все что необходимо для приготовления чашечки кофе из заказа клиента
- Конфигурация: Menu, описывает виды кофе, которые можно сегодня приготовить
Зачем нам компонент?
С концепцией компонента разобрались, если будут вопросы - пишите, дополню статью. Теперь более насущный вопрос, а зачем нам компонент? Здесь надо сделать небольшое лирическое отступление.
Перед разработкой любой программной системы и при добавлении новой функциональности в уже имеющуюся, необходимо хорошо продумать что и как мы будем делать. Обычно я даже прошу написать что-то вроде RFC, в котором было бы явно описано что и как. Но чтобы разобраться что и как мы хотим делать (и даже написать RFC), необходимо к этому продумыванию как-то подойти. Декомпозировать задачу (которая, на самом деле, редко бывает сформулирована формально) на составляющие и обозначить между ними связи. А если система уже имеется, то еще и “прицепить” новую “запчасть” к тем что уже есть, ничего не забыть и сделать это правильно. И если мы будем мыслить систему в виде взаимодействующих друг с другом компонентов, то у нас будет хорошее подспорье к тому, чтобы задачу проектирования решить успешно.
К тому же, компоненты - фрактальны. Т.е. можно сделать несколько уровней не только в контексте кто кому предоставляет конфигурацию и зависимости, но и кто из кого состоит. Таким образом на одном уровне абстракции компонент может быть одним кубиком, а на другом - графом взаимодействующих друг с другом более низкоуровневых компонент.
Вернемся к нашему баристе, для пользователя это своего рода черных ящик - мы ему заказ, он нам чашечку кофе (или ошибку, не забываем!). Однако, если вы как клиент постоите и посмотрите на работу баристы, то заметите, что чашка кофе - результат последовательности действий. И таким образом компонент “Бариста” состоял бы из сети компонентов, в который входили бы: кофемолка, кофемашина, мойка для чашей, и т.д.
Поэтому компонент дает нам удобную абстракцию для анализа систем и синтеза решений.
Класс как компонент
Вооружившись абстракцией компонента, давайте посмотрим на привычные конструкции из программирования. Любой класс можно рассматривать как компонент. Вернемся к примеру с баристой выше
struct ClientAsk {
enum CoffeeKinds {
case americano
case capuccino
// the rest ...
}
let coffeeKind: CoffeeKinds
let withSugar: Bool
}
protocol BaristaDependencies {
var grinder: Grinder { get }
var coffeeMachine: CoffeeMachine { get }
var waterTap: WaterTap { get }
// ...
}
protocol Menu {
var isCappuccinoAllowed: Bool { get }
// ...
}
final class Barista {
private let dependencies: BaristaDependencies
private let menu: Menu
init(dependencies: BaristaDependencies, menu: Menu) {
self.dependencies = dependencies
self.menu = menu
}
func makeCoffee(ask: ClientAsk) -> Result<Cup, BaristaErrors> {
// use dependencies and menu to process client ask and make a cup of coffee
// or report and error
}
}
Как я уже выше писал - это типичный компонент. Но давайте посмотрим на другие входящие в пример вещи. Является ли ClientAsk компонентом? А Cup? А BaristaDependencies? А Menu?
Интуиция подсказывает что скорее нет, ведь это всего лишь DTO (Data Transfer Object). Но это впечатление обманчиво - это тоже компоненты.
- ClientAsk и Cup это действительно DTO, объекты, чье единственное предназначение - перенос данных. Таким образом их входные и выходные данные хранятся напрямую в их полях и идентичны. Хотя порой такое бывает не всегда, и даже простые DTO могут на вход получать данные в одном виде, а на выходе давать в другом.
- BaristaDependencies как мы видим лишь контракт, который может скрывать в себе как просто прокси доступ к нужным объектам (про DI как-нибудь поразмышляю отдельно), так и какую-нибудь замысловатую логику получения этих объектов. На этом уровне контракт компонентом не является, но. Во-первых, является на другом, понятийном уровне, задача такого компонента - скрывать от пользователя реализацию получения зависимостей. Во-вторых, компонентом однозначно является реализация этого контракта.
- Menu является с одной стороны вариацией DTO’шки, с чуть более закрытым контрактом чем у ClientAsk например, а с другой стороны контрактом, который скрывает логику получения настроек (а она может быть весьма замысловатой). Как и BaristaDependencies, Menu является компонентом на понятийном уровне (если бы мы рисовали схемку, то Menu был бы квадратиком над стрелочкой), и реализация этого контракта также будет компонентом.
Модуль как компонент
Как я уже писал выше, компоненты фрактальны, а поэтому применять мы их можем не только к классам, но и к модулям. Таким образом у нас может быть модуль (давайте например думать в SPM-модулях для iOS/Swift) Баристы BaristaModule, который имеет Package.swift и в этом модуле лежит собственно исходный код по тому, как готовить кофе. Однако, реализовывать например, подачу воды или определение сортов кофе внутри этого модуля было бы неправильно, потому что подача воды и сорта кофе - это вещи, которыми пользуется не только бариста.
Таким образом мы могли бы определить еще модули, которые мы могли бы рассмотреть с позиции, что модуль BaristaModule - это компонент
- Входные данные: нам нужен способ получать заказы клиентов, поэтому входными данными может быть модуль заказов (AskModule), в котором определяются способы сделать заказ и формализуются для передачи баристе
- Выходные данные: нам нужен способ передать чашку клиенту, и здесь может быть модуль, который непосредственно доставляет чашку, например OfficientModule. К слову на вход и выход может работать один и тот же модуль официанта.
- Зависимости: как уже писал выше - это все что необходимо для баристы для работы, поэтому сюда все модули, которые предоставляют функционал баристе (WaterTap, MilkProvider, CoffeeMachine Shop, …).
- Конфигурация: тут модуль управления, например дирекция кофе-буфетов.