Coupling (koppeling) is een metriek die aangeeft hoe sterk een module van een applicatie afhankelijk is van een andere. Volgens Wikipedia is losse koppeling (low coupling) een kenmerk van een goed ontworpen systeem waarin modules kunnen worden gewijzigd zonder naburige te breken. Beheer van coupling is een van de hoofdtaken van de architect bij het ontwerpen van mobiele applicaties.
Belangrijkste punten
Coupling (koppeling) is een metriek die bepaalt hoe sterk een module of klasse is verbonden met een andere. Hoe meer een module weet over de interne structuur van een andere, hoe hoger de coupling en hoe moeilijker het systeem te wijzigen is. In een goed ontworpen architectuur moet coupling minimaal zijn — modules werken alleen samen via strikt gedefinieerde interfaces.
Er worden twee kanten van coupling onderscheiden: afferent (inkomende afhankelijkheden — hoeveel modules zijn afhankelijk van een gegeven) en efferent (uitgaande afhankelijkheden — van hoeveel modules hangt een gegeven module af). Analyse van deze metrieken maakt het mogelijk om "hete punten" in de architectuur te identificeren waar wijziging van één module vele andere zal beïnvloeden. Hulpmiddelen zoals IntelliJ Dependency Analyzer en Xcode Graph visualiseren deze verbanden.
Het is belangrijk te begrijpen dat nul coupling onmogelijk is — modules moeten op de een of andere manier samenwerken, anders is het geen systeem maar een verzameling geïsoleerde programma's. De taak van de architect is om coupling beheersbaar en transparant te maken. Ideaal: modules werken alleen samen via interfaces en geven alleen eenvoudige gegevens door, zonder kennis van elkaars interne structuur. Dit wordt loose coupling (losse koppeling) genoemd.
Zes typen coupling vormen een schaal van beste naar slechtste. Inzicht in deze schaal helpt bij het evalueren van bestaande code en het kiezen van de richting voor refactoring. De meeste mobiele projecten hebben gemengde coupling-typen, en de taak van de architect is om sterke typen geleidelijk te vervangen door zwakke.
Data coupling (gegevenskoppeling) — modules wisselen alleen eenvoudige gegevens uit via methodeparameters. Module A roept de methode van module B aan, geeft primitieven of eenvoudige structuren door en ontvangt een resultaat. Module A weet niet hoe B intern is geïmplementeerd. Dit is het meest gewenste type coupling: het minimaliseert de gevolgen van wijzigingen.
Voorbeeld: EmailValidator.isValid(email: String): Boolean. De consumerende klasse geeft een string door en ontvangt een Boolean, zonder kennis van de reguliere expressies of validatieregels in de validator. Wijziging van de validatielogica vereist geen wijziging van de consument — coupling is minimaal. Data coupling is het doel voor alle publieke interfaces in de applicatie.
Stamp coupling (structuurkoppeling) — modules wisselen samengestelde objecten uit, maar gebruiken slechts een deel van hun velden. Module A geeft een User-object door aan de methode calculateDiscount, die alleen user.status gebruikt. Probleem: als de User-structuur verandert (een verplicht veld wordt toegevoegd), verandert de module calculateDiscount niet, maar de consument die het User-object aanmaakt wel.
In de praktijk is stamp coupling onvermijdelijk en aanvaardbaar als het doorgegeven object een standaard gegevensmodel (Entity) is. Het probleem ontstaat wanneer een module een volledig object ontvangt voor slechts één veld. In dergelijke gevallen is het beter om de specifieke waarde direct door te geven (data coupling). Oplossing — analyseer het veldgebruik door de ontvangende partij.
Control coupling — een module geeft een vlag door aan een andere die het gedrag ervan stuurt (calculate(useNewAlgorithm: Boolean)). Dit is slechter dan stamp coupling omdat de consumerende module de interne werkingsvarianten van de aangeroepen module moet kennen. Oplossing: splits de methode in twee — calculateWithNewAlgorithm() en calculateWithLegacyAlgorithm().
External coupling — modules zijn afhankelijk van een extern protocol, gegevensformaat of API. Alle modules die dezelfde JSON parseren of met dezelfde database werken hebben external coupling. Volledig vermijden is niet mogelijk, maar het kan worden geïsoleerd: creëer een mappinglaag tussen het externe formaat en interne modellen. Common coupling — modules delen een gemeenschappelijke globale toestand. Content coupling — slechtste type, wanneer een module direct interne gegevens van een andere module wijzigt.
| Type coupling | Niveau | Beschrijving |
|---|---|---|
| Data | Beste | Doorgeven van eenvoudige gegevens via parameters |
| Stamp | Aanvaardbaar | Doorgeven van objecten met gedeeltelijk gebruik |
| Control | Gemiddeld | Gedragssturing via vlaggen |
| External | Hoog | Afhankelijkheid van extern protocol/formaatformaat |
| Common | Zeer hoog | Delen van globale toestand |
| Content | Onaanvaardbaar | Direct wijzigen van interne gegevens van een module |
Coupling-schaal van data (ideaal) tot content (catastrofe) — een praktisch hulpmiddel voor codebeoordeling. Als je common of content coupling in een project ziet — is dit het prioritaire doel voor refactoring. Data en stamp coupling zijn aanvaardbaar en aanwezig in elk project, maar hun aantal moet worden gecontroleerd.
Hoge coupling maakt ontwikkeling tot een traag proces waarbij elke wijziging tientallen potentieel gebroken modules moet controleren. In mobiele ontwikkeling is dit bijzonder kritisch: platforms worden jaarlijks bijgewerkt (Android API Level, iOS SDK), bibliotheken — per kwartaal, en bedrijfsvereisten — continu. Losse koppeling is de enige manier om met deze stroom van wijzigingen om te gaan zonder constante regressies.
Voorbeeld uit de praktijk: een mobiele applicatie waarin alle schermen direct NetworkingManager en DatabaseManager importeren. Bij het vervangen van de HTTP-client van Retrofit naar Ktor (Android) of van URLSession naar Alamofire (iOS) zou de ontwikkelaar elk scherm moeten aanpassen. Bij lage coupling volstaat het om één implementatie te wijzigen die verborgen zit achter de interface NetworkDataSource — consumenten merken de vervanging niet.
De invloed van coupling op unittesten is ook enorm. Een klasse met hoge coupling (direct aanmaken van afhankelijkheden via de constructor) kan niet geïsoleerd worden getest — ze sleept de database, het netwerk en de UI mee. Voor het testen van zo'n klasse moet je de emulator opstarten en wachten op integratietests. Een klasse met lage coupling accepteert afhankelijkheden via constructor injection en kan eenvoudig worden gemockt.
// Hoge coupling — klasse maakt zijn eigen afhankelijkheden aan
class ProfileViewModelHigh {
private val api = RetrofitApi()
private val db = RoomDatabase.getInstance()
private val cache = MemoryCache()
}
// Lage coupling — afhankelijkheden worden via de constructor doorgegeven
class ProfileViewModelLow(
private val api: ApiService,
private val db: DatabaseService,
private val cache: CacheService
)
In het eerste geval is ProfileViewModelHigh star verbonden aan concrete implementaties — het vervangen van Retrofit door Ktor vereist wijziging van de ViewModel-code. In het tweede geval is ProfileViewModelLow alleen afhankelijk van interfaces, waarvan de implementaties van buitenaf worden geleverd. Het testen van de tweede klasse is triviaal: we geven mock-implementaties door en controleren de logica zonder emulator.
Dependency Inversion Principle (D in SOLID) — de basis voor het verminderen van coupling. Het principe schrijft voor om af te hangen van abstracties, niet van concrete implementaties. In plaats van dat een klasse direct een RetrofitApi-object aanmaakt, moet deze de interface ApiService ontvangen. Dit verplaatst de koppeling van een specifieke bibliotheek naar het abstractieniveau, dat kan worden vervangen zonder de consument te wijzigen.
Observer pattern (of de reactieve versies — StateFlow, Combine Publishers) vermindert coupling tussen de gegevensbron en abonnees. De abonnee weet niet waar de gegevens vandaan komen — hij reageert eenvoudig op wijzigingen. Dit ontkoppelt de verzender en ontvanger: er kan een nieuwe gegevensbron worden toegevoegd zonder bestaande abonnees te wijzigen. EventBus en SharedFlow werken volgens hetzelfde principe.
Bridge pattern scheidt abstractie en implementatie, waardoor ze onafhankelijk kunnen veranderen. In mobiele ontwikkeling wordt Bridge bijvoorbeeld toegepast voor platformafhankelijke modules: een gemeenschappelijke ImageLoader-interface met verschillende implementaties voor iOS (Kingfisher, Nuke) en Android (Glide, Coil). Code die met ImageLoader werkt, is niet afhankelijk van de gekozen bibliotheek en kan deze vervangen door een eenvoudige implementatiewijziging.
Dependency Injection (DI) — het meest praktische hulpmiddel voor het verminderen van coupling in mobiele ontwikkeling. In plaats van dat een klasse zijn eigen afhankelijkheden aanmaakt, levert de DI-container (Hilt, Koin, Dagger voor Android; Swinject, Factory voor iOS) deze van buitenaf. De klasse ontvangt afhankelijkheden via constructor, method of property injection, zonder kennis van concrete implementaties.
DI documenteert expliciet de afhankelijkheden van een klasse: het volstaat naar de constructor te kijken om te begrijpen met welke modules de klasse samenwerkt. Als de constructor 8 parameters uit verschillende lagen ontvangt — is dit een signaal van overmatige coupling die refactoring vereist. Een goede gewoonte — niet meer dan 3-4 afhankelijkheden per klasse. Een groter aantal duidt op schending van Single Responsibility en overmatige coupling.
DI vereenvoudigt ook het testen: voor elke test maak je een klasse met mock-afhankelijkheden aan, zonder een echte database of netwerk nodig te hebben. In Flutter wordt DI geïmplementeerd via Provider, Riverpod of GetIt. Ongeacht het framework is het doel hetzelfde: de koppeling tussen modules verzwakken door afhankelijkheden expliciet en vervangbaar te maken. Toepassing van DI in een mobiel project is de facto standaard sinds de jaren 2020.
// DI-container bouwt de afhankelijkheidsgraaf
protocol AuthServiceProtocol {
func login(email: String, password: String) async throws -> User
}
final class AuthService: AuthServiceProtocol {
func login(email: String, password: String) async throws -> User {
// implementatie
}
}
// ViewModel weet niet van een concrete service — alleen protocol
final class LoginViewModel {
private let auth: AuthServiceProtocol
init(auth: AuthServiceProtocol) {
self.auth = auth
}
}
// DI Container — de enige plek waar concrete typen worden aangemaakt
final class DIContainer {
lazy var authService: AuthServiceProtocol = AuthService()
lazy var loginViewModel: LoginViewModel {
LoginViewModel(auth: self.authService)
}
}
Hier is LoginViewModel alleen afhankelijk van het protocol AuthServiceProtocol, niet van een concrete AuthService. Het vervangen van de implementatie (bijvoorbeeld overstap van Firebase Auth naar een eigen server) vereist alleen wijzigingen in DIContainer. Alle consumenten van AuthServiceProtocol blijven onaangetast — coupling is tot een minimum beperkt door abstractie en DI.
Veelgestelde vragen
Cohesion meet de interne samenhang van een module, coupling — de externe verbinding tussen modules. Een goede architectuur streeft naar hoge cohesion en lage coupling. Deze metrieken zijn omgekeerd evenredig: verhoging van cohesion vermindert meestal coupling en omgekeerd.
Data en stamp — normaal en aanwezig in elk project. Control coupling is aanvaardbaar in beperkte scenario's (bijvoorbeeld strategy pattern). External coupling is onvermijdelijk bij het werken met externe API's, maar moet worden geïsoleerd achter een mappinglaag. Common en content coupling — tekenen van architectuurproblemen die onmiddellijke refactoring vereisen.
Hulpmiddelen voor statische analyse: IntelliJ IDEA Dependency Matrix, Xcode Graph, Gradle Dependencies report, SonarQube. Metrieken: afferent coupling (Ca), efferent coupling (Ce), Instability (Ce/(Ca+Ce)). Hoge Instability (dicht bij 1) betekent dat de module gemakkelijk kan worden gewijzigd en weinig ernaar verwijst — dit is goed.
Extreem lage coupling kan wijzen op een overmatig aantal abstracties en interfaces die de navigatie door de code bemoeilijken. Als voor elke klasse een aparte interface is gemaakt, verspilt de programmeur tijd met springen tussen bestanden. Balans: interfaces voor de externe API van een module, maar niet voor elke interne hulpklasse.
Gebruik de Strangler Fig-techniek — vervang geleidelijk directe aanroepen via interfaces. Begin met extract interface voor klassen waarnaar het vaakst wordt verwezen. Introduceer vervolgens een DI-container. Dek de geïsoleerde code af met karakterisatietests om er zeker van te zijn dat refactoring het systeemgedrag niet verandert.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook