Wenn wir über Programmierung sprechen, meinen wir normalerweise das Schreiben einer Reihe von Funktionen, die einige Daten ändern und mit ihnen interagieren. Objektorientierte Programmierung (OOP) ist ein Programmiermodell, das sich stattdessen auf „Objekte“ konzentriert, die Daten enthalten und mit denen einige relevante Funktionen verknüpft sind. Die objektorientierte Programmierung besteht aus vier Säulen: Vererbung, Kapselung, Polymorphismus und Abstraktion. In diesem Blog werfen wir anhand von Beispielen einen Blick darauf, wie Sie jede davon in Golang implementieren können. Eine gewisse grundlegende Vorstellung von OOP wird empfohlen, aber wenn nicht, werde ich eine kurze Einführung in die Bedeutung aller vier Säulen geben.
Die Kernidee der objektorientierten Programmierung lässt sich in diesen Stichpunkten zusammenfassen:
Schauen wir uns einen Code in Golang an, um diese drei Konzepte zu verstehen:
package main import "fmt" type Batman struct { actor string year int } func (b Batman) SayImBatman() { fmt.Printf("I'm %s and I'm Batman from year %d\n", b.actor, b.year) } func main() { b1 := Batman{actor: "Michael Keaton", year: 1989} b2 := Batman{actor: "Christian Bale", year: 2005} b1.SayImBatman() b2.SayImBatman() }
In Golang sind Klassen nichts anderes als von uns definierte Typen. Diese Typen müssen nicht unbedingt eine Struktur sein, sind es aber normalerweise, da wir in OOP mit einer Sammlung von Daten arbeiten, die von jedem Typ sein können (String, Int usw.).
Klassen sind Blaupausen für Objekte. Immer wenn Sie eine Klasse instanziieren, wird ein Objekt gebildet. In diesem Beispiel sind b1 und b2 Objekte der Batman-Klasse.
Die SayImBatman-Funktion kann für jedes Objekt der Klasse aufgerufen werden. Da sie an die Batman-Klasse gebunden ist, wird sie nicht als reguläre Funktion, sondern als Methode der Klasse bezeichnet.
Ich denke, dies sollte die Grundlagen von OOP so weit klären, dass Sie mit dem nächsten Abschnitt fortfahren können, in dem wir uns die vier Säulen von OOP ansehen.
Vererbung führt in die Konzepte der Klassen Eltern und Kind in OOP ein. Eine untergeordnete Klasse ist eine von einer übergeordneten Klasse abgeleitete Klasse und erbt alle ihre Methoden und Eigenschaften (Daten). Schauen wir uns einen Code an, der uns hilft, dies zu verstehen:
package main import "fmt" type Hero struct { team string } type Batman struct { Hero name string } type Ironman struct { Hero power int } func (h Hero) SayTeam() { fmt.Println("My Team is", h.team) } func (b Batman) SayImBatman() { fmt.Printf("I'm %s and I'm Batman\n", b.name) } func (i Ironman) SayPowerLevel() { fmt.Printf("I'm Ironman and my powerlevel is %d\n", i.power) } func main() { b1 := Batman{Hero{team: "Justice League"}, "Christian Bale"} i1 := Ironman{Hero{team: "Avengers"}, 23} b1.SayImBatman() b1.SayTeam() i1.SayPowerLevel() i1.SayTeam() }
In diesem Beispiel sind Batman und Ironman untergeordnete Klassen der übergeordneten Klasse Hero. Sie haben Zugriff auf die Eigenschaften ihrer übergeordneten Klasse, also Team, und deren Methoden, also SayTeam. Wie Sie bei der Deklaration der b1- und i1-Instanzen sehen können, geben wir die Eigenschaften der übergeordneten Klasse sowie deren spezifische Eigenschaften für die jeweiligen Klassen an. Beide können die SayTeam-Methode aufrufen, die in der übergeordneten Klasse definiert ist. Sie verfügen aber auch über separate Eigenschaften und Methoden, die für jeden von ihnen einzigartig sind.
Golang implementiert die Vererbung mithilfe der Komposition (unter Verwendung einer Struktur innerhalb einer Struktur). Es verfügt nicht über eine integrierte klassenbasierte Vererbung wie andere OOP-Sprachen wie C oder Java.
Kapselung ist das Prinzip, die internen Eigenschaften eines Objekts zu verbergen und nicht zuzulassen, dass sie direkt geändert werden. Stattdessen ist es auf die Bereitstellung von Methoden zum Abrufen und Aktualisieren dieser Eigenschaften angewiesen. Schauen wir uns ein Beispiel an, um dies besser zu verstehen:
package main import "fmt" type Batman struct { actor string year int } func (b Batman) SayImBatman() { fmt.Printf("I'm %s and I'm Batman from year %d\n", b.actor, b.year) } func main() { b1 := Batman{actor: "Michael Keaton", year: 1989} b2 := Batman{actor: "Christian Bale", year: 2005} b1.SayImBatman() b2.SayImBatman() }
package main import "fmt" type Hero struct { team string } type Batman struct { Hero name string } type Ironman struct { Hero power int } func (h Hero) SayTeam() { fmt.Println("My Team is", h.team) } func (b Batman) SayImBatman() { fmt.Printf("I'm %s and I'm Batman\n", b.name) } func (i Ironman) SayPowerLevel() { fmt.Printf("I'm Ironman and my powerlevel is %d\n", i.power) } func main() { b1 := Batman{Hero{team: "Justice League"}, "Christian Bale"} i1 := Ironman{Hero{team: "Avengers"}, 23} b1.SayImBatman() b1.SayTeam() i1.SayPowerLevel() i1.SayTeam() }
In Golang beginnen Eigenschaften und Methoden, die aus dem Paket exportiert werden, mit einem Großbuchstaben. Wenn wir im utils-Paket „actor“ und „year“ in Kleinbuchstaben definieren, stellen wir sicher, dass sie nicht direkt geändert werden können. Stattdessen müssen Sie, wie Sie in der Datei main.go sehen, die exportierten Methoden (die mit einem Großbuchstaben beginnen) – GetActor, SetActor usw. – verwenden, um sie abzurufen und zu ändern.
Darum geht es bei der Kapselung – sicherzustellen, dass Sie versehentliche Änderungen an Daten verhindern und stattdessen Methoden für die sichere Interaktion mit den Daten bereitstellen.
Eine Sache, die Ihnen auffallen wird, ist, dass wir in allen Methoden für die Batman-Klasse einen Zeigerempfänger *Batman anstelle eines Wertempfängers Batman verwenden, wie wir es in den früheren Beispielen getan haben. Dies liegt daran, dass wir die ursprüngliche Struktur in den Set-Methoden ändern möchten. Und in Golang gilt als Best Practice: Wenn einige Methoden einen Zeigerempfänger benötigen, sorgen Sie dafür, dass alle Methoden aus Konsistenzgründen einen Zeigerempfänger verwenden. Aus diesem Grund verwenden auch die Get-Methoden einen Zeigerempfänger, obwohl sie die ursprüngliche Struktur nicht ändern.
Außerdem ist noch etwas zu beachten: Nur weil wir einen Zeigerempfänger verwenden, müssen wir Folgendes nicht tun: (&b1).GetActor. In Golang müssen Funktionen mit einem Zeigerargument einen Zeiger annehmen, Methoden mit einem Zeigerempfänger können jedoch entweder einen Wert oder einen Zeiger als Empfänger annehmen.
TL;DR: Golang übersetzt b1.GetActor automatisch als (&b1).GetActor, da die GetActor-Methode einen Zeigerempfänger hat, würde GetActor(b1) jedoch nicht in GetActor(&b1) übersetzen, wenn GetActor eine normale Funktion übernommen hätte ein Zeigerargument.
Die nächsten beiden Säulen von OOP können zusammengefasst werden, da die Codebeispiele für sie ziemlich ähnlich aussehen würden. Polymorphismus bezieht sich auf die Programmierpraxis, bei der zwei verschiedene Objekte zweier verschiedener Klassen als Objekte derselben gemeinsamen Oberklasse behandelt werden können. Das bedeutet, dass Sie dieselbe Funktion für zwei verschiedene Objekte aufrufen können, als wären sie Objekte derselben Klasse. Dies sollte Ihnen einen Eindruck von den beteiligten Schnittstellen vermitteln :)
Schauen wir uns einen Code an, um dies besser zu verstehen:
package main import "fmt" type Batman struct { actor string year int } func (b Batman) SayImBatman() { fmt.Printf("I'm %s and I'm Batman from year %d\n", b.actor, b.year) } func main() { b1 := Batman{actor: "Michael Keaton", year: 1989} b2 := Batman{actor: "Christian Bale", year: 2005} b1.SayImBatman() b2.SayImBatman() }
In diesem Beispiel können der StartFight-Funktion sowohl die b1- als auch die i1-Objekte übergeben werden, obwohl sie in keiner Beziehung zueinander stehen. Versuchen Sie zu verstehen, wie sich dies von der Vererbung unterscheidet, bei der die untergeordneten Klassen Zugriff auf die Methoden der übergeordneten Klasse hatten. In diesem Beispiel gibt es keine untergeordneten und übergeordneten Klassen (und es werden auch keine Methoden gemeinsam genutzt). Stattdessen werden zwei verschiedene Objekte von einer Funktion als gleich behandelt: Dies wird als Polymorphismus bezeichnet.
Dies kann nun auch als Beispiel für Abstraktion betrachtet werden. Abstraktion ist, wie der Name schon sagt, die Programmierpraxis, Implementierungsdetails zu verbergen und stattdessen nur Funktionen bereitzustellen, die die Dinge für Sie erledigen. In diesem Beispiel müssen Sie sich nicht darum kümmern, wie die Methoden der einzelnen Helden konfiguriert sind. Sie können die StartFight-Funktion jederzeit weiterhin verwenden, wenn Sie eine der Fight-Funktionen der Helden verwenden möchten. Auf diese Weise bleiben die Implementierungsdetails vor dem Benutzer verborgen und nur die wesentlichen Details werden offengelegt.
Um nun auf den Polymorphismus zurückzukommen, gibt es zwei weitere häufige Beispiele, und zwar das Überschreiben und Überladen von Methoden.
Methodenüberschreibung bezieht sich auf untergeordnete Klassen, die ihre eigene Implementierung von Methoden definieren, die in der übergeordneten Klasse definiert sind. Diese Implementierung wird nun anstelle der Implementierung der ursprünglichen übergeordneten Klasse verwendet. Nehmen wir den Code, den wir zuvor für die Vererbung verwendet haben, und sehen wir uns an, wie er mit Methodenüberschreibung aussieht:
package main import "fmt" type Hero struct { team string } type Batman struct { Hero name string } type Ironman struct { Hero power int } func (h Hero) SayTeam() { fmt.Println("My Team is", h.team) } func (b Batman) SayImBatman() { fmt.Printf("I'm %s and I'm Batman\n", b.name) } func (i Ironman) SayPowerLevel() { fmt.Printf("I'm Ironman and my powerlevel is %d\n", i.power) } func main() { b1 := Batman{Hero{team: "Justice League"}, "Christian Bale"} i1 := Ironman{Hero{team: "Avengers"}, 23} b1.SayImBatman() b1.SayTeam() i1.SayPowerLevel() i1.SayTeam() }
Die Ausgabe dieses Programms ist:
//oops-in-go/utils/utils.go package utils type Batman struct { actor string year int } func (b *Batman) GetActor() string { return b.actor } func (b *Batman) GetYear() int { return b.year } func (b *Batman) SetActor(actor string) { b.actor = actor } func (b *Batman) SetYear(year int) { b.year = year }
Die Objekte der Batman-Klasse verwenden jetzt ihre eigene SayTeam-Methode anstelle der der übergeordneten Hero-Klasse. Da die Ironman-Klasse über keine eigene SayTeam-Methode verfügt, verwendet ihr Objekt weiterhin die Methode ihrer übergeordneten Klasse. Das bedeutet „Methodenüberschreibung“, dass untergeordnete Klassen die in der übergeordneten Klasse definierten Methoden „überschreiben“.
Dies bezieht sich darauf, dass dieselbe Funktion mehrere verschiedene Argumente annehmen kann. Diese Argumente können in Anzahl oder Typ unterschiedlich sein. Golang bietet zwei Möglichkeiten, dies zu erreichen: durch variable Funktionen und die andere durch Schnittstellen.
Sehen wir uns den Code für beide an, damit Sie ihn besser verstehen:
// oops-in-go/main.go package main import ( "fmt" "oops-in-go/utils" ) func main() { b1 := utils.Batman{} b1.SetActor("Michael Keaton") b1.SetYear(1989) fmt.Printf("I'm %s and I'm Batman from year %d\n", b1.GetActor(), b1.GetYear()) b1.SetActor("Christian Bale") b1.SetYear(2005) fmt.Printf("I'm %s and I'm Batman from year %d\n", b1.GetActor(), b1.GetYear()) }
Hier können Sie die listMembers-Funktion mit einer beliebigen Anzahl von Argumenten „überladen“.
package main import "fmt" type Hero interface { Fight() } type Batman struct { weapon string } type Ironman struct { weapon string } func (b Batman) Fight() { fmt.Printf("Batman hits with a %s\n", b.weapon) } func (i Ironman) Fight() { fmt.Printf("Ironman hits with a %s\n", i.weapon) } func StartFight(h Hero) { fmt.Println("Fight has started.") h.Fight() } func main() { b1 := Batman{"Batarang"} i1 := Ironman{"Repulsor rays"} StartFight(b1) StartFight(i1) }
Die Ausgabe dieses Programms ist:
package main import "fmt" type Hero struct { team string } type Batman struct { Hero name string } type Ironman struct { Hero power int } func (h Hero) SayTeam() { fmt.Println("My Team is", h.team) } func (b Batman) SayImBatman() { fmt.Printf("I'm %s and I'm Batman\n", b.name) } func (i Ironman) SayPowerLevel() { fmt.Printf("I'm Ironman and my powerlevel is %d\n", i.power) } func (b Batman) SayTeam() { fmt.Printf("I'm Batman and my team is %s\n", b.team) } func main() { b1 := Batman{Hero{team: "Justice League"}, "Christian Bale"} i1 := Ironman{Hero{team: "Avengers"}, 23} b1.SayImBatman() b1.SayTeam() i1.SayPowerLevel() i1.SayTeam() }
Hier „überladen“ wir die saySomething-Methode, um Argumente unterschiedlicher Art zu akzeptieren. Wir nehmen eine leere Schnittstelle als Argument, die ein beliebiger Typ sein kann, prüfen dann mithilfe eines Switch-Cases ihren Typ und geben die Ausgabe entsprechend aus.
Ich bin mir bewusst, dass dies eine lange Lektüre war, und wenn Sie bis zum Ende durchgehalten haben, möchte ich Sie wissen lassen, dass ich wirklich glücklich bin :) Ich hoffe aufrichtig, dass Sie viel Neues über objektorientierte Programmierung gelernt haben und wie man es in Golang implementiert. Ich schreibe auf meiner Website Blogs zu verschiedenen technischen Konzepten. Wenn Sie daran interessiert sind, neue Dinge zu lernen, empfehle ich Ihnen, sich für meinen Newsletter anzumelden.
Das obige ist der detaillierte Inhalt vonEinführung in die objektorientierte Programmierung (OOP) in Golang. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!