Clean Architecture — une architecture en couches proposée par Robert Martin (Uncle Bob) en 2012, divisant une application en couches indépendantes : Domain (Entities, Use Cases), Data (Repositories, DataSources) et Presentation (ViewModels, Views). Le principe principal est la Dependency Rule : les dépendances pointent vers l'intérieur, les couches externes dépendent des couches internes, mais pas l'inverse. La Clean Architecture est utilisée dans le développement mobile pour les projets à forte complexité de logique métier. Plus de détails dans le livre The Clean Architecture.
Points Clés
Clean Architecture — un modèle architectural formulé par Robert Martin (Uncle Bob) en 2012. L'idée centrale est de diviser une application en couches avec une règle de dépendance stricte : le code à l'intérieur d'une couche ne connaît rien du code extérieur. Les couches externes (UI, frameworks, BD) sont des détails d'implémentation. Les couches internes (logique métier, règles d'entreprise) sont l'essence de l'application.
Couches de la Clean Architecture dans le développement mobile : 1) Domain — Entities (objets métier) et Use Cases (scénarios d'utilisation) ; 2) Data — RepositoryImpl (implémentations de référentiels), DataSources (réseau, BD, cache) ; 3) Presentation — ViewModels, Views (Compose/SwiftUI). Domain est la couche la plus interne sans dépendances. Data dépend de Domain (implémente les interfaces de référentiel). Presentation dépend de Domain (appelle les Use Cases, s'abonne aux résultats).
| Couche | Contient | Dépendances |
|---|---|---|
| Domain | Entities, Use Cases, Repository Interfaces | Aucune (Kotlin/Swift pur) |
| Data | RepositoryImpl, DataSources (API, BD, Cache) | Domain, Retrofit, Room, Ktor |
| Presentation | ViewModels, Views, Composables | Domain, Jetpack, SwiftUI |
Dependency Rule — la seule règle stricte de la Clean Architecture. Le code source ne peut référencer qu'une couche à l'intérieur de lui-même ou une couche inférieure (plus proche du centre). Presentation importe Domain. Domain N'importe PAS Data ou Presentation. Ceci est réalisé via le principe d'inversion des dépendances : Domain définit l'interface Repository, Data l'implémente. Presentation dépend de l'abstraction UseCase, pas d'un référentiel spécifique.
Domain — la couche la plus stable de l'application. Les Entities sont des objets métier indépendants des frameworks : User, Product, Order. Les Use Cases sont des classes avec une seule méthode invoke (ou operator fun invoke en Kotlin), implémentant un scénario : GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase. Les Repository Interfaces sont des abstractions d'accès aux données définies dans Domain, implémentées dans Data. Domain ne contient ni Android SDK, ni iOS UIKit, ni Retrofit, ni Room — seulement du Kotlin ou Swift pur.
// Entity — objet métier (Domain)
struct User: Equatable {
let id: Int
let name: String
let email: String
}
// Repository Interface — abstraction de données (Domain)
protocol UserRepository {
func getUser(id: Int) async throws -> User
func getUsers() async throws -> [User]
}
// Use Case — un scénario (Domain)
final class GetUserUseCase {
private let repository: UserRepository
init(repository: UserRepository) {
self.repository = repository
}
func execute(id: Int) async throws -> User {
return try await repository.getUser(id: id)
}
}
Use Case — « une classe avec une méthode » n'est pas un dogme mais une recommandation pratique. Lorsqu'un Use Case devient plus complexe (validation + journalisation + appel de référentiel), ses méthodes sont regroupées par sens : UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser. L'essentiel est qu'un Use Case ne doit pas savoir d'où viennent les données (réseau, BD, cache) ni qui les affiche (Compose, SwiftUI). Chez IT Sectr, nous allouons un Use Case pour chaque opération qui a une règle métier, une validation ou une combinaison de données provenant de deux sources.
Pureté du Domain est obtenue via le mapping DTO aux limites des couches. La couche Data reçoit des modèles JSON (DTO), les mappe vers des Entities du Domain. Presentation reçoit des Entities du Domain, les mappe vers des ViewModels (DisplayItem). Une Entity du Domain ne contient jamais d'annotations Retrofit, Room ou Codable — cela garantit que la couche n'aura pas besoin d'être modifiée lors du changement de BD de Room vers Realm ou du remplacement de Retrofit par Ktor.
Couche Data — implémentation des interfaces définies dans Domain. Contient RepositoryImpl (classes implémentant UserRepository) et DataSources (RemoteDataSource — API, LocalDataSource — BD, CacheDataSource — SharedPreferences/NSUserDefaults). La couche Data dépend de Domain (importe les interfaces de référentiel et les Entities) et des frameworks (Retrofit, Room, Ktor, CoreData). RepositoryImpl cache la source de données à Domain — le Use Case ne sait pas si les données viennent du réseau ou du cache.
// DTO — modèle pour le réseau (Data)
data class UserDto(
@SerializedName("id") val id: Int,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String,
@SerializedName("email") val email: String
)
// RepositoryImpl — implémentation (Data)
class UserRepositoryImpl(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) : UserRepository {
override suspend fun getUser(id: Int): User {
// Tentative de récupération depuis le cache
localDataSource.getUser(id)?.let { return it.toDomain() }
// Sinon — chargement depuis le réseau
val dto = remoteDataSource.fetchUser(id)
val user = dto.toDomain()
localDataSource.saveUser(user)
return user
}
override suspend fun getUsers(): List<User> {
return remoteDataSource.fetchAllUsers().map { it.toDomain() }
}
}
// Mapper — conversion DTO ↔ Domain
fun UserDto.toDomain() = User(
id = id,
name = "$firstName $lastName",
email = email
)
Stratégie de cache dans la couche Data : RepositoryImpl vérifie d'abord le stockage local ; si aucune donnée n'est trouvée, il charge depuis le réseau et sauvegarde localement. Si le réseau est indisponible, il retourne des données obsolètes avec un indicateur isStale. Le Use Case dans Domain ignore la stratégie — il reçoit User via Repository.getUser(id). Changer la stratégie (par exemple, invalidation du cache toutes les 15 minutes) n'affecte ni Domain ni Presentation.
Modularité dans Android — Kotlin Multiplatform permet d'extraire Domain dans un module KMP séparé sans dépendances Android SDK. Data est un module séparé avec une dépendance sur Domain. Presentation est un module Android avec une dépendance sur Domain. Dépendances Gradle : domain (Kotlin pur), data (domain + Retrofit + Room), app (domain + presentation + Hilt). Une telle modularité est essentielle pour les grands projets — CI compile Domain séparément, les tests unitaires de Domain ne nécessitent pas d'émulateur Android.
Couche Presentation — la couche la plus externe de la Clean Architecture. Contient des ViewModels (Android) / ObservableObject (iOS) et des vues (Compose/SwiftUI). Le ViewModel appelle un Use Case, reçoit le résultat et le transforme en état UI. La Vue s'abonne à l'État et le rend. Presentation dépend de Domain — importe les Use Cases et les Entities. Presentation n'importe pas la couche Data — les données arrivent via le Use Case, qui utilise en interne un Repository.
ViewModel dans la Clean Architecture ne contient pas de logique métier — il appelle le Use Case. Si un Use Case retourne User, le ViewModel le transforme en UserDisplayItem (name, emailFormatted, avatarUrl) — un modèle purement de présentation. Le Use Case ignore DisplayItem — il retourne une Entity. Cette séparation permet de tester le Use Case sans l'UI et le ViewModel sans le UseCase (via des mocks). Chez IT Sectr, nous suivons strictement : Use Case — logique métier, ViewModel — présentation uniquement, Vue — affichage uniquement.
// Use Case (Domain) — logique métier pure
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: Int): User {
return repository.getUser(id)
}
}
// ViewModel (Presentation) — présentation uniquement
class UserViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
private val _state = MutableStateFlow<UserScreenState>(UserScreenState.Loading)
val state: StateFlow<UserScreenState> = _state.asStateFlow()
fun loadUser(id: Int) {
viewModelScope.launch {
_state.value = UserScreenState.Loading
val user = getUserUseCase(id)
val displayItem = UserDisplayItem(
name = user.name,
email = user.email,
initials = user.name.split(" ").joinToString("") { it.first().toString() }
)
_state.value = UserScreenState.Success(displayItem)
}
}
}
data class UserDisplayItem(
val name: String,
val email: String,
val initials: String
)
sealed interface UserScreenState {
data object Loading : UserScreenState
data class Success(val displayItem: UserDisplayItem) : UserScreenState
data class Error(val message: String) : UserScreenState
}
Navigation dans la couche Presentation fait également partie de l'anneau externe. La Clean Architecture ne prescrit pas de mécanisme de navigation — il peut s'agir de NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) ou Router (VIPER). Il est important que les décisions de navigation soient prises par Presentation, mais la navigation ne doit pas pénétrer dans le Use Case. Le Use Case retourne un résultat, le ViewModel décide vers quel écran naviguer. Dans la Clean Architecture, la navigation est un détail qui peut être remplacé sans modifier Domain.
Clean Architecture sur Android est implémentée via des modules Gradle : domain (Kotlin pur), data (domain + Retrofit + Room), presentation (domain + Compose). Structure de dossiers : domain/user/User.kt, GetUserUseCase.kt, UserRepository.kt ; data/remote/UserRemoteDataSource.kt, local/UserDao.kt, repository/UserRepositoryImpl.kt ; presentation/ui/user/UserViewModel.kt, UserScreen.kt. DI (Hilt) connecte les couches : UserRepositoryImpl est lié à l'interface UserRepository dans le module domain.
Clean Architecture sur iOS utilise SPM ou des groupes Xcode sans modules séparés (en raison des limitations de Xcode). Domain — un dossier avec des fichiers qui n'importent ni UIKit ni SwiftUI. Data — un dossier avec APIClient, CoreDataStack, RepositoryImpl. Presentation — un dossier avec ViewModels et vues SwiftUI. DI via constructeur ou assemblage dans l'App. L'appel principal est async/await via UseCase.execute() avec vérification MainActor pour les mises à jour UI.
// Data Layer: Remote DataSource (iOS)
final class UserRemoteDataSource {
private let apiClient: APIClient
func fetchUser(id: Int) async throws -> UserDTO {
return try await apiClient.get("/users/\(id)")
}
}
// Repository Implementation (Data)
final class UserRepositoryImpl: UserRepository {
private let remote: UserRemoteDataSource
private let local: UserLocalDataSource
func getUser(id: Int) async throws -> User {
if let cached = try await local.getUser(id) {
return cached
}
let dto = try await remote.fetchUser(id)
let user = dto.toDomain()
try await local.saveUser(user)
return user
}
}
// Presentation: ViewModel + SwiftUI View
@MainActor
final class UserViewModel: ObservableObject {
@Published private(set) var state: UserScreenState = .loading
private let getUserUseCase: GetUserUseCase
func loadUser(id: Int) {
Task {
state = .loading
if let user = try? await getUserUseCase.execute(id: id) {
state = .success(user)
} else {
state = .error("Failed to load")
}
}
}
}
Clean Architecture dans les projets IT Sectr — notre standard pour les projets à partir de 30 jours. Nous utilisons une architecture à trois couches avec Kotlin Multiplatform pour Android/iOS depuis 2022. Domain — un module KMP partagé, Data — des modules de plateforme (Retrofit sur Android, URLSession sur iOS), Presentation — UI native. Cela donne 60–80 % de code de logique métier partagé entre iOS et Android, réduisant le temps de développement de 30–40 % par rapport à deux implémentations séparées.
Questions fréquentes
Trois au minimum : Domain, Data, Presentation. Pour les grands projets, on ajoute Framework (dépendances Android SDK/iOS UIKit) et Device (GPS, caméra, capteurs). Le nombre de couches n'est pas une règle stricte mais une question de commodité. L'essentiel est de suivre la Dependency Rule : les dépendances pointent vers l'intérieur, vers Domain. Vous pouvez commencer avec trois et ajouter des couches au fur et à mesure que le projet grandit.
Oui — de 30 à 50 % par rapport à MVVM en raison de l'extraction des interfaces de référentiel, des Use Cases et des mappers. C'est excessif pour une simple application CRUD. La Clean Architecture se justifie pour les projets avec une logique métier complexe où la testabilité et l'isolation des couches sont plus importantes que la vitesse de développement. Pour un MVP ou un prototype, utilisez MVVM — la Clean Architecture ralentira le démarrage.
Oui, c'est une pratique courante. Les Use Cases restent dans Domain, tandis que Presentation utilise le cycle MVI (Intent → Reducer → State). La couche Data reste la même, Domain reste le même. MVI dans la Presentation fournit un état d'écran prévisible, la Clean Architecture fournit l'isolation de la logique métier. Cette combinaison est utilisée dans les grands projets avec des dizaines de développeurs.
Un Use Case est nécessaire lorsque l'opération implique une règle métier : validation, combinaison de données de deux sources, calcul, journalisation, vérification des droits d'accès. Une simple requête getUser(id) sans logique supplémentaire peut appeler le Repository directement depuis le ViewModel. Cependant, pour la cohérence architecturale, de nombreuses équipes créent un Use Case pour chaque méthode publique du Repository — cela ajoute 5 à 10 % de code mais simplifie la lecture.
Domain : Tests unitaires des Use Cases avec Repository mock — Kotlin/Swift pur sans Android SDK. Data : Tests d'intégration de RepositoryImpl avec DataSource mock/fake. Presentation : Tests de ViewModel avec UseCase mock. Grâce à la Dependency Rule, chaque couche est testée isolément. Chez IT Sectr, la couverture de Domain atteint 95 %, Data — 70–80 %, Presentation — 60–70 %.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi