Clean Architecture — grunder, lager Entities, Use Cases och Gateways

Författare: IT Sectr Publicerad: 2026-02-17 Lästid: 10 min

Clean Architecture — flerskiktsarkitektur föreslagen av Robert Martin (Uncle Bob) 2012, som delar applikationen i oberoende lager: Domain (Entities, Use Cases), Data (Repositories, DataSources) och Presentation (ViewModels, Views). Huvudprincipen — Dependency Rule: beroenden riktas inåt, yttre lager är beroende av inre, men inte tvärtom. Clean Architecture tillämpas inom mobil utveckling för projekt med hög komplexitet i affärslogik. Mer — i boken The Clean Architecture.

Huvudpunkter

  • Clean Architecture — tre lager: Domain (affärslogik), Data (data), Presentation (UI) med Dependency Rule
  • Dependency Rule — beroenden riktas inåt, Domain känner inte till Data och Presentation
  • Use Cases (Interactors) — scenarier för affärslogik, varje Use Case — en klass med en metod
  • Repository Interface — dataabstraktion i Domain, implementering — i Data-lagret
  • Testbarhet — Domain och Use Cases testas med enhetstester utan Android SDK och iOS UIKit

Clean Architecture — grunder i flerskiktsarkitektur

Clean Architecture — arkitekturmönster formulerat av Robert Martin (Uncle Bob) 2012. Huvudidén — uppdelning av applikationen i lager med strikt beroenderegel: kod inuti ett lager känner inte till kod utanför. Yttre lager (UI, ramverk, databaser) — implementeringsdetaljer. Inre lager (affärslogik, företagsregler) — applikationens kärna.

Clean Architecture-lager inom mobil utveckling: 1) Domain — Entities (affärsobjekt) och Use Cases (användningsfall); 2) Data — RepositoryImpl (implementering av repositories), DataSources (nätverk, databas, cache); 3) Presentation — ViewModels, Views (Compose/SwiftUI). Domain — det innersta lagret, utan beroenden. Data är beroende av Domain (implementerar repository-gränssnitten). Presentation är beroende av Domain (anropar Use Cases, prenumererar på resultatet).

LagerInnehållerBeroenden
DomainEntities, Use Cases, Repository InterfacesInga (ren Kotlin/Swift)
DataRepositoryImpl, DataSources (API, DB, Cache)Domain, Retrofit, Room, Ktor
PresentationViewModels, Views, ComposablesDomain, Jetpack, SwiftUI

Dependency Rule — den enda strikta regeln i Clean Architecture. Källkod kan endast referera till lagret inom sig själv eller till lagret under (närmare centrum). Presentation importerar Domain. Domain importerar INTE Data eller Presentation. Detta uppnås genom beroendeinversion (Dependency Inversion Principle): Domain definierar Repository-gränssnittet, Data implementerar det. Presentation är beroende av UseCase-abstraktionen, inte av ett specifikt repository.

Domain Layer: Entities, Use Cases och Repository Interfaces

Domain — det mest stabila lagret i applikationen. Entities — affärsobjekt oberoende av ramverk: User, Product, Order. Use Cases — klasser med en metod invoke (eller operator fun invoke i Kotlin), som implementerar ett scenario: GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase. Repository Interfaces — abstraktioner av dataåtkomst, definierade i Domain, implementerade i Data. Domain innehåller inte Android SDK, iOS UIKit, Retrofit, Room — endast ren Kotlin eller Swift.

swift
// Entity — affärsobjekt (Domain)
struct User: Equatable {
    let id: Int
    let name: String
    let email: String
}

// Repository Interface — dataabstraktion (Domain)
protocol UserRepository {
    func getUser(id: Int) async throws -> User
    func getUsers() async throws -> [User]
}

// Use Case — ett scenario (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 — «klass med en metod» — inte ett dogma, utan en praktisk rekommendation. När Use Case blir mer komplext (validering + loggning + anrop av repository), grupperas dess metoder efter betydelse: UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser. Viktigt — Use Case ska inte veta var data kommer ifrån (nätverk, databas, cache) och vem som visar den (Compose, SwiftUI). På IT Sectr tilldelar vi ett Use Case för varje operation som har en affärsregel, validering eller kombination av data från två källor.

Renhet i Domain uppnås genom DTO-mappning vid lagergränserna. Data-lagret tar emot JSON-modeller (DTO), mappar dem till Domain Entity. Presentation tar emot Domain Entity, mappar till ViewModel (DisplayItem). Domain Entity innehåller aldrig Retrofit-, Room- eller Codable-annotationer — detta garanterar att lagret inte behöver ändras vid byte från Room till Realm eller vid ersättning av Retrofit med Ktor.

Data Layer: Repository Implementation och DataSources

Data Layer — implementering av gränssnitten definierade i Domain. Innehåller RepositoryImpl (klasser som implementerar UserRepository) och DataSources (RemoteDataSource — API, LocalDataSource — databas, CacheDataSource — SharedPreferences/NSUserDefaults). Data-lagret är beroende av Domain (importerar repository-gränssnitt och Entities) och av ramverk (Retrofit, Room, Ktor, CoreData). RepositoryImpl döljer datakällan för Domain — Use Case vet inte om data kommer från nätverket eller cachen.

kotlin
// DTO — modell för nätverk (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 — implementering (Data)
class UserRepositoryImpl(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {

    override suspend fun getUser(id: Int): User {
        // Försöker hämta från cache
        localDataSource.getUser(id)?.let { return it.toDomain() }
        // Om inte — laddar från nätverket
        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 — konvertering DTO ↔ Domain
fun UserDto.toDomain() = User(
    id = id,
    name = "$firstName $lastName",
    email = email
)

Cachere strategi i Data Layer: RepositoryImpl kontrollerar först lokal lagring, i avsaknad av data — laddar från nätverket och sparar lokalt. Om nätverket inte är tillgängligt — returnerar föråldrad data med markeringen isStale. Use Case i Domain känner inte till strategin — tar emot User via Repository.getUser(id). Ändring av strategi (t.ex. cache-invalidering var 15:e minut) påverkar inte Domain och Presentation.

Modularitet i Android — Kotlin Multiplatform gör det möjligt att flytta Domain till en separat KMP-modul utan beroenden av Android SDK. Data — separat modul med beroende av Domain. Presentation — Android-modul med beroende av Domain. Gradle-beroenden: domain (pure Kotlin), data (domain + Retrofit + Room), app (domain + presentation + Hilt). Sådan modularitet är obligatorisk för stora projekt — CI bygger Domain separat, enhetstester för Domain kräver ingen Android-emulator.

Presentation Layer: ViewModels och Views

Presentation Layer — det yttersta lagret i Clean Architecture. Innehåller ViewModels (Android) / ObservableObject (iOS) och Views (Compose/SwiftUI). ViewModel anropar Use Case, tar emot resultatet och omvandlar det till UI-tillstånd (State). View prenumererar på State och visar. Presentation är beroende av Domain — importerar Use Cases och Entities. Presentation importerar inte Data Layer — data kommer via Use Case, som internt använder Repository.

ViewModel i Clean Architecture innehåller inte affärslogik — anropar Use Case. Om Use Case returnerar User, omvandlar ViewModel det till UserDisplayItem (name, emailFormatted, avatarUrl) — en ren presentationsmodell. Use Case känner inte till DisplayItem — returnerar Entity. Denna separation gör det möjligt att testa Use Case utan UI och ViewModel utan UseCase (via mock). På IT Sectr följer vi strikt: Use Case — affärslogik, ViewModel — endast presentation, View — endast visning.

kotlin
// Use Case (Domain) — ren affärslogik
class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(id: Int): User {
        return repository.getUser(id)
    }
}

// ViewModel (Presentation) — endast presentation
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 i Presentation-lagret — en del av den yttre ringen. Clean Architecture föreskriver ingen navigeringsmekanism — det kan vara NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) eller Router (VIPER). Viktigt: navigeringsbeslutet fattas av Presentation, men navigering bör inte tränga in i Use Case. Use Case returnerar resultatet, ViewModel bestämmer vilken skärm som ska visas. I Clean Architecture är navigering en detalj som kan bytas ut utan att ändra Domain.

Clean Architecture på iOS och Android: kodexempel

Clean Architecture på Android implementeras via Gradle-moduler: domain (pure Kotlin), data (domain + Retrofit + Room), presentation (domain + Compose). Mappstruktur: 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) kopplar samman lagren: UserRepositoryImpl binds till UserRepository-gränssnittet i domain-modulen.

Clean Architecture på iOS använder SPM eller Xcode-grupper utan separata moduler (på grund av Xcode-begränsningar). Domain — mapp med filer som inte importerar UIKit eller SwiftUI. Data — mapp med APIClient, CoreDataStack, RepositoryImpl. Presentation — mapp med ViewModels och SwiftUI Views. DI via konstruktor eller assembly i App. Huvudanropet — async/await via UseCase.execute() med MainActor-kontroll för UI-uppdateringar.

swift
// 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 i IT Sectr-projekt — vår standard för projekt från 30 dagar. Vi har använt trelagersarkitektur med Kotlin Multiplatform för Android/iOS sedan 2022. Domain — gemensam KMP-modul, Data — plattformsmoduler (Retrofit på Android, URLSession på iOS), Presentation — inbyggt UI. Detta ger 60–80% gemensam affärslogikkod mellan iOS och Android, vilket minskar utvecklingstiden med 30–40% jämfört med två separata implementationer.

Vanliga frågor

Hur många lager ska finnas i Clean Architecture?

Minst tre: Domain, Data, Presentation. För stora projekt läggs Framework (Android SDK/iOS UIKit-beroenden) och Device (GPS, kamera, sensorer) till. Antalet lager — ingen strikt regel, utan en bekvämlighetsfråga. Det viktiga är att följa Dependency Rule: beroenden riktas inåt, mot Domain. Man kan börja med tre och lägga till lager allteftersom projektet växer.

Ökar Clean Architecture mängden kod?

Ja — med 30–50% jämfört med MVVM på grund av separering av repository-gränssnitt, Use Cases och mapprar. För en enkel CRUD-applikation är detta överflödigt. Clean Architecture är motiverad för projekt med komplex affärslogik, där testbarhet och lagerisolering är viktigare än utvecklingshastighet. För MVP eller prototyp, använd MVVM — Clean Architecture kommer att sakta ner lanseringen.

Kan Clean Architecture kombineras med MVI?

Ja, det är vanlig praxis. Use Cases stannar i Domain och Presentation använder MVI-cykeln (Intent → Reducer → State). Data Layer — samma, Domain — samma. MVI i Presentation ger förutsägbart skärmtillstånd, Clean Architecture — isolering av affärslogik. Denna kombination används i stora projekt med dussintals utvecklare.

Behövs Use Cases för varje dataförfrågan?

Use Case behövs när operationen innehåller en affärsregel: validering, kombination av data från två källor, beräkning, loggning, kontroll av åtkomsträttigheter. En enkel getUser(id)-förfrågan utan extra logik kan anropa Repository direkt från ViewModel. För arkitekturens enhetlighet skapar dock många team ett Use Case för varje publik metod i Repository — detta tillför 5–10% kod men förenklar läsningen.

Hur testar man Clean Architecture?

Domain: enhetstester av Use Cases med mock Repository — ren Kotlin/Swift utan Android SDK. Data: integrationstester av RepositoryImpl med mock/fake DataSource. Presentation: tester av ViewModel med mock UseCase. Tack vare Dependency Rule testas varje lager isolerat. På IT Sectr når domäntäckningen 95%, Data — 70–80%, Presentation — 60–70%.

Sammanfattning

  • Clean Architecture — tre lager (Domain, Data, Presentation) med Dependency Rule inåt
  • Dependency Rule — Domain känner inte till Data och Presentation, isolering via gränssnitt
  • Domain — Entities, Use Cases, Repository Interfaces — ren Kotlin/Swift utan ramverk
  • Data — RepositoryImpl, DataSources (nätverk, databas, cache) — implementering av Domain-gränssnitt
  • Presentation — ViewModels, Views — endast visning, affärslogik i Use Cases
  • Testning — Domain täcks av enhetstester 90–95%
  • KMP — Clean Architecture med Kotlin Multiplatform ger 60–80% gemensam kod iOS + Android

Vi utvecklar en mobil applikation nyckelfärdigt

IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.

Diskutera projektet

Läs också