Der „Besucher“ ist eines der Verhaltensmuster, die im Lehrbuch
„Gang of Four“, „GoF“, „Design Patterns: Elemente wiederverwendbarer objektorientierter Software“ beschrieben sind ”) .
Kurz gesagt, die Vorlage kann nützlich sein, wenn Aktionen desselben Typs für eine Gruppe von Objekten unterschiedlichen Typs ausgeführt werden müssen, die nicht miteinander verbunden sind. Mit anderen Worten, erweitern Sie die Funktionalität dieser Reihe von Typen mit einer bestimmten Operation desselben Typs oder mit einer einzigen Quelle. Gleichzeitig sollten Struktur und Implementierung erweiterbarer Typen nicht beeinträchtigt werden.
Die Idee lässt sich am einfachsten anhand eines Beispiels erklären.
Ich möchte sofort einen Vorbehalt machen, dass das Beispiel fiktiv und für akademische Zwecke komponiert ist. Das heißt, Dieses Material soll die Rezeption von OOP einführen und keine hochspezialisierten Probleme diskutieren.Ich möchte auch darauf hinweisen, dass der Code in den Beispielen geschrieben wurde, um die Entwurfstechnik zu studieren. Ich bin mir seiner (Code-) Mängel und der Möglichkeiten bewusst, ihn für den Einsatz in realen Projekten zu verbessern.Beispiel
Angenommen, Sie haben einen Subtyp von
UITableViewController
, der mehrere Subtypen von
UITableViewCell
:
class FirstCell: UITableViewCell { } class SecondCell: UITableViewCell { } class ThirdCell: UITableViewCell { } class TableVC: UITableViewController { override func viewDidLoad() { super.viewDidLoad() tableView.register(FirstCell.self, forCellReuseIdentifier: "FirstCell") tableView.register(SecondCell.self, forCellReuseIdentifier: "SecondCell") tableView.register(ThirdCell.self, forCellReuseIdentifier: "ThirdCell") } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return FirstCell() return SecondCell() return ThirdCell() } }
Angenommen, Zellen unterschiedlicher Subtypen haben unterschiedliche Höhen.
Natürlich kann die Höhenberechnung direkt in die Implementierung jedes Zelltyps eingefügt werden. Was aber, wenn die Höhe der Zelle nicht nur von ihrem eigenen Typ abhängt, sondern auch von äußeren Bedingungen? Beispielsweise kann ein Zelltyp in verschiedenen Tabellen mit unterschiedlichen Höhen verwendet werden. In diesem Fall möchten wir absolut nicht, dass die
UITableViewCell
Unterklassen die Anforderungen ihrer "Superview" oder "View Controller" kennen.
Anschließend kann die Höhenberechnung in den
UITableViewController
Methoden durchgefĂĽhrt werden: Initialisieren
UITableViewCell
entweder die
UITableViewCell
mit dem Höhenwert oder
UITableViewCell
Instanz in einen bestimmten Subtyp um und geben Sie verschiedene Werte in der
tableView(_:heightForRowAt:)
Methode zurĂĽck
tableView(_:heightForRowAt:)
. Dieser Ansatz kann aber auch unflexibel werden und sich in eine lange Folge von "Wenn" -Operatoren oder ein sperriges "Schalter" -Konstrukt verwandeln.
Lösen des Problems mithilfe der Vorlage "Besucher"
Natürlich kann nicht nur die Vorlage „Besucher“ dieses Problem lösen, sondern er kann es auch ganz elegant tun.Dazu erstellen wir zunächst einen Typ, der tatsächlich ein „Besucher“ von Zelltypen und ein Objekt ist, dessen Aufgabe es ist, nur die Höhe der Tabellenzelle zu berechnen:
struct HeightResultVisitor { func visit(_ ell: FirstCell) -> CGFloat { return 10.0 } func visit(_ ell: SecondCell) -> CGFloat { return 20.0 } func visit(_ ell: ThirdCell) -> CGFloat { return 30.0 } }
Der Typ kennt jeden verwendeten Subtyp und gibt fĂĽr jeden den gewĂĽnschten Wert zurĂĽck.
Zweitens muss jeder Subtyp von
UITableViewCell
diesen "Besucher" "empfangen" können. Dazu deklarieren wir ein Protokoll mit einer solchen "Empfangs" -Methode, die von allen verwendeten Zelltypen implementiert wird:
protocol HeightResultVisitable { func accept(_ visitor: HeightResultVisitor) -> CGFloat } extension FirstCell: HeightResultVisitable { func accept(_ visitor: HeightResultVisitor) -> CGFloat { return visitor.visit(self) } } extension SecondCell: HeightResultVisitable { func accept(_ visitor: HeightResultVisitor) -> CGFloat { return visitor.visit(self) } } extension ThirdCell: HeightResultVisitable { func accept(_ visitor: HeightResultVisitor) -> CGFloat { return visitor.visit(self) } }
Innerhalb der
UITableViewController
Unterklasse kann die Funktionalität wie folgt verwendet werden:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let cell = tableView.cellForRow(at: indexPath) as! HeightResultVisitable return cell.accept(HeightResultVisitor()) }
Könnte besser sein!
Höchstwahrscheinlich möchten wir keinen solchen Code haben, der fest an eine bestimmte Funktionalität gebunden ist. Vielleicht möchten wir in der Lage sein, unseren Zellen neue Funktionen hinzuzufügen, aber nicht nur in Bezug auf ihre Höhe, sondern beispielsweise die Hintergrundfarbe, den Text in der Zelle usw., und nicht an den Typ des Rückgabewerts gebunden zu sein. Die Protokolle mit
associatedtype
Typ (
"Protokoll mit assoziiertem Typ", "PAT" ) helfen hier:
protocol CellVisitor { associatedtype T func visit(_ cell: FirstCell) -> T func visit(_ cell: SecondCell) -> T func visit(_ cell: ThirdCell) -> T }
Seine Implementierung für die Rückgabe der Zellenhöhe:
struct HeightResultCellVisitor: CellVisitor { func visit(_ cell: FirstCell) -> CGFloat { return 10.0 } func visit(_ cell: SecondCell) -> CGFloat { return 20.0 } func visit(_ cell: ThirdCell) -> CGFloat { return 30.0 } }
Auf der "Host" -Seite reicht es aus, nur ein gemeinsames Protokoll und dessen einzige Implementierung zu haben - fĂĽr jeden "Besucher" dieses Typs. Nur die "Besucher" -Parteien kennen die verschiedenen Arten von RĂĽckgabewerten.
Das Protokoll fĂĽr den "empfangenden Besucher" (im Buch "GoF" heiĂźt diese Seite "Element") des Typs hat folgende Form:
protocol Visitableell where Self: UITableViewCell { func accept<V: CellVisitor>(_ visitor: V) -> VT }
(Möglicherweise gibt es keine Einschränkungen für den Implementierungstyp. In diesem Beispiel ist es jedoch nicht sinnvoll, dieses Protokoll durch Unterklassen von
UITableViewCell
zu implementieren.)
Und seine Implementierung in Subtypen von
UITableViewCell
:
extension FirstCell: Visitableell { func accept<V: CellVisitor>(_ visitor: V) -> VT { return visitor.visit(self) } } extension SecondCell: Visitableell { func accept<V: CellVisitor>(_ visitor: V) -> VT { return visitor.visit(self) } } extension ThirdCell: Visitableell { func accept<V: CellVisitor>(_ visitor: V) -> VT { return visitor.visit(self) } }
Und schlieĂźlich verwenden Sie:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let cell = tableView.cellForRow(at: indexPath) as! Visitableell return cell.accept(HeightResultCellVisitor()) }
Auf diese Weise können wir mit verschiedenen Implementierungen des „Besuchers“ im Allgemeinen fast alles erstellen, und von der „empfangenden Seite“ wird nichts benötigt, um die neue Funktionalität zu unterstützen. Diese Partei wird nicht einmal wissen, was genau der "Gast" verliehen hat.
Ein weiteres Beispiel
Versuchen wir, die Hintergrundfarbe der Zelle mit einem ähnlichen "Besucher" zu ändern:
struct ColorResultCellVisitor: CellVisitor { func visit(_ cell: FirstCell) -> UIColor { return .black } func visit(_ cell: SecondCell) -> UIColor { return .white } func visit(_ cell: ThirdCell) -> UIColor { return .red } }
Ein Beispiel fĂĽr die Verwendung dieses Besuchers:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { cell.contentView.backgroundColor = (cell as! Visitableell).accept(ColorResultCellVisitor()) }
Etwas in diesem Code sollte verwirrend sein ... Am Anfang wurde gesagt, dass der „Besucher“ der Klasse von außen Funktionen hinzufügen kann. Ist es also möglich, alle Funktionen zum Ändern der Hintergrundfarbe der Zelle darin zu "verbergen" und nicht nur den Wert daraus zu erhalten? Du kannst. Dann nimmt der
associatedtype
Typ den Wert
Void
(aka ()
- ein leeres Tupel) an :
struct BackgroundColorSetter: CellVisitor{ func visit(_ cell: FirstCell) { cell.contentView.backgroundColor = .black } func visit(_ cell: SecondCell) { cell.contentView.backgroundColor = .white } func visit(_ cell: ThirdCell) { cell.contentView.backgroundColor = .red } }
Verwendung:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { (cell as! Visitableell).accept(BackgroundColorSetter()) }
Anstelle einer Schlussfolgerung
Das Muster mag Ihnen vielleicht auf den ersten Blick gefallen, Sie müssen es jedoch vorsichtig verwenden. Sein Erscheinen im Code kann oft ein Zeichen für allgemeinere Mängel in der Architektur sein. Vielleicht versuchen Sie, Dinge zu verbinden, die nicht verbunden werden sollten. Vielleicht lohnt es sich, die zusätzliche Funktionalität auf die eine oder andere Weise um eine höhere Abstraktionsebene zu erhöhen.
Auf die eine oder andere Weise hat fast jedes Muster seine Vor- und Nachteile, und bevor Sie es verwenden, sollten Sie immer bewusst denken und eine Entscheidung treffen. Muster sind einerseits eine Möglichkeit, Programmiertechniken zu verallgemeinern, um das Lesen und die Diskussion von Code zu erleichtern. Und auf der anderen Seite - ein Weg, um ein Problem zu lösen (manchmal künstlich eingeführt). Und natürlich bringen Sie den Code auf keinen Fall fanatisch in alle bekannten Muster, nur weil sie verwendet werden.
Ich denke ich bin fertig! Alles schöne Code und weniger "Bugs"!
Meine anderen Artikel zu Designmustern: