Fachkonzept - Datenkapselung
Daten schützen
Wenn man den Zugriff auf Attribute unkontrolliert zulässt, besteht die Gefahr,
dass Benutzer der Klasse Attributwerte auf unzulässige Art verändern.
Wenn z.B. in einer Klasse Mensch ein Attribut alter
existiert, dann sollte es nicht möglich sein, das Alter auf einen negativen Wert
zu setzen.
Datenkapselung bedeutet, dass ein Objekt den direkten Zugriff auf seine Daten verhindert und nur indirekten, kontrollierten Zugriff erlaubt.
In Kotlin geschieht dies automatisch, da für jedes Attribut, für das Daten gespeichert
werden müssen, ein sogenanntes Backing Field generiert wird.
Auf dieses Backing Field kann nur über einen Getter und
einen Setter zugegriffen werden und muss dort
mit dem Namen field angesprochen werden.
Wenn man selbst keinen Getter oder Setter definiert, dann wird
automatisch ein Standard-Getter und -Setter vom Compiler hinzugefügt.
Dieser hat z.B. folgende Form:
class Konto {
var kontostand: Double = 0.0
get() {
return field // Zugriff muss über 'field' erfolgen
}
set(value) { // 'value' als Name des Parameters ist nur Konvention
field = value
}
}
fun main() {
val konto = Konto()
konto.kontostand = 100.0 // Im Hintergrund wird automatisch 'set' aufgerufen
println(konto.kontostand) // Im Hintergrund wird automatisch 'get' aufgerufen
}
Zeile 3 bis Zeile 8 könnten auch weggelassen werden und würden dann vom Compiler automatisch beim Kompilieren hinzugefügt werden.
Standardfälle
Die Angabe von benutzerdefinierten Gettern und Settern bietet vielfältige Möglichkeiten, um den Zugriff auf Daten zu kontrollieren. Beim Zugriff auf Attribute gibt es drei Fälle, die besonders häufig vorkommen.
Keine benutzerdefinierten Getter und Setter
Wenn der Zugriff auf Daten unkritisch ist, nutzen wir in Kotlin val (nur
lesen) oder var (lesen und schreiben), um ein Attribut zu definieren
und schreiben keine eigenen Getter und Setter.
Die Entscheidung, ob ein Attribut nur lesbar oder auch schreibbar sein soll, ist
eigentlich auch schon eine Art Datenkapselung.
Man sollte nur die Attribute mit var definieren, die auch wirklich
verändert werden müssen.
Für eine Spielfigur, bei welcher die Rolle nach der Erzeugung nicht
mehr veränderbar - also nur noch lesbar - sein soll und deren Level keinen Einschränkungen
unterliegt, könnten wir beispielsweise folgende Klasse definieren:
class Spielfigur(
val rolle: String,
var level: Int
) {
// ... weitere Attribute und Methoden
}
fun main() {
val held = Spielfigur("Magier", 1)
held.rolle = "Krieger" // Fehler: val kann nicht verändert werden
println(held.rolle) // Automatisch erzeugter Getter wird aufgerufen
held.level = 2 // Automatisch erzeugter Setter wird aufgerufen
println(held.level) // Automatisch erzeugter Getter wird aufgerufen
}
Für beide Attribute werden Speicherplätze angelegt, in denen die Daten gespeichert werden. In Kotlin werden diese Speicherplätze Backing Fields genannt.
Kontrolle beim Schreiben: Setter
In einigen Fällen wollen wir verhindern, dass unkontrolliert Werte in Attribute
geschrieben werden.
Ein Mensch beispielsweise kann nicht -5 Jahre alt sein. Hier nutzen wir einen
benutzerdefinierten Setter.
Wir prüfen den Wert, bevor wir ihn wirklich im Backing
Field speichern, das wir über den Bezeichner field ansprechen.
class Mensch {
var alter: Int = 0
set(wert) {
if (wert >= 0) {
field = wert // 'field' ist der tatsächliche Speicherplatz der Variable
} else {
field = 0 // Falls der Wert negativ ist, wird 0 gespeichert
// alternativ hätte man den else-Zweig weglassen können, um den
// schon gespeicherten Wert nicht zu überschreiben
}
}
}
fun main() {
val user = Mensch()
user.alter = 10 // benutzerdefinierter Setter wird aufgerufen und speichert 10
println(user.alter) // automatisch erzeugter Getter wird aufgerufen
user.alter = -5 // benutzerdefinierter Setter wird aufgerufen und speichert 0
println(user.alter) // automatisch erzeugter Getter wird aufgerufen
}
Kontrolle beim Lesen: Getter
Analog zum Schreiben können wir auch beim Lesen den Zugriff steuern. Wenn man einen in einem Backing field gespeicherten Wert hat, kann dieser meistens unproblematisch gelesen werden. In solchen Fällen benutzt man normalerweise keinen benutzerdefinierten Getter.
In einigen Fällen gibt es aber Eigenschaften von Objekten, die nicht in einem Backing field gespeichert werden, sondern sich aus anderen Daten berechnen lassen. Man nennt solche Eigenschaften abgeleitete Attribute oder computed properties. Hier bietet sich die Verwendung eines benutzerdefinierten Getters an.
Ein Rechteck kennt seine Breite und Höhe – die Fläche ergibt sich daraus automatisch bei jeder Abfrage.
class Rechteck(val breite: Double, val hoehe: Double) {
val flaeche: Double
get() = breite * hoehe // 'flaeche' wird jedes Mal neu berechnet
}
fun main() {
val rechteck = Rechteck(2.0, 3.0)
println(rechteck.flaeche) // 6.0
rechteck.flaeche = 10.0 // Zuweisung nicht möglich
}
Abgeleitete Attribute stellt man im Klassendiagramm mit einem vorangestellten
/ dar.
Optionale Vertiefungen
Setter und Konstruktoren
Wenn du ein Attribut direkt im Konstruktor - also in den Klammern hinter dem Klassennamen - definierst, kannst du dort keinen set-Block anfügen. Um die Logik des Setters auch für den ersten Wert zu nutzen, der beim Erstellen des Objekts übergeben wird, geht man so vor:
- Man nimmt den Wert im Konstruktor nur als Parameter entgegen (ohne val oder var).
- Man definiert das eigentliche Attribut im Körper der Klasse und definiert einen Setter.
- Man schreibt einen init-Block und weist darin den Parameter dem Attribut zu. Ein init-Block wird automatisch beim Erzeugen eines Objekts aufgerufen.
class User(startAlter: Int) { // 'startAlter' ist Parameter, kein Attribut
var alter: Int = 0
set(value) {
if(value >= 0) {
field = value
} else {
field = 0
}
}
init {
alter = startAlter // JETZT wird der Setter aufgerufen!
}
}
fun main() {
val user = User(-5)
println(user.alter)
}
Private Attribute und Methoden
In manchen Fällen möchte man, dass ein Attribut von außen gar nicht sichtbar ist.
Es soll nur intern innerhalb der Klasse genutzt werden. Dafür nutzt man das
Schlüsselwort private.
Man kann mit Hilfe des Schlüsselworts private auch Methoden verstecken.
Dies macht dann Sinn, wenn man Hilfsmethoden hat, die nur innerhalb der Klasse genutzt
werden.
class Tresor {
private var geheimPin: Int = 1234
fun pruefePin(eingabe: Int): Boolean {
// Interner Zugriff ist erlaubt:
erstelleBackup()
return eingabe == geheimPin
}
private fun erstelleBackup() {
// ... speichert Pin in einer Datei als Backup
}
}
fun main() {
val meinTresor = Tresor()
meinTresor.geheimPin = 4321 // 'geheimPin' ist hier unsichtbar -> Fehler
meinTresor.erstelleBackup() // 'erstelleBackup' ist hier unsichtbar -> Fehler
}
Man kann mit Hilfe von private auch nur den Setter unsichtbar machen.
Damit ergeben sich dann weitere Möglichkeiten für den Zugriff auf Attribute.
Das kann z.B. Sinn machen, wenn man Abhängigkeiten zwischen Attributen hat
und diese nur gemeinsam geändert werden sollen:
class Timer {
var min: Int = 0
private set
var max: Int = 0
private set
fun setzeBereich(neuesMin: Int, neuesMax: Int) {
if(neuesMin >= 0 && neuesMin < neuesMax) {
min = neuesMin
max = neuesMax
}
}
}
fun main() {
val timer = Timer()
timer.setzeBereich(10, 20) // Setzt intern min und max
println(timer.min) // 10
println(timer.max) // 20
timer.min = 15 // Fehler: Setter ist private
}