Баллада о компоненте

 

В последнее время веду много архитектурных дискуссий, и в их рамках часто возникает необходимость определить некоторые элементы в системе и задать правила взаимодействия этих элементов. Чаще всего это делается через картинку с квадратиками и стрелочками между ними. Однако за этими квадратиками и стрелочками часто кроется недосказанность, которая потом проявляется при разработке или даже при эксплуатации системы.

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

Вот она, баллада о компоненте

Что такое компонент?

Для начала определим, что такое собственно компонент, обычно я даю такую картинку:

компонент

Если говорить нестрого, то компонент - это некоторая сущность, имеющая некоторое назначение и решающая какую-то одну задачу. Если задач несколько - то скорее всего мы имеем дело с несколькими компонентами и надо подумать над декомпозицией. К компоненту, уже по определению, применяется принцип 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
	}
}

В данном случае

Зачем нам компонент?

С концепцией компонента разобрались, если будут вопросы - пишите, дополню статью. Теперь более насущный вопрос, а зачем нам компонент? Здесь надо сделать небольшое лирическое отступление.

Перед разработкой любой программной системы и при добавлении новой функциональности в уже имеющуюся, необходимо хорошо продумать что и как мы будем делать. Обычно я даже прошу написать что-то вроде 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). Но это впечатление обманчиво - это тоже компоненты.

Модуль как компонент

Как я уже писал выше, компоненты фрактальны, а поэтому применять мы их можем не только к классам, но и к модулям. Таким образом у нас может быть модуль (давайте например думать в SPM-модулях для iOS/Swift) Баристы BaristaModule, который имеет Package.swift и в этом модуле лежит собственно исходный код по тому, как готовить кофе. Однако, реализовывать например, подачу воды или определение сортов кофе внутри этого модуля было бы неправильно, потому что подача воды и сорта кофе - это вещи, которыми пользуется не только бариста.

Таким образом мы могли бы определить еще модули, которые мы могли бы рассмотреть с позиции, что модуль BaristaModule - это компонент