Object-Oriented Programming
Van losse variabelen naar een betrouwbaar domeinmodel. Leer waarom classes, constructors, encapsulation, interfaces en Kotlin-typeconstructies belangrijk zijn, en wanneer je ze juist niet nodig hebt.
Wat bouwen we deze week?
In Week 01 werkten we met losse waarden en een consolebattle. Nu modelleren we de vaste gegevens van monsters met drie types: Element, Stats en Monster. De battle-engine volgt in Week 03. Zo houden we het domein klein en vermijden we dat dezelfde HP op verschillende plaatsen wordt opgeslagen.
Ontwerpcontract v5.0: Monster(id, name, element, stats); Stats(hp, attack, defense, speed); Element.FIRE/WATER/WIND. De constructor valideert de basiswaarden. Het domein heeft géén permanente currentHp en geen verplichte Attackable-interface. Die concepten behandelen we waar nuttig als zelfstandige OO-demonstratie.
Alle 26 voorbeelden zijn afzonderlijke mini-programma’s; herhaalde classnamen in verschillende voorbeelden zijn geen extra domeinmodellen. Alleen onderwerp 26 legt de definitieve modelstructuur vast.
De 26 OO-onderwerpen
Elk onderwerp bevat een uitleg, de werking, een compleet Kotlin-voorbeeld, de verwachte uitvoer, de reden voor de aanpak, een afweging en de toepassing in MonsterBattles. De kleine codeblokken zijn afzonderlijk uitvoerbaar; ze zijn niet bedoeld om samen als één bestand te worden geplakt.
01Object-Oriented Programming
Wat is het? Objectgeoriënteerd programmeren bundelt gegevens en gedrag in objecten. Een object heeft een eigen toestand en functies die op die toestand werken.
Hoe werkt het? Een class bevat properties en memberfuncties. Met een constructor maak je een object; daarna roep je gedrag via de puntnotatie aan.
class Monster(val name: String, val hp: Int) {
fun describe(): String = "$name has $hp HP"
}
fun main() {
val pyron = Monster("Pyron", 100)
println(pyron.describe())
}Pyron has 100 HPWaarom deze aanpak? Een monster bewaart niet alleen zijn naam, maar kan ook zijn eigen toestand beschrijven.
Wanneer kies je iets anders? Losse top-level functies zijn prima voor kleine berekeningen; zet gedrag in classes als gegevens en gedrag werkelijk bij elkaar horen.
02Classes
Wat is het? Een class is een blauwdruk voor objecten. In de class definieer je welke eigenschappen en functies elk object heeft.
Hoe werkt het? Een class is een typebeschrijving. Constructorparameters met val worden direct publieke, alleen-leesbare properties; een memberfunctie kan ze gebruiken.
class Monster(val name: String, val attack: Int) {
fun showAttack() = println("$name: $attack ATK")
}
fun main() {
Monster("Pyron", 30).showAttack()
}Pyron: 30 ATKWaarom deze aanpak? We definiëren een herbruikbaar monstertype in plaats van losse variabelen per monster.
Wanneer kies je iets anders? Een data class past beter bij louter waardedata. Maak geen aparte class als één eenvoudige top-level functie al volstaat.
03Objects: instanties aanmaken
Wat is het? Met een constructoraanroep maak je een instantie van een class. Elke instantie heeft zijn eigen propertywaarden.
Hoe werkt het? Twee aanroepen van dezelfde constructor maken afzonderlijke instanties. Een wijziging aan de ene instantie verandert niet automatisch de andere.
class Monster(val name: String, var hp: Int)
fun main() {
val pyron = Monster("Pyron", 100)
val aquara = Monster("Aquara", 110)
pyron.hp -= 10
println("${pyron.name}: ${pyron.hp}")
println("${aquara.name}: ${aquara.hp}")
}Pyron: 90
Aquara: 110Waarom deze aanpak? Deze mutatie demonstreert een los onderwijsvoorbeeld; in het definitieve domein staat alleen de vaste Stats.hp, niet de actuele battle-HP.
Wanneer kies je iets anders? Gebruik kopieën van onveranderlijke data wanneer mutatie onnodig is; dat voorkomt onbedoelde bijwerkingen.
04Primary constructors
Wat is het? De primary constructor staat in de class-header. Parameters met val of var worden meteen properties; zonder val of var zijn het alleen parameters.
Hoe werkt het? De primary constructor staat direct achter de classnaam. Parameters met val of var worden properties, de overige parameters zijn alleen tijdens initialisatie beschikbaar.
class Monster(val name: String, val element: String, val maxHp: Int)
fun main() {
val monster = Monster("Pyron", "FIRE", 100)
println("${monster.name} (${monster.element}): ${monster.maxHp} HP")
}Pyron (FIRE): 100 HPWaarom deze aanpak? Naam, element en begin-HP zijn verplicht bij het aanmaken van een monster.
Wanneer kies je iets anders? Gebruik een factoryfunctie als objectconstructie veel controles of alternatieve creatiepaden vereist.
05Secondary constructors
Wat is het? Een secondary constructor biedt een extra manier om een object aan te maken. Als een primary constructor bestaat, moet de secondary constructor daarnaar delegeren.
Hoe werkt het? Een secondary constructor staat in de classbody en gebruikt bij aanwezigheid van een primary constructor verplicht this(...) om daarnaar te delegeren.
class Monster(val name: String, val hp: Int) {
constructor(name: String) : this(name, 100)
}
fun main() {
println(Monster("Pyron").hp)
println(Monster("Aquara", 110).hp)
}100
110Waarom deze aanpak? Een testmonster kan een standaardhoeveelheid HP krijgen zonder die bij iedere aanroep op te geven.
Wanneer kies je iets anders? Default arguments zijn meestal eenvoudiger dan extra constructors. Gebruik secondary constructors alleen bij een echt andere aanmaakroute.
06Constructor chaining
Wat is het? Constructor chaining betekent dat een constructor een andere constructor aanroept met this(...). Zo loopt alle initialisatie via een gedeeld pad.
Hoe werkt het? Met this(...) roept een constructor een andere constructor van dezelfde class aan. Daardoor kan gemeenschappelijke initialisatie op één plaats staan.
class Monster(val name: String, val hp: Int) {
constructor(name: String) : this(name, 100)
constructor() : this("Unknown")
}
fun main() {
println("${Monster().name}: ${Monster().hp}")
println("${Monster("Pyron").name}: ${Monster("Pyron").hp}")
}Unknown: 100
Pyron: 100Waarom deze aanpak? Verschillende manieren om monsters te maken gebruiken dezelfde basisinitialisatie.
Wanneer kies je iets anders? Default parameters vermijden vaak constructor chaining. Kies chaining wanneer je meerdere expliciete constructorroutes nodig hebt.
07Initialization blocks: init
Wat is het? Een init-block draait bij het maken van een object. Het is geschikt om constructorargumenten te controleren en afgeleide eigenschappen te initialiseren.
Hoe werkt het? Het init-blok draait tijdens het construeren van een object, nadat de voorafgaande property-initializers zijn uitgevoerd. Gebruik het voor invarianten zoals hp > 0.
class Monster(val name: String, val maxHp: Int) {
init {
require(name.isNotBlank()) { "Name is required" }
require(maxHp > 0) { "HP must be positive" }
}
}
fun main() {
val monster = Monster("Pyron", 100)
println("${monster.name}: ${monster.maxHp} HP")
}Pyron: 100 HPWaarom deze aanpak? Ongeldige monsterstats worden al bij de constructie geweigerd.
Wanneer kies je iets anders? In services gebruik je require voor invoer en check voor ongeldige interne toestand; init is geschikt voor objectinvarianten.
08Properties, getters en setters
Wat is het? Properties gebruiken val of var. Een custom getter berekent een waarde wanneer je die opvraagt; een custom setter kan wijzigingen controleren.
Hoe werkt het? Properties hebben desgewenst een eigen getter of setter. Een getter berekent een waarde bij het lezen; een setter controleert of verwerkt nieuwe waarden.
class Monster(val name: String, initialHp: Int) {
var hp: Int = initialHp
set(value) {
field = value.coerceAtLeast(0)
}
val isAlive: Boolean
get() = hp > 0
}
fun main() {
val monster = Monster("Pyron", 100)
monster.hp = -8
println("HP: ${monster.hp}, alive: ${monster.isAlive}")
}HP: 0, alive: falseWaarom deze aanpak? We berekenen of een monster nog leeft op basis van zijn actuele HP.
Wanneer kies je iets anders? Een gewone val zonder custom getter is duidelijker als de opgeslagen waarde niet berekend of gevalideerd hoeft te worden.
09Visibility modifiers
Wat is het? public is overal toegankelijk, private alleen binnen de declaratie of class, protected in de class en subclasses, en internal binnen dezelfde module.
Hoe werkt het? public is overal toegankelijk, private alleen binnen de declaratie of bestandscontext, protected binnen de class en subclasses, internal binnen dezelfde module.
class Monster(val name: String) {
private var hp = 100
fun takeDamage(amount: Int) {
require(amount >= 0)
hp = (hp - amount).coerceAtLeast(0)
}
fun status(): String = "$name: $hp HP"
}
fun main() {
val pyron = Monster("Pyron")
pyron.takeDamage(18)
println(pyron.status())
}Pyron: 82 HPWaarom deze aanpak? Niet iedere helper of muterende property hoeft buiten het monstermodel zichtbaar te zijn.
Wanneer kies je iets anders? Houd zichtbaarheid zo beperkt mogelijk; internal is modulebreed en geen beveiligingsmechanisme.
10Encapsulation
Wat is het? Encapsulation beschermt de interne toestand van een object. Externe code gebruikt gecontroleerde functies in plaats van willekeurig properties te wijzigen.
Hoe werkt het? Maak een waarde van buitenaf leesbaar, maar verbied directe wijziging via private set. Laat alleen gecontroleerde methoden de toestand veranderen.
class Monster(val name: String, maxHp: Int) {
var hp: Int = maxHp
private set
fun takeDamage(damage: Int) {
require(damage >= 0)
hp = (hp - damage).coerceAtLeast(0)
}
}
fun main() {
val pyron = Monster("Pyron", 100)
pyron.takeDamage(18)
println("${pyron.name}: ${pyron.hp} HP")
}Pyron: 82 HPWaarom deze aanpak? Damage kan niet negatief zijn en HP kan niet onder nul zakken.
Wanneer kies je iets anders? Voor onveranderlijke waardedata is een val vaak beter dan een var met private set. Voor de basisstats kiezen we daarom immutable Stats.
11Backing properties
Wat is het? Een private backing property bewaart wijzigbare data; een publieke read-only property maakt die data alleen leesbaar. Dit is een alternatief voor private set.
Hoe werkt het? Een private mutable property met een publieke read-only getter geeft een bewust afgeschermde blik op de interne toestand.
class Monster(private val maxHp: Int) {
private var _currentHp = maxHp
val currentHp: Int get() = _currentHp
fun takeDamage(amount: Int) {
require(amount >= 0)
_currentHp = (_currentHp - amount).coerceAtLeast(0)
}
}
fun main() {
val monster = Monster(100)
monster.takeDamage(18)
println(monster.currentHp)
}82Waarom deze aanpak? De actuele HP is zichtbaar, maar mag alleen via takeDamage() veranderen.
Wanneer kies je iets anders? private set is meestal korter dan een afzonderlijke _property plus getter. Een backing property is nuttig bij verschillende publieke en interne types.
12Abstraction
Wat is het? Abstraction laat alleen relevante details zien. De gebruiker van een class hoeft niet te weten hoe de berekening of interne opslag precies werkt.
Hoe werkt het? Abstraction laat alleen de noodzakelijke interface van een begrip zien: wat het object kan, zonder de interne berekening steeds bloot te leggen.
class Monster(val attack: Int) {
fun damageAgainst(defense: Int): Int = maxOf(1, attack - defense)
}
fun main() {
val pyron = Monster(30)
println(pyron.damageAgainst(12))
}18Waarom deze aanpak? Een battle-service kan attack() aanroepen zonder de implementatie van de damageberekening te kennen.
Wanneer kies je iets anders? Een pure top-level functie is geschikter voor een berekening die geen objecttoestand nodig heeft, zoals de latere DamageCalculator.
13Interfaces
Wat is het? Een interface beschrijft een contract: welke functies of properties een type moet aanbieden. Verschillende classes kunnen hetzelfde contract implementeren.
Hoe werkt het? Een interface definieert een contract. Een class implementeert de functies met override. Via de interface kan de aanroeper verschillende implementaties gebruiken.
interface Attackable {
fun calculateDamage(defense: Int): Int
}
class Monster(private val attack: Int) : Attackable {
override fun calculateDamage(defense: Int): Int =
maxOf(1, attack - defense)
}
fun main() {
val attacker: Attackable = Monster(30)
println(attacker.calculateDamage(12))
}18Waarom deze aanpak? Andere onderdelen van het spel kunnen met Attackable werken zonder een concrete monsterclass te kennen.
Wanneer kies je iets anders? Een interface voegt waarde toe bij uitwisselbare implementaties. Voeg hem niet toe als er nooit meerdere gedragingen nodig zijn.
14Interface versus abstract class
Wat is het? Een interface legt vooral gedrag vast; een abstract class kan ook gedeelde constructorwaarden en toestand beheren. Beide kunnen abstracte en concrete members hebben.
Hoe werkt het? Een interface legt vooral een contract vast; een abstract class kan gedeelde toestand en implementatie bevatten. Een concrete subclass moet abstracte leden implementeren.
interface Attackable {
fun attack(): Int
}
abstract class Monster(val name: String) : Attackable {
fun introduce() = println("I am $name")
}
class FireMonster(name: String) : Monster(name) {
override fun attack(): Int = 30
}
fun main() {
val pyron = FireMonster("Pyron")
pyron.introduce()
println(pyron.attack())
}I am Pyron
30Waarom deze aanpak? Een Monster kan een gedeelde basisclass krijgen en daarnaast Attackable implementeren.
Wanneer kies je iets anders? Composition is vaak eenvoudiger dan inheritance. De echte basisgame heeft daarom geen FireMonster-subclass nodig.
15Enum classes
Wat is het? Een enum class vertegenwoordigt een vaste verzameling benoemde waarden. De compiler voorkomt dat willekeurige strings als element worden gebruikt.
Hoe werkt het? Een enum class beperkt waarden tot vooraf gedefinieerde entries. Een enum is geschikt als een element alleen FIRE, WATER of WIND mag zijn.
enum class Element { FIRE, WATER, WIND }
fun main() {
val element = Element.FIRE
println(element)
println(Element.entries.size)
}FIRE
3Waarom deze aanpak? De toegestane monsterelementen zijn FIRE, WATER en WIND.
Wanneer kies je iets anders? Een sealed hiërarchie past beter als ieder type wezenlijk verschillend gedrag of data heeft; een enum is eenvoudiger voor drie vaste labels.
16Enum properties
Wat is het? Een enum kan constructorparameters, properties en functies hebben. Elke enumwaarde draagt dan eigen vaste metadata.
Hoe werkt het? Elke enum-entry kan constructorargumenten krijgen. Daarmee heeft bijvoorbeeld FIRE een presentatienaam zonder losse when-vertakkingen.
enum class Element(val displayName: String) {
FIRE("Fire"), WATER("Water"), WIND("Wind")
}
fun main() {
println(Element.FIRE.displayName)
println(Element.WATER.displayName)
}Fire
WaterWaarom deze aanpak? We geven elk element een leesbare naam zonder losse if- of when-constructies.
Wanneer kies je iets anders? Zet alleen stabiele, elementgebonden data in de enum; dynamische spelregels horen in afzonderlijke code.
17Data classes
Wat is het? Een data class is bedoeld voor waarden en gegevens. Kotlin genereert onder andere equals(), hashCode(), toString(), componentN() en copy() op basis van de primary-constructorproperties.
Hoe werkt het? Een data class genereert value-based equals, hashCode, toString, componentN en copy op basis van de primary-constructorproperties.
data class Stats(val hp: Int, val attack: Int, val defense: Int, val speed: Int)
fun main() {
val stats = Stats(100, 30, 10, 110)
println(stats)
}Stats(hp=100, attack=30, defense=10, speed=110)Waarom deze aanpak? Stats is een goede data class; een Monster met veranderende HP en identiteit modelleren we als gewone class.
Wanneer kies je iets anders? Een gewone class is passend wanneer identiteit of levenscyclus belangrijker is dan value equality, zoals bij Monster.
18Data class-functionaliteit
Wat is het? Bij een data class werken equals(), hashCode(), toString(), copy() en componentN() op de properties in de primary constructor. copy() maakt een shallow copy.
Hoe werkt het? copy maakt een nieuw object met optioneel aangepaste velden. equals en hashCode vergelijken datawaarden; component1 levert het eerste constructorveld.
data class Stats(val hp: Int, val attack: Int)
fun main() {
val original = Stats(100, 30)
val boosted = original.copy(attack = 35)
println(original == Stats(100, 30))
println(original.hashCode() == Stats(100, 30).hashCode())
println(boosted)
println(original.component1())
}true
true
Stats(hp=100, attack=35)
100Waarom deze aanpak? We kunnen een gewijzigde statset maken zonder het origineel aan te passen.
Wanneer kies je iets anders? copy is shallow: geneste mutable objecten worden niet automatisch diep gekopieerd. Vermijd dat misverstand.
19Destructuring declarations
Wat is het? Met destructuring haal je meerdere componenten uit een object. Bij data classes gebruikt Kotlin hiervoor automatisch gegenereerde componentN()-functies.
Hoe werkt het? Destructuring gebruikt automatisch gegenereerde componentN-functies om onderdelen van een object in afzonderlijke lokale variabelen te plaatsen.
data class Stats(val hp: Int, val attack: Int)
fun main() {
val stats = Stats(100, 30)
val (hp, attack) = stats
println("HP: $hp, Attack: $attack")
}HP: 100, Attack: 30Waarom deze aanpak? We kunnen HP en Attack overzichtelijk uit een Stats-object halen.
Wanneer kies je iets anders? Gebruik named property access (stats.hp) als dit duidelijker leest dan een lange destructuringlijst.
20Equality: == versus ===
Wat is het? == vergelijkt waarden via equals(); === vergelijkt of twee referenties exact naar hetzelfde object wijzen. Bij data classes is value equality standaard gegenereerd.
Hoe werkt het? == roept gelijkheidsvergelijking via equals aan; === test of twee referenties naar exact hetzelfde object wijzen.
data class Stats(val hp: Int)
fun main() {
val first = Stats(100)
val second = Stats(100)
val alias = first
println(first == second)
println(first === second)
println(first === alias)
}true
false
trueWaarom deze aanpak? Twee Stats-objecten kunnen inhoudelijk gelijk zijn, zonder dezelfde instantie te zijn.
Wanneer kies je iets anders? Gebruik == voor gewone waardevergelijkingen; === alleen wanneer objectidentiteit echt relevant is.
21Het keyword this
Wat is het? this verwijst naar de huidige instantie. Het is nuttig om een property te onderscheiden van een parameter met dezelfde naam.
Hoe werkt het? Met this verwijs je expliciet naar de huidige instantie. Dat is vooral nuttig bij naamconflicten tussen parameter en property.
class Monster(name: String) {
var name: String = name
private set
fun rename(name: String) {
this.name = name
}
}
fun main() {
val monster = Monster("Pyron")
monster.rename("Pyron Prime")
println(monster.name)
}Pyron PrimeWaarom deze aanpak? Een monster kan een nieuwe naam krijgen via een gecontroleerde methode.
Wanneer kies je iets anders? Als veldnamen verschillend zijn is this vaak niet nodig. Het expliciet gebruiken is geen verplichting.
22Sealed classes
Wat is het? Een sealed class begrenst welke directe subtypes bestaan. In een when-expressie kan Kotlin controleren of alle mogelijke gevallen zijn behandeld.
Hoe werkt het? Een sealed class beperkt de toegestane subtypen. Een when over alle bekende varianten kan daardoor exhaustief zijn, zonder else.
sealed class BattleAction {
data class Attack(val damage: Int) : BattleAction()
data object Surrender : BattleAction()
}
fun describe(action: BattleAction): String = when (action) {
is BattleAction.Attack -> "Attack: ${action.damage}"
BattleAction.Surrender -> "Surrender"
}
fun main() {
println(describe(BattleAction.Attack(18)))
println(describe(BattleAction.Surrender))
}Attack: 18
SurrenderWaarom deze aanpak? Een battleactie heeft een beperkt aantal vormen: aanval of overgave.
Wanneer kies je iets anders? Een enum is compacter voor louter vaste waarden; een sealed class is interessanter als varianten verschillende gegevens bevatten.
23Sealed interfaces
Wat is het? Een sealed interface beperkt de directe implementaties. Anders dan een class heeft een interface geen constructor en geen eigen backing state.
Hoe werkt het? Een sealed interface heeft een gesloten groep implementaties, maar erft zelf niet van één vaste superclass. when kan ook hier alle varianten afhandelen.
sealed interface BattleOutcome {
data class Winner(val name: String) : BattleOutcome
data object Draw : BattleOutcome
}
fun message(outcome: BattleOutcome): String = when (outcome) {
is BattleOutcome.Winner -> "Winner: ${outcome.name}"
BattleOutcome.Draw -> "Draw"
}
fun main() {
println(message(BattleOutcome.Winner("Pyron")))
}Winner: PyronWaarom deze aanpak? We modelleren een vast aantal mogelijke uitkomsten van een battle.
Wanneer kies je iets anders? Een gewone interface is beter wanneer externe modules nieuwe implementaties moeten kunnen aanbieden.
24Functional interfaces en SAM
Wat is het? Een fun interface heeft precies één abstracte functie. Daardoor kan Kotlin een lambda omzetten naar een implementatie van die interface: SAM-conversie.
Hoe werkt het? Een fun interface heeft precies één abstracte functie en kan met een lambda worden ingevuld via SAM-conversie.
fun interface DamageRule {
fun calculate(attack: Int, defense: Int): Int
}
fun main() {
val normalAttack = DamageRule { attack, defense ->
maxOf(1, attack - defense)
}
println(normalAttack.calculate(30, 12))
}18Waarom deze aanpak? We geven een damageberekening door zonder een aparte implementatieclass te schrijven.
Wanneer kies je iets anders? Een lambda-parameter van een higher-order function is eenvoudiger wanneer een apart genoemd contract geen waarde toevoegt.
25Kotlin type hierarchy
Wat is het? Any is het basistype van alle niet-nullbare Kotlin-types. Subtyping laat je een specifiek object behandelen als algemener type. Nullable types staan hier los van: Any? kan ook null bevatten.
Hoe werkt het? Any is de bovenliggende niet-nullbare supertypefamilie; Any? laat ook null toe. Een subtype kan als algemener type worden behandeld.
open class Monster(val name: String)
class FireMonster(name: String) : Monster(name)
fun main() {
val monster: Monster = FireMonster("Pyron")
val general: Any = monster
val nullable: Any? = null
println((general as Monster).name)
println(nullable == null)
}Pyron
trueWaarom deze aanpak? Een generieke functie kan verschillende monsters en statwaarden ontvangen, maar moet bij Any? rekening houden met null.
Wanneer kies je iets anders? Gebruik concrete typen aan de rand van je domein, en interfaces/basistypen waar abstractie flexibiliteit geeft. Vermijd onveilige casts.
26MonsterBattles-domeinmodel
Wat is het? We verbinden de lesstof met het definitieve, compacte MonsterBattles-domein: Element, Stats en Monster. Dit voorbeeld toont objectcreatie en validatie, geen volledige battle-engine.
Hoe werkt het? De definitieve game gebruikt Element als enum, Stats als immutable data class en Monster als gewone class met identiteit. De constructor valideert waarden; de battle houdt actuele HP later lokaal bij.
enum class Element { FIRE, WATER, WIND }
data class Stats(
val hp: Int,
val attack: Int,
val defense: Int,
val speed: Int
) {
init {
require(hp > 0) { "HP must be positive" }
require(attack >= 0) { "Attack cannot be negative" }
require(defense >= 0) { "Defense cannot be negative" }
require(speed >= 0) { "Speed cannot be negative" }
}
}
class Monster(
val id: Int,
val name: String,
val element: Element,
val stats: Stats
) {
init {
require(id > 0) { "ID must be positive" }
require(name.isNotBlank()) { "Name is required" }
}
}
fun main() {
val pyron = Monster(1, "Pyron", Element.FIRE, Stats(100, 30, 10, 110))
val aquara = Monster(2, "Aquara", Element.WATER, Stats(110, 25, 12, 95))
println("${pyron.name} (${pyron.element}): ${pyron.stats.hp} HP")
println("${aquara.name} (${aquara.element}): ${aquara.stats.hp} HP")
println("First attacker: ${if (pyron.stats.speed >= aquara.stats.speed) pyron.name else aquara.name}")
}Pyron (FIRE): 100 HP
Aquara (WATER): 110 HP
First attacker: PyronWaarom deze aanpak? Deze drie types worden in Week 03, 04 en 07 opnieuw gebruikt. Actuele battle-HP hoort bewust niet thuis in het opgeslagen Monster-object.
Wanneer kies je iets anders? Voeg Attackable of elementsubclasses alleen toe als de opdracht of echt verschillend gedrag dat vraagt. In de basisgame blijft damage een pure functie in Week 03.
Monster heeft één Stats-object en verwijst naar één Element. Klik om de bestaande PNG volledig te bekijken. Plaats het bestand op assets/images/diagrams/02-class-diagram.png.Cheat Sheet
| Kotlin | Betekenis | Gebruik |
|---|---|---|
class Monster(...) | Gewone class met identiteit | Domeinmodel |
data class Stats(...) | Waardedata, value equality en copy() | Immutable basisstats |
enum class Element | Beperkte set vaste labels | FIRE, WATER, WIND |
init { require(...) } | Constructorinvarianten bewaken | Positieve ID en HP |
val / var | Niet opnieuw toewijzen / wel opnieuw toewijzen | Beheer mutability bewust |
private set | Publiek leesbaar, setter afgeschermd | Onderwijsvoorbeeld encapsulation |
private / internal / protected / public | Toegankelijkheid regelen | Kleine publieke API |
constructor(...) : this(...) | Secondary constructor en chaining | Alternatieve constructieroute |
interface / override | Contract implementeren | Uitwisselbaar gedrag |
abstract class | Onvolledige gedeelde basisclass | Bij gedeelde toestand of gedrag |
copy(), componentN() | Gegenereerde functies van data classes | Stats vergelijken / uitpakken |
== / === | Gelijkheid van inhoud / objectidentiteit | Waardedata versus referentie |
sealed class / sealed interface | Gesloten verzameling varianten | Exhaustive when |
fun interface | Eén abstracte functie (SAM) | Lambda als implementatie |
this | Verwijzing naar huidige instantie | Naamconflict oplossen |
Any / Any? | Algemeen type / inclusief null | Typehiërarchie |