Опишите Clean Architecture. В чём её основные принципы и преимущества?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Clean Architecture в Android
Что такое Clean Architecture
Clean Architecture — это архитектурный паттерн, предложенный Robert C. Martin (Uncle Bob), который структурирует приложение в слои, отделяя бизнес-логику от деталей реализации (UI, БД, сеть).
Основная идея: зависимости должны идти только внутрь. Внешние слои зависят от внутренних, но не наоборот.
Слои Clean Architecture
1. Entities (Domain Logic)
Самый внутренний слой — содержит бизнес-логику, независимую от фреймворка.
// Entities не зависят от Android, Retrofit, Room и т.д.
data class User(
val id: Int,
val name: String,
val email: String
) {
fun isValidEmail(): Boolean = email.contains("@")
fun isAdult(): Boolean = age >= 18
}
data class Post(
val id: Int,
val title: String,
val content: String,
val authorId: Int,
val likes: Int = 0
) {
fun canBeEdited(userId: Int): Boolean = authorId == userId
fun isPopular(): Boolean = likes > 1000
}
Характеристики:
- Чистые POJO/data class без зависимостей
- Содержат только бизнес-правила
- Переиспользуются в разных частях приложения
- Легко тестируются (no mocks)
2. Use Cases (Application Business Rules)
Второй слой — содержит бизнес-правила конкретного приложения, не зависит от UI/DB/Network деталей.
// Интерфейс (Input Port)
interface FetchUserUseCase {
suspend operator fun invoke(userId: Int): Result<User>
}
// Реализация
class FetchUserUseCaseImpl(
private val userRepository: UserRepository // Зависит от интерфейса, не реализации!
) : FetchUserUseCase {
override suspend fun invoke(userId: Int): Result<User> = try {
val user = userRepository.getUser(userId)
Result.Success(user)
} catch (e: Exception) {
Result.Error(e)
}
}
// Ещё пример
interface CreatePostUseCase {
suspend operator fun invoke(title: String, content: String): Result<Post>
}
class CreatePostUseCaseImpl(
private val postRepository: PostRepository,
private val authService: AuthService
) : CreatePostUseCase {
override suspend fun invoke(title: String, content: String): Result<Post> = try {
val currentUser = authService.getCurrentUser()
?: return Result.Error(Exception("Not authenticated"))
val post = Post(
id = UUID.randomUUID(),
title = title,
content = content,
authorId = currentUser.id
)
postRepository.save(post)
Result.Success(post)
} catch (e: Exception) {
Result.Error(e)
}
}
Характеристики:
- Содержат бизнес-логику приложения
- Используют интерфейсы Repository, Services (не их реализации)
- Не знают про Android, UI, БД
- Suspend/Flow для асинхронности
- Основная логика для unit тестирования
3. Interface Adapters (Gateways, Controllers, Presenters)
Третий слой — адаптирует данные между Use Cases и внешними деталями.
// Repository интерфейс (Interface Adapter)
interface UserRepository {
suspend fun getUser(id: Int): User
suspend fun saveUser(user: User)
fun observeUser(id: Int): Flow<User>
}
// Реализация (зависит от Room/Retrofit)
class UserRepositoryImpl(
private val userApi: UserApi, // Retrofit
private val userDao: UserDao // Room
) : UserRepository {
override suspend fun getUser(id: Int): User {
val remoteUser = try {
userApi.fetchUser(id) // Получить с сервера
} catch (e: Exception) {
userDao.getUser(id) // Fallback на локальный кеш
}
userDao.insert(remoteUser) // Сохранить локально
return remoteUser
}
override suspend fun saveUser(user: User) {
userApi.updateUser(user) // Отправить на сервер
userDao.insert(user) // Сохранить локально
}
override fun observeUser(id: Int): Flow<User> =
userDao.observeUser(id) // Слушать изменения в БД
}
// ViewModel (Presenter)
class UserViewModel(
private val fetchUserUseCase: FetchUserUseCase,
private val deleteUserUseCase: DeleteUserUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<User>>(UiState.Idle)
val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
fun loadUser(userId: Int) {
viewModelScope.launch {
_uiState.value = UiState.Loading
_uiState.value = fetchUserUseCase(userId) // Вызов Use Case
}
}
}
Характеристики:
- ViewModel, Repository, Presenters
- Адаптируют данные между Use Cases и Frameworks
- Содержат минимум логики
- Легко заменяются (полиморфизм через интерфейсы)
4. Frameworks & Drivers (External Layers)
Самый внешний слой — UI, БД, сеть, фреймворки.
// UI Layer (Compose)
@Composable
fun UserScreen(
viewModel: UserViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
when (val state = uiState) {
UiState.Idle -> Button(onClick = { viewModel.loadUser(1) }) { Text("Load") }
UiState.Loading -> CircularProgressIndicator()
is UiState.Content<*> -> UserCard(state.data as User)
is UiState.Error -> ErrorMessage(state.error)
}
}
// Database Layer (Room)
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: Int,
val name: String,
val email: String
)
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUser(id: Int): UserEntity
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: UserEntity)
}
// Network Layer (Retrofit)
interface UserApi {
@GET("/users/{id}")
suspend fun fetchUser(@Path("id") id: Int): UserDto
}
// DTO для сериализации
@Serializable
data class UserDto(
val id: Int,
val name: String,
val email: String
) {
fun toDomain() = User(id, name, email)
}
Характеристики:
- UI компоненты, Activity, Fragment
- Room, Retrofit, Firebase
- Android фреймворк код
- Меньше всего тестируется
Основные принципы Clean Architecture
1. Зависимость идёт только внутрь
External (Frameworks)
↓ зависит
Interface Adapters (Repositories, ViewModels)
↓ зависит
Application (Use Cases)
↓ зависит
Entities (Domain)
Правило: Entities не знают про Use Cases, Use Cases не знают про ViewModel, ViewModel не знает про Retrofit.
// ✓ Правильно
class UserViewModel(useCase: FetchUserUseCase) // ViewModel зависит от Use Case
// ✗ Неправильно
class FetchUserUseCase(viewModel: UserViewModel) // Use Case зависит от ViewModel
2. Инверсия управления через интерфейсы
// ✓ Правильно — Use Case зависит от интерфейса
class FetchUserUseCaseImpl(
private val repository: UserRepository // Зависимость на интерфейс!
)
// ✗ Неправильно — Use Case зависит от конкретной реализации
class FetchUserUseCaseImpl(
private val userRepositoryImpl: UserRepositoryImpl // Конкретная реализация
)
3. Разделение ответственности
Каждый слой отвечает только за свою задачу:
// Domain (Use Case) — ТОЛЬКО бизнес-логика
class CreatePostUseCase(repository: PostRepository) {
suspend operator fun invoke(title: String): Result<Post> {
// Проверка, что пост не пустой
if (title.isBlank()) return Result.Error("Title required")
// Создание поста
return repository.save(Post(title = title))
}
}
// Application (Repository) — адаптация к БД/API
class PostRepositoryImpl(val db: PostDao, val api: PostApi) : PostRepository {
override suspend fun save(post: Post): Result<Post> {
return try {
api.sendPost(post.toDto()) // Преобразование для API
db.insert(post.toEntity()) // Преобразование для БД
Result.Success(post)
} catch (e: Exception) {
Result.Error(e)
}
}
}
// Presentation (ViewModel) — управление UI состоянием
class PostViewModel(val createPostUseCase: CreatePostUseCase) : ViewModel() {
private val _title = MutableStateFlow("")
fun createPost() {
viewModelScope.launch {
val result = createPostUseCase(_title.value) // Вызов Use Case
_uiState.value = when (result) {
is Result.Success -> UiState.PostCreated(result.data)
is Result.Error -> UiState.Error(result.error.message)
}
}
}
}
Преимущества Clean Architecture
✓ 1. Тестируемость
// Use Case легко тестировать, мокируя только Repository интерфейс
class FetchUserUseCaseTest {
@Test
fun testFetchUserSuccess() = runTest {
val mockRepository = mockk<UserRepository>()
coEvery { mockRepository.getUser(1) } returns User(1, "Alice")
val useCase = FetchUserUseCaseImpl(mockRepository)
val result = useCase(1)
assert(result is Result.Success)
assert((result as Result.Success).data.name == "Alice")
}
}
✓ 2. Независимость от фреймворков
Bизнес-логика не зависит от Android, Retrofit, Room — можно переиспользовать в backend, desktop, etc.
✓ 3. Гибкость и расширяемость
Легко менять реализацию слоёв без изменения остального кода:
// Была реализация через Room
class UserRepositoryRoom : UserRepository { }
// Заменяем на Firestore — ViewModel не меняется
class UserRepositoryFirestore : UserRepository { }
// Можно передать в ViewModel обе реализации через DI
✓ 4. Масштабируемость
Когда приложение растёт, понятная структура слоёв упрощает навигацию:
app/
├── domain/
│ ├── entities/
│ └── usecases/
├── application/
│ ├── repositories/ (интерфейсы)
│ └── services/
├── infrastructure/
│ ├── database/ (Room реализация)
│ ├── network/ (Retrofit реализация)
│ └── repositories/ (реализация интерфейсов)
└── presentation/
├── viewmodels/
├── screens/
└── components/
✓ 5. Управление зависимостями
Просто понять, как компоненты зависят друг от друга:
UserScreen (Compose)
↑ uses
UserViewModel
↑ uses
FetchUserUseCase
↑ depends on
UserRepository (interface)
↑ implemented by
UserRepositoryImpl → Room + Retrofit
✓ 6. Командная разработка
Разработчики могут работать параллельно:
- One team: Domain + Use Cases
- Other team: Repositories + Database
- Third team: UI + ViewModels
Практический пример: Reddit-like приложение
// Domain
data class Post(val id: String, val title: String, val content: String, val likes: Int)
data class Comment(val id: String, val postId: String, val text: String, val author: String)
interface PostRepository {
suspend fun getPosts(): List<Post>
suspend fun getPost(id: String): Post
suspend fun createPost(title: String, content: String): Post
}
// Use Case
class GetPostsUseCase(private val repository: PostRepository) {
suspend operator fun invoke(): Result<List<Post>> = try {
Result.Success(repository.getPosts())
} catch (e: Exception) {
Result.Error(e)
}
}
// Repository Implementation
class PostRepositoryImpl(
private val api: RedditApi,
private val db: PostDao
) : PostRepository {
override suspend fun getPosts(): List<Post> {
return try {
val remotePosts = api.getPosts() // Retrofit
db.insertAll(remotePosts.map { it.toEntity() })
remotePosts.map { it.toDomain() }
} catch (e: Exception) {
db.getAll().map { it.toDomain() } // Fallback на локальные данные
}
}
}
// ViewModel
class PostListViewModel(
private val getPostsUseCase: GetPostsUseCase
) : ViewModel() {
private val _posts = MutableStateFlow<List<Post>>(emptyList())
val posts: StateFlow<List<Post>> = _posts.asStateFlow()
init {
loadPosts()
}
private fun loadPosts() {
viewModelScope.launch {
val result = getPostsUseCase()
_posts.value = (result as? Result.Success)?.data ?: emptyList()
}
}
}
// UI
@Composable
fun PostListScreen(
viewModel: PostListViewModel = hiltViewModel()
) {
val posts by viewModel.posts.collectAsState()
LazyColumn {
items(posts) { post ->
PostCard(post)
}
}
}
Антипаттерны
✗ Смешивание слоёв
// Плохо — Use Case содержит UI логику
class FetchUserUseCase {
suspend fun execute(): Result<User> {
val user = api.getUser()
Toast.makeText(context, "User loaded").show() // UI код в Use Case!
return Result.Success(user)
}
}
✗ Зависимость на конкретные реализации
// Плохо
class ViewModel(val repository: UserRepositoryImpl) // Конкретная реализация
// Хорошо
class ViewModel(val repository: UserRepository) // Интерфейс
✗ Слишком сложные Use Cases
// Плохо — Use Case делает слишком много
class CreateUserUseCase(repo: Repository, api: Api, db: Database, cache: Cache) {
suspend fun execute(): User { ... } // 200 строк логики
}
// Хорошо — одна ответственность
class CreateUserUseCase(private val repository: UserRepository) {
suspend operator fun invoke(name: String): Result<User> { ... }
}
Выводы
Clean Architecture — это не dogma, а руководство для структурирования кода таким образом, чтобы:
- Бизнес-логика была отделена от технических деталей
- Код был тестируем без моков всего приложения
- Было легко менять реализации слоёв
- Новым разработчикам было понятно, где искать код
- Приложение росло контролируемо
Золотое правило: Слои зависят только от слоёв внутри них.