2.5 Objektorientierte Programmierung

In den letzen beiden Abschnitten hatten wir uns mit Funktionen und Structs beschäftigt, aber hast Du jemals daran gedacht, Funktionen als Eigenschaft in einem Struct zu verwenden? In diesem Abschnitt werde ich Dir eine besondere Art von Funktionen vorstellen, die einen Reciever (engl. to recieve - empfangen) besitzen. Sie werden auch Methoden genannt.

Methoden

Sagen wir, Du definierst einen Struct mit dem Namen "Rechteck" und möchtest die Fläche ausrechnen. Normalerweise würden wir folgenden Code verwenden, um dies zu bewerkstelligen.

package main
import "fmt"

type Rechteck struct {
    breite, höhe float64
}

func Fläche(r Rechteck) float64 {
    return r.breite*r.höhe
}

func main() {
    r1 := Rechteck{12, 2}
    r2 := Rechteck{9, 4}
    fmt.Println("Fläche von r1: ", Fläche(r1))
    fmt.Println("Fläche von r2: ", Fläche(r2))
}

Das obere Beispiel kann die Fläche eines Rechtecks ermitteln. Dafür haben wir die Funktion Fläche() verwendet, aber es ist keine Methode des Structs Rechteck (welche mit einer Klasse in klassischen objektorientierten Sprachen wären). Die Funktion und der Struct sind voneinander unabhängig.

Soweit ist dies noch kein Problem. Wenn Du aber den Flächeninhalt eines Kreies, Quadrahts, Fünfecks oder einer anderen Form berechnen musst, brauchst Du dafür Funktionen mit einem sehr ähnlichen Namen,

Abbildung 2.8 Beziehung zwischen Funktionen und Structs

Offensichtlich ist dies nicht sehr praktisch. Auch sollte die Fläche eine Eigenschaft des Kreises oder Rechtecks sein.

Aus diesem Grund gibt es das Konzept der Methoden. Diese sind immer mit einem Datentyp verbunden. Sie haben den selben Syntax wie normale Funktionen, jedoch besitzen sie einen weiteren Parameter nach dem Schlüsselwort func, den Reciever.

Im selben Beispiel würde mit Rechteck.Fläche() die Methode des Structs Rechteck aufgerufen. Somit gehören länge, breite und Fläche() zum Struct.

Wie Rob Pike sagte:

"A method is a function with an implicit first argument, called a receiver."

"Eine Methode ist eine Funktion mit einem ersten, impliziten Argument, welches Reciever genannt wird."

Syntax einer Methode.

func (r RecieverTyp) funcName(Parameter) (Ergebnis)

Lass uns das Beispiel von vorhin umschreiben.

package main
import (
    "fmt"
    "math"
)

type Rechteck struct {
    breite, höhe float64
}

type Kreis struct {
    radius float64
}

func (r Rechteck) fläche() float64 {
    return r.breite*r.höhe
}

func (c Kreis) fläche() float64 {
    return c.radius * c.radius * math.Pi
}

func main() {
    r1 := Rechteck{12, 2}
    r2 := Rechteck{9, 4}
    c1 := Kreis{10}
    c2 := Kreis{25}

    fmt.Println("Fläche von r1 ist: ", r1.fläche())
    fmt.Println("Fläche von r2 ist: ", r2.fläche())
    fmt.Println("Fläche von c1 ist: ", c1.fläche())
    fmt.Println("Fläche von c2 ist: ", c2.fläche())
}

Anmerkungen zu der Nutzung von Methoden.

  • Beide Methoden haben den selben Namen, gehören jedoch verschiedenen Recievern an.
  • Methoden können auf die Eigenschaften des Recievers zugreifen.
  • Nutze ., um Methoden wie Eigenschaften aufzurufen.

Abbildung 2.9 Die Methoden sind in jedem Struct verschieden

Im oberen Beispiel ist die Methode Fläche() für Rechteck und Kreis zugänglich, sodass sie zwei verschiedene Reciever besitzt.

Bei der Nutzung von Methoden werden dieser Kopien der Werte eines Structs übergeben. Fläche() bekommt also nicht einen Zeiger zum Original übergeben, sondern nur eine Kopie dessen. Somit kann das Orginal nicht verändert werden. Ein * vor dem Reciver übergibt dageben den Zeiger und erlaubt damit auch Zugriff auf das Original.

Können Reciever nur in Verbindung mit Structs genutzt werden? Natürlich nicht. Jeder Datentyp kann als Reciever fungieren. Dies wird Dir vielleicht bei selbstdefinierten Datentypen etwas komisch vorkommen, aber mit den Structs haben wir das Selbe gemacht - einen eigenen Datentypen erstellt.

Das Folgende Schema kann genutzt werden, um einen neuen Datentypen zu definieren.

type Name Datentyp

Beispiele für selbstdefinierte Datentypen:

type alter int

type geld float32

type monate map[string]int

m := monate {
    "Januar":31,
    "Februar":28,
    ...
    "Dezember":31,
}

Ich hoffe, dass Du die Definition eigener Datentypen jetzt verstanden hast. Es ist typedef aus C sehr ähnlich. Im oberen Beispiel ersetzen wir int einfach durch alter.

Kommen wir zurück zu unseren Methoden.

Du kannst soviele Datentypen und dazugehörige Methoden erstellen, wie Du willst.

package main
import "fmt"

const(
    WEISS = iota
    SCHWARZ
    BLAU
    ROT
    GELB
)

type Farbe byte

type Box struct {
    breite, höhe, tiefe float64
    farbe Farbe
}

type BoxListe []Box // Ein Slice mit Elementen vom Typ Box

func (b Box) Volumen() float64 {
    return b.breite * b.höhe * b.tiefe
}

func (b *Box) SetzeFarbe(c Farbe) {
    b.farbe = c
}

func (bl BoxListe) HöchsteFarbe() Farbe {
    v := 0.00
    k := Farbe(WEISS)
    for _, b := range bl {
        if b.Volumen() > v {
            v = b.Volumen()
            k = b.farbe
        }
    }
    return k
}

func (bl BoxListe) MaleAlleSchwarzAn() {
    for i, _ := range bl {
        bl[i].SetzeFarbe(SCHWARZ)
    }
}

func (c Farbe) String() string {
    strings := []string {"WEISS", "SCHWARZ", "BLAU", "ROT", "GELB"}
    return strings[c]
}

func main() {
    boxes := BoxListe {
        Box{4, 4, 4, ROT},
        Box{10, 10, 1, GELB},
        Box{1, 1, 20, SCHWARZ},
        Box{10, 10, 1, BLAU},
        Box{10, 30, 1, WEISS},
        Box{20, 20, 20, GELB},
    }

    fmt.Printf("Wir haben %d Boxen in unserer Liste\n", len(boxes))
    fmt.Println("Das Volumen der Ersten beträgt ", boxes[0].Volumen(), "cm³")
    fmt.Println("Die Farbe der letzen Box ist",boxes[len(boxes)-1].farbe.String())
    fmt.Println("Die größte Box ist ", boxes.HöchsteFarbe().String())

    fmt.Println("Malen wir sie alle schwarz an")
    boxes.MaleAlleSchwarzAn()
    fmt.Println("Die Farbe der zweiten Box lautet ", boxes[1].farbe.String())

    fmt.Println("Offensichtlich ist die größte Box ", boxes.HöchsteFarbe().String())
}

Wir hatten ein paar Konstanten und selbstdefinierte Datentypen erstellt.

  • Nutze Farbe als Alias für byte.
  • Wir definierten den Struct Box mit den Eigenschaften breite, länge, tiefe und farbe.
  • Wir definierten einen Slice BoxListe mit Elementen vom Typ Box.

Dann haben wir Methoden für unsere selbsterstellten Datentypen hinzugefügt.

  • Volumen() nutzt Box als Reciever und gibt das Volumen einer Box.
  • SetzeFarbe(c Farbe) ändert die Farbe einer Box.
  • GrößteFarbe() gibt die Box mit dem größten Volumen zurück.
  • MaleAlleSchwarzAn() setzt die Farbe aller Boxen auf schwarz.
  • String() benutzt Farbe als Reciever und gibt den Farbnamen als String zurück.

Ist es nicht einfacher, Wörter zum Beschreiben unserer Anforderungen zu benutzen? Oftmals definieren wir unsere Anforderungen schon vor dem Programmieren.

Zeiger als Reciever

Werfen wir einen näheren Blick auf die Methode SetzeFarbe(). Ihr Reciever ist der Zeiger mit dem Verweis zu einer Box. Warum benutzen wir hier einen Zeiger? Wie bereits erwähnt, erhälst Du mit *Box Zugriff auf das Original und kannst es somit ändern. Nützten wir keinen Zeiger, so hätte die Methode nur eine Kopie des Wertes übergeben bekommen.

Wenn wir einen Reciever als ersten Parameter einer Methode sehen, dürfte ihr Zweck leicht zu verstehen sein.

Du fragst Dich bestimmt, warum wir nicht einfach (*b).Farbe=c verwenden, statt b.Color=c in der SetzeFarbe() Methode. Beide Wege sind OK und Go weiß die erste Zuweisung zu interpretieren. Findest Du Go nun nicht auch faszinierend?

Des Weiteren fragst Du dich vielleicht auch, warum wir nicht (&bl[i]).SetzeFarbe(BLACK) in MaleAlleSchwarzAn() nutzen, so wie es in SetzeFarbe() der Fall ist. Nochmals, beide Varianten sind OK und Go weiß damit umzugehen.

Vererbung von Methoden

Wir haben die Vererbung bzw. das Einbetten von Eigengeschaften bereits im letzen Abschnitt kennengelernt. Ähnlich funktioniert auch das Einbetten von Methoden. Wenn ein Struct eigene Methoden hat und es in ein weiteres Struct eingebettet wird, so werden die Methoden wie die Eigenschaften mit eingebettet, also vererbt.

package main
import "fmt"

type Mensch struct {
    name string
    alter int
    telefon string
}

type Student struct {
    Mensch // Eingebetter Struct als Eigenschaft ohne Namen
    schule string
}

type Mitarbeiter struct {
    Mensch 
    unternehmen string
}

// Definiere eine Methode für Mensch
func (h *Mensch) SagHallo() {
    fmt.Printf("Hallo, ich bin %s. Du erreichst mich unter %s\n", h.name, h.telefon)
}

func main() {
    mark := Student{Mensch{"Mark", 25, "222-222-YYYY"}, "MIT"}
    sam := Mitarbeiter{Mensch{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}

    mark.SagHallo()
    sam.SagHallo()
}

Das Überladen von Methoden

Wenn wir für Mitarbeiter eine eigene Methode SagHallo() erstellen wollen, wird die Methode SagHallo() von Mensch durch die von Mitarbeiter überladen.

package main
import "fmt"

type Mensch struct {
    name string
    alter int
    telefon string
}

type Student struct {
    Mensch 
    schule string
}

type Mitarbeiter struct {
    Mensch 
    unternehmen string
}

func (h *Mensch) SagHallo() {
    fmt.Printf("Hallo, ich bin %s und Du erreicht mich unter %s\n", h.name, h.telefon)
}

func (e *Mitarbeiter) SagHallo() {
    fmt.Printf("Hallo, ich bin %s, arbeite bei %s. Du erreicht mich unter %s\n", e.name,
        e.unternehmen, e.telefon) // Du kannst die Argumente auch auf zwei Zeilen verteilen.
}

func main() {
    mark := Student{Mensch{"Mark", 25, "222-222-YYYY"}, "MIT"}
    sam := Mitarbeiter{Mensch{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}

    mark.SagHallo()
    sam.SagHallo()
}

Nun bist Du bereit, Dein eigenes, objektorientiers Programm zu schreiben. Auch Methoden unterliegen der Regel, dass die Groß- und Kleinschreibung des ersten Buchstaben über die Sichtbarkeit (öffentlich oder privat) entscheidet.

results matching ""

    No results matching ""