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

Опишите Clean Architecture. В чём её основные принципы и преимущества?

2.8 Senior🔥 251 комментариев
#Архитектура и паттерны#Многомодульность

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

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

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

# 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, а руководство для структурирования кода таким образом, чтобы:

  • Бизнес-логика была отделена от технических деталей
  • Код был тестируем без моков всего приложения
  • Было легко менять реализации слоёв
  • Новым разработчикам было понятно, где искать код
  • Приложение росло контролируемо

Золотое правило: Слои зависят только от слоёв внутри них.

Опишите Clean Architecture. В чём её основные принципы и преимущества? | PrepBro