← Назад к вопросам

Когда стоит использовать агрегацию?

1.0 Junior🔥 141 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Когда стоит использовать агрегацию (Composition)

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

Основной принцип

Агрегация означает: "A has B" (A содержит B). Это отличается от наследования "A is B" (A является B).

// Наследование (IS-A)
class Dog : Animal() {
    fun bark() {}
}

// Агрегация (HAS-A)
class Dog {
    val animal = Animal()  // содержит Animal
    fun bark() {}
}

Когда использовать агрегацию

1. Повторное использование функциональности

Если нужно использовать функцию разных классов, но они не связаны наследованием:

// Плохо - неправильное наследование
class Car : Engine() {  // Car НЕ является Engine!
    fun drive() {}
}

// Правильно - агрегация
class Car {
    val engine = Engine()  // Car содержит Engine
    fun drive() {
        engine.start()
    }
}

class Motorcycle {
    val engine = Engine()  // Motorcycle тоже содержит Engine
    fun ride() {
        engine.start()
    }
}

2. Гибкость и подстановка

Агрегация с интерфейсами позволяет легко менять реализацию:

interface Storage {
    fun save(data: String)
    fun load(): String
}

class FileStorage : Storage {
    override fun save(data: String) { /* файловая система */ }
    override fun load(): String { /* чтение с диска */ }
}

class DatabaseStorage : Storage {
    override fun save(data: String) { /* БД */ }
    override fun load(): String { /* из БД */ }
}

class UserRepository(val storage: Storage) {  // агрегация
    fun saveUser(user: User) {
        storage.save(user.toJson())
    }
}

// Легко менять реализацию
val repo1 = UserRepository(FileStorage())
val repo2 = UserRepository(DatabaseStorage())

3. Android: Dependency Injection

В Android очень часто используется агрегация с DI для управления зависимостями:

class UserViewModel(
    private val repository: UserRepository,
    private val logger: Logger,
    private val analytics: Analytics
) : ViewModel() {
    // Все зависимости инъектируются, не созданы в ViewModel
    fun loadUsers() {
        logger.info("Loading users")
        val users = repository.getUsers()
        analytics.trackEvent("users_loaded")
    }
}

// С Hilt
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository,
    private val logger: Logger
) : ViewModel()

4. Single Responsibility Principle (SRP)

Агрегация помогает разделить ответственность:

// Плохо - одна большая ответственность
class UserManager {
    fun createUser() { /* логика */ }
    fun validateEmail() { /* логика */ }
    fun sendNotification() { /* логика */ }
    fun saveToDatabase() { /* логика */ }
}

// Правильно - разделённая ответственность
class EmailValidator
class NotificationService
class UserRepository

class UserManager(
    val validator: EmailValidator,
    val notificationService: NotificationService,
    val repository: UserRepository
) {
    fun createUser(email: String) {
        validator.validate(email)  // делегирует
        repository.save(User(email))  // делегирует
        notificationService.send(email)  // делегирует
    }
}

Агрегация vs Наследование

Пример с неправильным наследованием

// Плохо - Square НЕ является Rectangle!
class Rectangle {
    var width: Int = 0
    var height: Int = 0
    fun area() = width * height
}

class Square : Rectangle() {
    override fun area() = width * width
}

// Проблема: Square может иметь разные width и height
val square = Square()
square.width = 5
square.height = 10
println(square.area())  // 50, но квадрат имеет разные стороны!

Правильное решение через агрегацию:

interface Shape {
    fun area(): Int
}

class Rectangle(val width: Int, val height: Int) : Shape {
    override fun area() = width * height
}

class Square(val side: Int) : Shape {
    override fun area() = side * side
}

// Теперь контракт соблюдается
val square = Square(5)
println(square.area())  // 25

Шаблон Strategy через агрегацию

Агрегация идеальна для паттерна Strategy:

interface SortingStrategy {
    fun sort(list: List<Int>): List<Int>
}

class QuickSort : SortingStrategy {
    override fun sort(list: List<Int>): List<Int> { /* реализация */ }
}

class MergeSort : SortingStrategy {
    override fun sort(list: List<Int>): List<Int> { /* реализация */ }
}

class DataProcessor(
    val sortingStrategy: SortingStrategy  // агрегация стратегии
) {
    fun process(data: List<Int>): List<Int> {
        return sortingStrategy.sort(data)
    }
}

// Легко менять стратегию
val processor1 = DataProcessor(QuickSort())
val processor2 = DataProcessor(MergeSort())

Агрегация в Android Architecture

// Clean Architecture: слои взаимодействуют через агрегацию

// Domain
interface UserRepository {
    suspend fun getUsers(): List<User>
}

// Application
class GetUsersUseCase(val repository: UserRepository) {
    suspend operator fun invoke(): List<User> {
        return repository.getUsers()
    }
}

// Presentation
class UserViewModel(
    val useCase: GetUsersUseCase
) : ViewModel() {
    val users = MutableLiveData<List<User>>()
    
    fun loadUsers() {
        viewModelScope.launch {
            users.value = useCase()
        }
    }
}

// Каждый слой содержит (агрегирует) компоненты из нижних слоёв

Когда наследование ВСЕ-ТАКИ правильно

Наследование используется когда есть чёткое отношение IS-A и единственное наследование:

// Правильное наследование
abstract class Vehicle {
    abstract fun start()
}

class Car : Vehicle() {
    override fun start() { println("Car starts") }
}

class Motorcycle : Vehicle() {
    override fun start() { println("Motorcycle starts") }
}

// Car IS-A Vehicle - отношение верно

Практический совет

Правило композиции (Composition over Inheritance):

  • Сначала используй агрегацию и интерфейсы
  • Наследование только когда есть явное IS-A отношение
  • Prefer composition to inheritance

Заключение

Агрегация — более гибкий способ организации кода, чем наследование. Она позволяет легче тестировать, переиспользовать код и менять реализацию. Главное правило: если неуверен, используй агрегацию. Наследование должно быть исключением, а не правилом.