Когда стоит использовать агрегацию?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Когда стоит использовать агрегацию (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
Заключение
Агрегация — более гибкий способ организации кода, чем наследование. Она позволяет легче тестировать, переиспользовать код и менять реализацию. Главное правило: если неуверен, используй агрегацию. Наследование должно быть исключением, а не правилом.