Architekturmuster "Builder" im Universum von "Swift" und "iOS" / "macOS"

Dieses Mal möchte ich ein wenig über ein anderes generatives Designmuster aus dem Arsenal der Gang of Four sprechen - „Builder“ . Es stellte sich heraus, dass ich im Laufe meiner (wenn auch nicht zu umfangreichen) Erfahrung ziemlich oft sah, dass das Muster im Java- Code im Allgemeinen und in Android- Anwendungen im Besonderen verwendet wurde. In „iOS“ -Projekten war das Muster für mich ziemlich selten, egal ob sie in „Swift“ oder „Objective-C“ geschrieben wurden . Trotz all seiner Einfachheit kann es sich in geeigneten Fällen als recht praktisch und, wie man sagen kann, als mächtig herausstellen.


Bild


Die Vorlage wird verwendet, um den komplexen Initialisierungsprozess zu ersetzen, indem das gewünschte Objekt Schritt für Schritt erstellt wird, wobei die Finalisierungsmethode am Ende aufgerufen wird. Die Schritte können optional sein und sollten keine strenge Aufrufsequenz haben.


Bild


Stiftungsbeispiel


In Fällen, in denen die gewünschte „URL“ nicht festgelegt ist, sondern aus Komponenten besteht (z. B. der Hostadresse und dem relativen Pfad zur Ressource), haben Sie wahrscheinlich den praktischen URLComponents Mechanismus aus der Foundation- Bibliothek verwendet.


URLComponents ist größtenteils nur eine Klasse, die viele Variablen kombiniert, in denen die Werte verschiedener URL-Komponenten gespeichert sind, sowie die url Eigenschaft, die die entsprechende URL für den aktuellen Komponentensatz zurückgibt. Zum Beispiel:


 var urlComponents = URLComponents() urlComponents.scheme = "https" urlComponents.user = "admin" urlComponents.password = "qwerty" urlComponents.host = "somehost.com" urlComponents.port = 80 urlComponents.path = "/some/path" urlComponents.queryItems = [URLQueryItem(name: "page", value: "0")] _ = urlComponents.url // https://admin:qwerty@somehost.com:80/some/path?page=0 

Tatsächlich ist der obige Anwendungsfall die Implementierung des Builder-Musters. In diesem Fall fungiert URLComponents als Builder selbst. Das Zuweisen von Werten zu seinen verschiedenen Eigenschaften ( scheme , host usw.) ist die schrittweise Initialisierung des zukünftigen Objekts, und das Aufrufen der url Eigenschaft ist wie eine Finalisierungsmethode.


In den Kommentaren kam es zu heftigen Kämpfen um die "RFC" -Dokumente, die die "URL" und "URI" beschreiben. Um genauer zu sein, schlage ich beispielsweise vor anzunehmen, dass wir nur über die "URL" von Remote-Ressourcen sprechen und diese nicht berücksichtigen "URL" -Schemata, wie beispielsweise "Datei".


Alles scheint in Ordnung zu sein, wenn Sie diesen Code selten verwenden und alle Feinheiten kennen. Aber was ist, wenn Sie etwas vergessen? Zum Beispiel etwas Wichtiges wie eine Hostadresse? Was wird Ihrer Meinung nach das Ergebnis der Ausführung des folgenden Codes sein?


 var urlComponents = URLComponents() urlComponents.scheme = "https" urlComponents.path = "/some/path" _ = urlComponents.url 

Wir arbeiten mit Eigenschaften, nicht mit Methoden, und es werden mit Sicherheit keine Fehler "rausgeworfen". Die url Eigenschaft "finalizing" gibt einen optionalen Wert zurück. Vielleicht erhalten wir also nil ? Nein, wir erhalten ein vollwertiges Objekt vom Typ URL mit einem bedeutungslosen Wert - "https: / some / path". Daher kam mir der Gedanke, das Schreiben meines eigenen "Builders" basierend auf der oben beschriebenen "API" zu üben.


(Es hätte ein "Emoji" "Fahrrad" geben sollen, aber "Habr" zeigt es nicht an)


Trotzdem halte ich URLComponents gute und bequeme „API“ zum Zusammenstellen von „URLs“ aus Komponenten und im Gegenteil zum „Parsen“ der Komponenten der bekannten „URLs“. Darauf basierend schreiben wir jetzt unseren eigenen Typ, der die "URL" aus den Teilen sammelt und die "API" besitzt (angenommen), die wir im Moment benötigen.


Erstens möchte ich die unterschiedliche Initialisierung beseitigen, indem ich allen erforderlichen Eigenschaften neue Werte zuweise. Stattdessen implementieren wir die Möglichkeit, eine Instanz des Builders zu erstellen und allen Eigenschaften Werte zuzuweisen, indem wir von der Kette aufgerufene Methoden verwenden. Die Kette endet mit einer Finalisierungsmethode, deren Ergebnis die entsprechende Instanz der URL . Vielleicht sind Sie auf Ihrer Lebensreise auf so etwas wie " StringBuilder in Java" gestoßen - wir werden uns jetzt um eine solche "API" bemühen.


Um Methodenschritte entlang der Kette aufrufen zu können, muss jeder von ihnen eine Instanz des aktuellen Builders zurückgeben, in der die entsprechende Änderung gespeichert wird. Aus diesem Grund und auch um das mehrfache Kopieren von Objekten und das Tanzen um mutating Methoden, insbesondere ohne nachzudenken, loszuwerden, werden wir unseren Builder zu einer Klasse erklären:


 final class URLBuilder { } 

Wir deklarieren Methoden, die die Parameter der zukünftigen "URL" angeben, unter Berücksichtigung der oben genannten Anforderungen:


 final class URLBuilder { private var scheme = "https" private var user: String? private var password: String? private var host: String? private var port: Int? private var path = "" private var queryItems: [String : String]? func with(scheme: String) -> URLBuilder { self.scheme = scheme return self } func with(user: String) -> URLBuilder { self.user = user return self } func with(password: String) -> URLBuilder { self.password = password return self } func with(host: String) -> URLBuilder { self.host = host return self } func with(port: Int) -> URLBuilder { self.port = port return self } func with(path: String) -> URLBuilder { self.path = path return self } func with(queryItems: [String : String]) -> URLBuilder { self.queryItems = queryItems return self } } 

Wir speichern die angegebenen Parameter in den privaten Eigenschaften der Klasse für die zukünftige Verwendung durch die Finalisierungsmethode.


Eine weitere Hommage an die „API“, auf der unsere Klasse basiert, ist die path Eigenschaft, die im Gegensatz zu allen benachbarten Eigenschaften nicht optional ist. Wenn kein relativer Pfad vorhanden ist, wird eine leere Zeichenfolge als Wert gespeichert.


Um diese endgültige Methode zu schreiben, müssen Sie über ein paar weitere Dinge nachdenken. Erstens hat die „URL“ einige Teile, ohne die sie, wie eingangs angegeben, keinen Sinn mehr ergibt - dies ist scheme und host . Wir haben den ersten mit dem Standardwert „ausgezeichnet“. Wenn wir ihn vergessen haben, erhalten wir höchstwahrscheinlich immer noch das erwartete Ergebnis.


Mit dem zweiten sind die Dinge etwas komplizierter: Es kann kein Standardwert zugewiesen werden. In diesem Fall gibt es zwei Möglichkeiten: Wenn für diese Eigenschaft kein Wert vorhanden ist, geben Sie entweder nil oder geben Sie einen Fehler aus, und lassen Sie den Clientcode selbst entscheiden, was damit geschehen soll. Die zweite Option ist komplizierter, ermöglicht es Ihnen jedoch, einen bestimmten Programmiererfehler explizit anzugeben. Vielleicht gehen wir zum Beispiel diesen Weg.


Ein weiterer interessanter Punkt betrifft die user und password : Sie sind nur dann sinnvoll, wenn sie gleichzeitig verwendet werden. Was aber, wenn ein Programmierer vergisst, einen dieser beiden Werte zuzuweisen?


Und wahrscheinlich ist das Letzte, was zu berücksichtigen ist, dass wir als Ergebnis der Finalisierungsmethode den Wert der url Eigenschaft von URLComponents haben URLComponents , und dies ist in diesem Fall nicht sehr nützlich, optional. Für jede Kombination von festgelegten Werten von nil Eigenschaften erhalten wir sie jedoch nicht. (Nur eine leere, gerade erstellte Instanz von URLComponents hat keinen Wert.) Um dies zu überwinden, können Sie verwenden ! - Bediener "erzwungenes Auspacken". Aber im Allgemeinen möchte ich seine Verwendung nicht fördern, daher abstrahieren wir in unserem Beispiel kurz vom Wissen über die Feinheiten von „Foundation“ und betrachten die diskutierte Situation als einen Systemfehler, dessen Auftreten nicht von unserem Code abhängt.


Also:


 extension URLBuilder { func build() throws -> URL { guard let host = host else { throw URLBuilderError.emptyHost } if user != nil { guard password != nil else { throw URLBuilderError.inconsistentCredentials } } if password != nil { guard user != nil else { throw URLBuilderError.inconsistentCredentials } } var urlComponents = URLComponents() urlComponents.scheme = scheme urlComponents.user = user urlComponents.password = password urlComponents.host = host urlComponents.port = port urlComponents.path = path urlComponents.queryItems = queryItems?.map { URLQueryItem(name: $0, value: $1) } guard let url = urlComponents.url else { throw URLBuilderError.systemError // Impossible? } return url } enum URLBuilderError: Error { case emptyHost case inconsistentCredentials case systemError } } 

Das ist vielleicht alles! Nun könnte eine explodierte Erstellung der „URL“ aus dem Beispiel am Anfang folgendermaßen aussehen:


 _ = try URLBuilder() .with(user: "admin") .with(password: "Qwerty") .with(host: "somehost.com") .with(port: 80) .with(path: "/some/path") .with(queryItems: ["page": "0"]) .build() // https://admin:Qwerty@somehost.com:80/some/path?page=0 

Natürlich mit try außerhalb eines do catch oder ohne Operator ? Wenn ein Fehler auftritt, stürzt das Programm ab. Wir haben dem „Kunden“ jedoch die Möglichkeit gegeben, Fehler nach eigenem Ermessen zu behandeln.


Ja, und eine weitere nützliche Funktion der schrittweisen Erstellung mit dieser Vorlage ist die Möglichkeit, Schritte in verschiedenen Teilen des Codes zu platzieren. Nicht der häufigste Fall, aber dennoch. Danke Akryukov für die Erinnerung!


Fazit


Die Vorlage ist extrem einfach zu verstehen und alles Einfache ist, wie Sie wissen, genial. Oder umgekehrt? Nun, egal. Die Hauptsache ist, dass ich ohne ein Zucken meiner Seele sagen kann, dass es (die Vorlage) mir bereits geholfen hat, Probleme bei der Erstellung großer und komplexer Initialisierungsprozesse zu lösen. Zum Beispiel der Prozess der Vorbereitung einer Kommunikationssitzung mit dem Server in der Bibliothek, den ich vor fast zwei Jahren für einen Dienst geschrieben habe. Der Code ist übrigens "Open Source" und auf Wunsch ist es durchaus möglich, sich damit vertraut zu machen . (Obwohl seitdem natürlich viel Wasser geflossen ist und andere Programmierer diesen Code angewendet haben.)


Meine anderen Beiträge zu Designmustern:



Und dies ist mein „Twitter“ , um ein hypothetisches Interesse an meiner öffentlich-beruflichen Tätigkeit zu befriedigen.

Source: https://habr.com/ru/post/de457086/


All Articles