Das Interpreter-Muster bezieht sich auf Verhaltensentwurfsmuster. Mit diesem Muster können Sie Ihre eigene Programmiersprache implementieren, indem Sie mit einem AST-Baum arbeiten, dessen Eckpunkte terminale und nicht-terminale Ausdrücke sind, die die Interpret-Methode implementieren, die die Funktionalität der Sprache bereitstellt.
Terminalausdruck – zum Beispiel die Zeichenfolgenkonstante – “Hallo Welt”
Nicht-terminaler Ausdruck – zum Beispiel Print(“Hello World”), enthält Print und ein Argument aus dem Terminalausdruck “Hello World”
Was ist der Unterschied? Der Unterschied besteht darin, dass die Interpretation bei terminalen Ausdrücken endet, bei nicht-terminalen Ausdrücken jedoch ausführlich über alle eingehenden Eckpunkte/Argumente hinweg fortgesetzt wird. Wenn der AST-Baum nur aus nicht-terminalen Ausdrücken bestünde, würde die Anwendung niemals abgeschlossen werden, weil Für jeden Prozess ist eine gewisse Endlichkeit erforderlich. Diese Endlichkeit ist das, was terminale Ausdrücke ausmachen. Sie enthalten normalerweise Daten, zum Beispiel Zeichenfolgen.
Ein Beispiel für einen AST-Baum finden Sie unten:
Dcoetzee, CC0, über Wikimedia Commons
Wie Sie sehen können, sind Terminalausdrücke konstant und variabel, nichtterminale Ausdrücke sind der Rest.
Was nicht enthalten ist
Die Interpreter-Implementierung umfasst nicht das Parsen der in den AST-Baum eingegebenen Sprachzeichenfolge. Es reicht aus, Klassen von Terminal- und Nicht-Terminal-Ausdrücken zu implementieren, Methoden mit dem Kontextargument am Eingang zu interpretieren, einen AST-Ausdrucksbaum zu erstellen und die Interpret-Methode am Stammausdruck auszuführen. Ein Kontext kann verwendet werden, um den Anwendungsstatus zur Laufzeit zu speichern.
Implementierung
Das Muster beinhaltet:
Client – gibt den AST-Baum zurück und führt Interpret(context) für den Wurzelknoten (Client) aus
Kontext – enthält den Status der Anwendung, der bei der Interpretation an Ausdrücke übergeben wird (Kontext)
Abstrakter Ausdruck – eine abstrakte Klasse, die die Methode Interpret(context) (Expression) enthält
Terminalausdruck ist ein endgültiger Ausdruck, ein Nachkomme eines abstrakten Ausdrucks (TerminalExpression)
Ein nicht-terminaler Ausdruck ist kein endlicher Ausdruck; er enthält Zeiger auf Scheitelpunkte tief im AST-Baum. Untergeordnete Scheitelpunkte wirken sich normalerweise auf das Ergebnis der Interpretation des nicht-terminalen Ausdrucks aus (NonTerminalExpression).
Client-Beispiel in C#
static void Main(string[] args)
{
var context = new Context();
var initialProgram = new PerformExpression(
new IExpression[] {
new SetExpression("alpha", "1"),
new GetExpression("alpha"),
new PrintExpression(
new IExpression[] {
new ConstantExpression("Hello Interpreter Pattern")
}
)
}
);
System.Console.WriteLine(initialProgram.interpret(context));
}
}
Beispiel für einen abstrakten Ausdruck in C#
{
String interpret(Context context);
}
Beispiel für einen Terminalausdruck in C# (String-Konstante)
Beispiel eines nichtterminalen Ausdrucks in C# (Starten und Verketten der Ergebnisse untergeordneter Scheitelpunkte unter Verwendung des Trennzeichens „;“
{
public PerformExpression(IExpression[] leafs) : base(leafs) {
this.leafs = leafs;
}
override public String interpret(Context context) {
var output = "";
foreach (var leaf in leafs) {
output += leaf.interpret(context) + ";";
}
return output;
}
}
Können Sie es funktionell umsetzen?
Bekanntlich sind alle Turing-vollständigen Sprachen gleichwertig. Ist es möglich, das objektorientierte Muster auf die funktionale Programmiersprache zu übertragen?
Für ein Experiment nehmen wir eine FP-Sprache für das Web namens Elm. In Elm gibt es keine Klassen, aber Datensätze und Typen. Daher sind die folgenden Datensätze und Typen an der Implementierung beteiligt:
Ausdruck – Auflistung aller möglichen Sprachausdrücke (Ausdruck)
Untergeordneter Ausdruck – ein Ausdruck, der dem nichtterminalen Ausdruck (ExpressionLeaf) untergeordnet ist
Kontext – ein Datensatz, der den Status der Anwendung speichert (Kontext)
Funktionen, die Interpret(Kontext)-Methoden implementieren – alle notwendigen Funktionen, die die Funktionalität von Terminal- und Nicht-Terminal-Ausdrücken implementieren
Hilfsdatensätze des Interpreter-Status – notwendig für den korrekten Betrieb des Interpreters, sie speichern den Interpreter-Status und den Kontext
Ein Beispiel für eine Funktion, die die Interpretation für den gesamten Satz möglicher Ausdrücke in Elm implementiert:
case input.expression of
Constant text ->
{
output = text,
context = input.context
}
Perform leafs ->
let inputs = List.map (\leaf -> { expressionLeaf = leaf, context = input.context } ) leafs in
let startLeaf = { expressionLeaf = (Node (Constant "")), context = { variables = Dict.empty } } in
let outputExpressionInput = List.foldl mergeContextsAndRunLeafs startLeaf inputs in
{
output = (runExpressionLeaf outputExpressionInput).output,
context = input.context
}
Print printExpression ->
run
{
expression = printExpression,
context = input.context
}
Set key value ->
let variables = Dict.insert key value input.context.variables in
{
output = "OK",
context = { variables = variables }
}
Get key ->
{
output = Maybe.withDefault ("No value for key: " ++ key) (Dict.get key input.context.variables),
context = input.context
}
Was ist mit dem Parsen?
Das Parsen von Quellcode in einen AST-Baum ist nicht im Interpreter-Muster enthalten; es gibt mehrere Ansätze zum Parsen von Quellcode, aber dazu ein anderes Mal mehr. Bei der Implementierung des Interpreters für Elm habe ich einen einfachen Parser im AST-Baum geschrieben, der aus zwei Funktionen besteht: Parsen eines Scheitelpunkts und Parsen untergeordneter Scheitelpunkte.
parseLeafs state =
let tokensQueue = state.tokensQueue in
let popped = pop state.tokensQueue in
let tokensQueueTail = tail state.tokensQueue in
if popped == "Nothing" then
state
else if popped == "Perform(" then
{
tokensQueue = tokensQueue,
result = (state.result ++ [Node (parse tokensQueue)])
}
else if popped == ")" then
parseLeafs {
tokensQueue = tokensQueueTail,
result = state.result
}
else if popped == "Set" then
let key = pop tokensQueueTail in
let value = pop (tail tokensQueueTail) in
parseLeafs {
tokensQueue = tail (tail tokensQueueTail),
result = (state.result ++ [Node (Set key value)])
}
else if popped == "Get" then
let key = pop tokensQueueTail in
parseLeafs {
tokensQueue = tail tokensQueueTail,
result = (state.result ++ [Node (Get key)])
}
else
parseLeafs {
tokensQueue = tokensQueueTail,
result = (state.result ++ [Node (Constant popped)])
}
parse tokensQueue =
let popped = pop tokensQueue in
let tokensQueueTail = tail tokensQueue in
if popped == "Perform(" then
Perform (
parseLeafs {
tokensQueue = tokensQueueTail,
result = []
}
).result
else if popped == "Set" then
let key = pop tokensQueueTail in
let value = pop (tail tokensQueueTail) in
Set key value
else if popped == "Print" then
Print (parse tokensQueueTail)
else
Constant popped
In dieser Notiz werde ich über die Bedeutung von Architekturentscheidungen bei der Entwicklung, der Unterstützung einer Anwendung und in einer Teamentwicklungsumgebung schreiben.
In meiner Jugend habe ich an einer Taxi-Bestellanwendung gearbeitet. Im Programm können Sie einen Abholpunkt und einen Abgabepunkt auswählen, die Fahrtkosten und die Tarifart berechnen und tatsächlich ein Taxi bestellen. Ich habe die Anwendung in der letzten Phase des Vorabstarts erhalten; nach dem Hinzufügen mehrerer Korrekturen wurde die Anwendung im AppStore veröffentlicht. Bereits zu diesem Zeitpunkt war dem gesamten Team klar, dass die Implementierung sehr schlecht war, keine Entwurfsmuster verwendet wurden, alle Komponenten des Systems eng miteinander verbunden waren und es im Allgemeinen möglich war, es in eine große kontinuierliche Klasse (Gottobjekt) zu schreiben. Es hätte sich nichts geändert, so wie die Klassen ihre Verantwortungsgrenzen durcheinander brachten und sich in ihrer Gesamtmasse in einer toten Kopplung überlappten. Später beschloss das Management, die Anwendung unter Verwendung der richtigen Architektur von Grund auf neu zu schreiben, was auch geschah und das Endprodukt für mehrere Dutzend B2B-Kunden implementiert wurde.
Ich werde jedoch einen merkwürdigen Vorfall aus der Architektur der Vergangenheit beschreiben, von dem ich manchmal mitten in der Nacht schweißgebadet aufwache oder mich mitten am Tag plötzlich daran erinnere und hysterisch zu lachen beginne. Die Sache ist die, dass ich den Kerl an der Stange beim ersten Mal nicht treffen konnte, was den Großteil der Bewerbung zum Scheitern brachte, aber das Wichtigste zuerst.
Es war ein gewöhnlicher Arbeitstag, einer der Kunden erhielt die Aufgabe, das Anwendungsdesign leicht zu verfeinern – Es ist einfach, das Symbol in der Mitte des Auswahlbildschirms für die Abholadresse um ein paar Pixel nach oben zu verschieben. Nun, nachdem ich die Aufgabe professionell auf 10 Minuten geschätzt hatte, hob ich das Symbol um 20 Pixel an, völlig ahnungslos, und beschloss, den Taxiauftrag zu überprüfen.
Was? Die App zeigt den Bestellbutton nicht mehr an? Wie ist das passiert?
Ich traute meinen Augen nicht; nachdem ich das Symbol um 20 Pixel erhöht hatte, zeigte die Anwendung die Schaltfläche „Bestellung fortsetzen“ nicht mehr an. Nachdem ich die Änderung rückgängig gemacht hatte, sah ich die Schaltfläche wieder. Hier stimmte etwas nicht. Nachdem ich 20 Minuten im Debugger verbracht hatte, hatte ich es ein wenig satt, die vielen Aufrufe überlappender Klassen abzuwickeln, aber ich entdeckte, dass *das Verschieben des Bildes die Logik der Anwendung wirklich verändert*
Es drehte sich alles um das Symbol in der Mitte – Ein Mann auf einer Stange, der beim Bewegen der Karte nach oben sprang, um die Bewegung der Kamera zu animieren. Auf diese Animation folgte das Verschwinden des Knopfes unten. Anscheinend ging das Programm davon aus, dass der um 20 Pixel verschobene Mann einen Sprung machte, und versteckte daher gemäß seiner internen Logik die Bestätigungsschaltfläche.
Wie kann das passieren? Hängt der *Zustand* des Bildschirms wirklich nicht vom Muster der Zustandsmaschine ab, sondern von der *Darstellung* der Position des Mannes auf der Stange?
Es stellte sich heraus, dass jedes Mal, wenn die Karte gezeichnet wurde, die Anwendung *visuell* in die Mitte des Bildschirms gestochen und überprüft, was dort war. Wenn sich ein Mann auf einer Stange befindet, bedeutet dies, dass die Kartenverschiebungsanimation beendet ist und angezeigt werden muss Taste. Wenn der Mann nicht da ist, wird die Karte verschoben und die Schaltfläche muss ausgeblendet werden.
Im obigen Beispiel ist alles in Ordnung, erstens ist es ein Beispiel für Goldberg-Maschinen (abstruse Maschinen), zweitens ein Beispiel für die Zurückhaltung des Entwicklers, irgendwie mit anderen Entwicklern im Team zu interagieren (versuchen Sie, es ohne herauszufinden). Drittens können Sie alle Probleme nach SOLID, Mustern (Code-Smells), MVC-Verletzungen und vielem mehr auflisten.
Versuchen Sie, dies nicht zu tun, entwickeln Sie sich in alle möglichen Richtungen und helfen Sie Ihren Kollegen bei ihrer Arbeit. Frohes neues Jahr euch allen.
Fassade bezieht sich auf strukturelle Gestaltungsmuster. Es bietet eine einzige Schnittstelle, die die Arbeit mit komplexen Systemen ermöglicht, sodass Clients keine Implementierungsdetails zu diesen Systemen haben müssen, wodurch ihr Code vereinfacht wird und eine lose Kopplung zwischen Clients und Systemen auf niedrigerer Ebene implementiert wird. GoF hat ein gutes Beispiel für eine Fassade – Ein Programmiersprachen-Compiler, der verschiedenen Clients, die unterschiedliche Ziele verfolgen, die Möglichkeit bietet, Code über eine einzige Compiler-Fassadenschnittstelle zusammenzustellen.
Abstrakte Fabrik– Bietet eine Schnittstelle zum Erstellen verwandter Objekte, ohne bestimmte Klassen anzugeben.
Mir gefällt der alternative Name für dieses Muster wirklich – Kit (Kit)
Es ist der Factory-Methode sehr ähnlich, allerdings müssen Abstrakte Fabrikendie Beziehung zwischen den erstellten Objekten beschreiben, andernfalls handelt es sich einfach um ein Gottobjekt Antimuster, das alles erschafft, ist willkürlich.
Stellen Sie sich die Entwicklung eines AR-Frameworks für Brillen vor; wir zeigen auf dem Bildschirm Indoor-Navigationspfeile, Symbole von Geschäften, interessante Orte, Fenster und Schaltflächen mit Informationen über jeden Ort an, an dem sich der Benutzer gerade befindet.
Gleichzeitig benötigen wir die Möglichkeit, das Erscheinungsbild und Verhalten von AR-Umgebungssteuerungen anzupassen. Genau für diesen Fall müssen Sie das Muster Set verwenden.
Lassen Sie uns die Schnittstelle von Abstract Factory und Abstract Products schreiben – übergeordnete Protokolle, AR-Umgebungselemente:
protocol ARFactory {
func arrow() -> ARArrow
func icon() -> ARIcon
func button() -> ARButton
func window() -> ARWindow
}
protocol ARArrow {
var image: { get }
func handleSelection()
}
protocol ARIcon {
var image: { get }
var title: String
}
protocol ARButton {
var title: String
func handleSelection()
}
protocol ARWindow {
var title: String
var draw(canvas: Canvas)
}
Jetzt müssen Kit-Entwickler eine Concrete Factory basierend auf der Abstract Factory-Schnittstelle implementieren, und sie müssen alle Elemente zusammen implementieren; der Rest der Anwendung wird in der Lage sein, mit der Factory zu arbeiten, ohne ihren Code zu ändern.< /p>
Das Factory-Methode-Muster bezieht sich auf generative Designmuster. Dieses Muster beschreibt die Erstellung einer Schnittstelle zum Erstellen eines Objekts einer bestimmten Klasse. Es scheint einfach, oder?
Theoretisch
Angenommen, wir entwickeln ein Framework für die Arbeit mit AR-Brillen. Wenn Sie den Kopf zur Seite neigen, sollte vor den Augen des Benutzers ein Menü mit verfügbaren Anwendungen erscheinen. Anwendungen werden von Drittunternehmen entwickelt, die Kunden unseres Frameworks sind. Natürlich wissen wir nicht, welche Anwendungen, Symbole und Namen angezeigt werden sollen. Daher müssen wir eine Schnittstelle bereitstellen, um das Symbol und die zugehörigen Informationen zur Anwendung zu implementieren. Nennen wir es Produkt:
protocol Product {
var name: String { get }
var image: Image { get }
var executablePath: String { get }
}
Als nächstes müssen wir eine Schnittstelle bereitstellen, damit unsere Kunden die Ausstellung einer Reihe von Anwendungen für ihr spezifisches Produkt implementieren können – eine Reihe von Anwendungssymbolen mit Namen, die wir bereits im Framework zeichnen werden.
Lassen Sie uns diese Schnittstelle schreiben – Creator-Schnittstelle, die eine Factory-Methode enthält, die ein Array von Produkten zurückgibt.
Der erste Kunde unseres AR-Frameworks war die Firma 7B – führender Anbieter von Software für Kaffeemaschinen in Honduras. Sie möchten Augmented-Reality-Brillen verkaufen, mit denen man Kaffee zubereiten, prüfen kann, ob Wasser/Bohnen voll sind, und mithilfe des Indoor-Kartenmodus den Weg zur nächsten Kaffeemaschine zeigen kann.
Sie übernehmen die Entwicklung der Software; wir sind lediglich verpflichtet, eine Dokumentation zu den Schnittstellen Creator und Product für die korrekte Anzeige der Liste der Anwendungen und deren Weitergabe bereitzustellen starten.
Nach der Übertragung der Dokumentation implementiert Unternehmen 7B mithilfe der Creator -Schnittstelle den Specific Creator – Klasse, die ein Array von Anwendungssymbolen zurückgibt. Die Symbolanwendungen selbst sind spezifische Produktklassen, die die Produkt-Schnittstelle implementieren.
Beispielcode für Spezifische Produkte:
class CoffeeMachineLocator: implements Product {
let name = “7B Coffee Machine Locator v.3000”
let image = Image.atPath(“images/locator.tga”)
let executablePath = “CoffeeMachineLocator.wasm”
}
class iPuchinno: implements Product {
let name = “iPuchinno 1.0.3”
let image = Image.atPath(“images/puchino.pvrtc”)
let executablePath = “neutron/ipuchBugFixFinalNoFreezeFixAlpha4.js”
}
Klasse Concrete Creator, die ein Array von zwei Anwendungen ergibt:
Danach stellt das Unternehmen 7B die Bibliothek von Concrete Products, Concrete Creator, zusammen, kombiniert sie mit unserem Framework und beginnt mit dem Verkauf von AR-Brillen für seine Kaffeemaschinen, Ergänzungen unsererseits nicht erforderlich.
Befehlsmuster bezieht sich auf Verhaltensentwurfsmuster.
Das ist das Muster, an dem ich am längsten festgehalten habe. Es ist so einfach, dass es sehr komplex ist. Aber ich persönlich finde, dass das Schöne am Selbststudium darin besteht, dass man alle Zeit der Welt hat, ein bestimmtes Thema aus allen Blickwinkeln zu recherchieren.
In GoF wird die Anwendbarkeit also recht prägnant und klar beschrieben: Verkapselt eine Anfrage als Objekt und ermöglicht es Ihnen, Clients mit unterschiedlichen Anfragen zu parametrisieren, Warteschlangen zu verwenden, Anfragen zu protokollieren und Abbruchvorgänge durchzuführen.
Jetzt implementieren wir eine einfache Version des Befehls aus der Beschreibung:
string fakeTrumpsRequest = “SELECT * from Users where name beginsWith DonaldTrump”
Wir haben die Anfrage in ein String-Klassenobjekt gekapselt. Es kann zum Konfigurieren von Clients, zum Hinzufügen von Befehlen zur Warteschlange, zum Protokollieren und zum Abbrechen (unter Verwendung des „Snapshot“-Musters) verwendet werden.
Mir scheint, dass dies völlig ausreicht, um SQL-Abfragen und dergleichen durchzuführen, aber dann gibt es noch Implementierungsdetails, unterschiedliche Anwendungsmöglichkeiten, die Codebasis des Musters, Client-Rollen und Hilfsklassen sind ebenfalls sehr unterschiedlich.
Materialteile
Das
Befehlsmuster beginnt mit einem Befehlsprotokoll, das eine einzelne execute()-Methode enthält. Als nächstes kommt der Spezifische Befehl und Empfänger. Der CC implementiert die Operation auf dem Empfänger und beschreibt die Verbindung zwischen dem Empfänger und der Aktion. Ist etwas unklar? Ich auch, aber lasst uns weitermachen. Der Clienterstellt eine Instanz eines Spezifischen Befehls und verknüpft ihn mit dem Empfänger. Aufrufer – Objekt, das den Prozess des Startens von Befehlen ausführt.
Jetzt versuchen wir es anhand eines Beispiels herauszufinden. Nehmen wir an, wir möchten myOS auf myPhone aktualisieren. Dazu starten wir die Anwendung myOS_Update! und drücken darin die Schaltfläche „Jetzt aktualisieren“. Nach 10 Sekunden wird das System dies tun Melden Sie ein erfolgreiches Update.
Der Client im obigen Beispiel ist die myOS_Update!-Anwendung, der Invoker ist die Schaltfläche „Jetzt aktualisieren!“ und startet den Spezifischen Befehl b>Aktualisierung des Systems mithilfe der Methodeexecute(), die auf den Receiver zugreift. Betriebssystem-Update-Daemon.
Beispiel verwenden
Akzeptieren wir die Benutzeroberfläche der myOS_Update-Anwendung! so gut, dass sie beschlossen, es als separates Produkt zu verkaufen, um eine Schnittstelle für die Aktualisierung anderer Betriebssysteme bereitzustellen. In diesem Fall implementieren wir eine Anwendung mit Unterstützung für Erweiterungen durch Bibliotheken. In den Bibliotheken wird es Implementierungen von Spezifischen Befehlen und Empfängern geben. Wir belassen den statischen/unveränderlichen Invoker , Client, Protokoll Befehle.
Es besteht also keine Notwendigkeit, veränderlichen Code zu unterstützen, da unser Code unverändert bleibt und Probleme aufgrund von Fehlern im Code ihrer Spezifischen Befehle nur bei der Implementierung auf der Clientseite auftreten können Empfänger. Außerdem besteht in dieser Implementierung keine Notwendigkeit, den Quellcode der Hauptanwendung zu übertragen, d. h. wir haben Befehle und UI-Interaktionen mithilfe des Musters Befehl gekapselt.
Das Builder-Muster gehört zu einer Gruppe von Mustern, deren Existenz mir nicht besonders klar ist, ich stelle die offensichtliche Redundanz fest. Gehört zur Gruppe der generativen Designmuster. Wird verwendet, um eine einfache Schnittstelle zum Erstellen komplexer Objekte zu implementieren.
Anwendbarkeit
Vereinfachung der Schnittstelle. Dies kann die Erstellung eines Objekts in Konstruktoren mit einer großen Anzahl von Argumenten erleichtern und die Lesbarkeit des Codes objektiv verbessern.
Beispiel in C++ ohne Builder:
auto weapon = new Weapon(“Claws”);
monster->weapon = weapon;
auto health = new MonsterHealth(100);
monster->health = health;
Пример со строителем на C++:
.addWeapon(“Claws”)
.addHealth(100)
.build();
Однако в языках поддерживающих именованные аргументы (named arguments), необходимость использовать именно для этого случая отпадает.
Пример на Swift с использованием named arguments:
let monster = Monster(weapon: “Claws”, health: 100)
Unveränderlichkeit. Mit dem Builder können Sie die Kapselung des erstellten Objekts bis zur endgültigen Montagephase sicherstellen. Hier müssen Sie sorgfältig darüber nachdenken, ob Sie durch die Verwendung eines Musters vor der hohen Dynamik der Umgebung, in der Sie arbeiten, bewahrt werden. Vielleicht bringt die Verwendung des Musters nichts, weil im Entwicklungsteam einfach keine Kapselungskultur herrscht .
Interaktion mit Komponenten in verschiedenen Phasen der Objekterstellung. Mithilfe des Musters ist es außerdem möglich, die schrittweise Erstellung eines Objekts bei der Interaktion mit anderen Komponenten des Systems sicherzustellen. Höchstwahrscheinlich ist dies sehr nützlich (?)
Kritik
Natürlich müssen Sie *sorgfältig* darüber nachdenken, ob es sich lohnt, das Muster in Ihrem Projekt flächendeckend zu nutzen. Sprachen mit moderner Syntax und einer erweiterten IDE machen die Verwendung des Builders überflüssig, was die Lesbarkeit des Codes verbessert (siehe den Punkt zu benannten Argumenten) Hätte dieses Muster 1994 verwendet werden sollen, als das GoF-Buch veröffentlicht wurde? Höchstwahrscheinlich ja, aber gemessen an der Open-Source-Codebasis jener Jahre nutzten ihn nur wenige Menschen.
Das Composite-Muster bezieht sich auf strukturelle Designmuster; in inländischen Quellen ist es als „Compositor“ bekannt. Nehmen wir an, wir entwickeln eine Anwendung – Fotoalbum. Der Benutzer kann Ordner erstellen, dort Fotos hinzufügen und andere Manipulationen durchführen. Sie benötigen auf jeden Fall die Möglichkeit, die Anzahl der Dateien in Ordnern und die Gesamtzahl aller Dateien und Ordner anzuzeigen. Es ist offensichtlich, dass Sie einen Baum verwenden müssen, aber wie implementiert man eine Baumarchitektur mit einer einfachen und praktischen Schnittstelle? Das Composite-Muster kommt zur Rettung.
Wir implementieren die Component-Schnittstelle mit der von uns benötigten dataCount()-Methode, über die wir die Anzahl der Dateien/Verzeichnisse zurückgeben. Erstellen wir eine Directory-Klasse mit einer Schnittstelle, die es Ihnen ermöglicht, Instanzen von Klassen hinzuzufügen/zu entfernen, die die Component-Schnittstelle gemäß dem Muster implementieren. Dies ist Composite. Wir erstellen außerdem eine File-Klasse, in der wir Bytes mit einer Fotokarte speichern, von Component erben und über dataCount 1 zurückgeben, was bedeutet, dass es nur ein Foto gibt!
Als nächstes implementieren wir im Verzeichnis die dataCount()-Methode – indem Sie alle im Array von Komponenten liegenden Elemente durchgehen und alle ihre dataCount’s addieren.
Alles ist bereit!
Unten ist ein Beispiel in Go:
package main
import "fmt"
type component interface {
dataCount() int
}
type file struct {
}
type directory struct {
c []component
}
func (f file) dataCount() int {
return 1
}
func (d directory) dataCount() int {
var outputDataCount int = 0
for _, v := range d.c {
outputDataCount += v.dataCount()
}
return outputDataCount
}
func (d *directory) addComponent(c component) {
d.c = append(d.c, c)
}
func main() {
var f file
var rd directory
rd.addComponent(f)
rd.addComponent(f)
rd.addComponent(f)
rd.addComponent(f)
fmt.Println(rd.dataCount())
var sd directory
sd.addComponent(f)
rd.addComponent(sd)
rd.addComponent(sd)
rd.addComponent(sd)
fmt.Println(sd.dataCount())
fmt.Println(rd.dataCount())
}
Das Adaptermuster bezieht sich auf strukturelle Designmuster.
Der Adapter ermöglicht die Daten-/Schnittstellenkonvertierung zwischen zwei Klassen/Schnittstellen.
Angenommen, wir entwickeln ein System zur Bestimmung der Käuferziele in einem Geschäft auf der Grundlage neuronaler Netze. Das System empfängt einen Videostream von einer Filialkamera, identifiziert Kunden anhand ihres Verhaltens und klassifiziert sie in Gruppen. Arten von Gruppen – kam, um zu kaufen (potenzieller Käufer), nur um zuzusehen (Zauberer), kam, um etwas zu stehlen (Dieb), kam, um die Ware zurückzugeben (unzufriedener Käufer), kam betrunken/high (potenzieller Rowdy).
Wie alle erfahrenen Entwickler finden wir ein fertiges neuronales Netzwerk, das Affenarten in einem Käfig anhand eines Videostreams klassifizieren kann, den das Zoologische Institut des Berliner Zoos freundlicherweise frei zur Verfügung gestellt hat, und auf einem Videostream nachzutrainieren aus dem Laden und erhalten Sie ein funktionierendes, hochmodernes System.
Es gibt nur ein kleines Problem – Der Videostream ist im MPEG2-Format kodiert und unser System unterstützt nur OGG Theora. Wir haben nicht den Quellcode des Systems, das Einzige, was wir tun können, ist – Ändern Sie den Datensatz und trainieren Sie das neuronale Netzwerk. Was zu tun? Schreiben Sie eine Adapterklasse, die den Stream von mpeg2 -> OGG Theora überträgt und an das neuronale Netzwerk sendet.
Nach dem klassischen Schema umfasst das Muster Client, Ziel, Adaptee und Adapter. Der Client ist in diesem Fall ein neuronales Netzwerk, das einen Videostream in OGG Theora empfängt, Ziel – die Schnittstelle, mit der es interagiert, adaptee – Schnittstelle zum Senden von Videostreams im MPEG2-Format, Adapter – konvertiert mpeg2 in OGG Theora und sendet es über die Zielschnittstelle.
Das Delegatenmuster ist eines der wichtigsten Entwurfsmuster. Nehmen wir an, wir entwickeln eine Friseuranwendung. Die Anwendung verfügt über einen Kalender zum Auswählen eines Tages für die Aufzeichnung; durch Tippen auf das Datum sollte eine Liste mit Friseuren mit einer Auswahl geöffnet werden. Lassen Sie uns eine naive Verknüpfung von Systemkomponenten implementieren, Kalender und Bildschirm mithilfe von Zeigern aufeinander kombinieren, um eine Listenanzeige zu implementieren:
// псевдокод
class BarbershopScreen {
let calendar: Calendar
func showBarbersList(date: Date) {
showSelectionSheet(barbers(forDate: date))
}
}
class Calendar {
let screen: BarbershopScreen
func handleTap(on date: Date) {
screen.showBarbersList(date: date)
}
}
Nach ein paar Tagen ändern sich die Anforderungen; vor der Anzeige der Liste müssen Sie Angebote mit einer Auswahl an Dienstleistungen (Bartschneiden usw.) anzeigen, jedoch nicht immer, an allen Tagen außer Samstag. Wir fügen dem Kalender eine Prüfung hinzu, ob Samstag ist oder nicht. Abhängig davon nennen wir die Methode der Liste der Friseure oder der Liste der Dienstleistungen. Der Übersichtlichkeit halber werde ich Folgendes demonstrieren:
// псевдокод
class BarbershopScreen {
let calendar: Calendar
func showBarbersList(date: Date) {
showSelectionSheet(barbers(forDate: date))
}
func showOffersList() {
showSelectionSheet(offers)
}
}
class Calendar {
let screen: BarbershopScreen
func handleTap(on date: Date) {
if date.day != .saturday {
screen.showOffersList()
}
else {
screen.showBarbersList(date: date)
}
}
}
Eine Woche später werden wir gebeten, einen Kalender zum Feedback-Bildschirm hinzuzufügen, und in diesem Moment passiert das erste architektonische Ups! Was zu tun? Der Kalender ist eng mit dem Friseurtermin-Bildschirm verknüpft. Wow! Pfui! oh-oh Wenn Sie weiterhin mit dieser verrückten Anwendungsarchitektur arbeiten, sollten Sie eine Kopie der gesamten Kalenderklasse erstellen und diese Kopie mit dem Feedback-Bildschirm verknüpfen. Ok, sieht gut aus, dann haben wir noch ein paar Bildschirme und mehrere Kopien des Kalenders hinzugefügt, und dann war es soweit. Wir wurden gebeten, das Design des Kalenders zu ändern, was bedeutet, dass wir jetzt alle Kopien des Kalenders finden und bei allen die gleichen Änderungen vornehmen müssen. Dieser „Ansatz“ hat großen Einfluss auf die Entwicklungsgeschwindigkeit und erhöht die Wahrscheinlichkeit, einen Fehler zu machen. Dies hat zur Folge, dass solche Projekte im Chaos enden, wenn selbst der Autor der ursprünglichen Architektur nicht mehr versteht, wie Kopien seiner Klassen funktionieren, und andere im Laufe der Zeit hinzugefügte Hacks plötzlich auseinanderfallen. Was musste getan werden, oder noch besser, womit konnte man noch nicht zu spät beginnen? Verwenden Sie das Delegationsmuster! Die Delegation ist eine Möglichkeit, Klassenereignisse über eine gemeinsame Schnittstelle weiterzuleiten. Unten finden Sie ein Beispiel für einen Delegaten für einen Kalender:
Daher haben wir den Kalender vollständig vom Bildschirm entkoppelt. Bei der Auswahl eines Datums aus dem Kalender wird das Datumsauswahlereignis – *delegiert* die Ereignisverarbeitung an den Abonnenten; Der Abonnent ist der Bildschirm. Welche Vorteile ergeben sich für uns aus diesem Ansatz? Jetzt können wir die Kalender- und Bildschirmlogik unabhängig voneinander ändern, ohne Klassen zu duplizieren, was die weitere Unterstützung vereinfacht; Dadurch wird das „Prinzip der Alleinverantwortung“ für die Umsetzung von Systemkomponenten umgesetzt und das DRY-Prinzip eingehalten. Wenn Sie die Delegation verwenden, können Sie die Logik für die Anzeige von Fenstern und die Reihenfolge von allem auf dem Bildschirm hinzufügen und ändern. Dies hat keinerlei Auswirkungen auf den Kalender und andere Klassen, die objektiv nicht an Prozessen teilnehmen sollten, die nicht direkt mit ihnen zusammenhängen.< br />Alternativ können Programmierer, die sich nicht allzu sehr darum kümmern, Nachrichten über einen gemeinsamen Bus senden, ohne eine separate Protokoll-/Delegiertenschnittstelle zu schreiben, wo es besser wäre, die Delegation zu verwenden. Ich habe in einem früheren Beitrag über die Nachteile dieses Ansatzes geschrieben – „Beobachtermuster.“
Das Observer-Muster bezieht sich auf Verhaltensentwurfsmuster. Das Muster ermöglicht es Ihnen, eine Änderung im Zustand eines Objekts über eine gemeinsame Schnittstelle an Abonnenten zu senden. Nehmen wir an, wir entwickeln einen Messenger für Programmierer, wir haben einen Chat-Bildschirm in der Anwendung. Wenn Sie eine Nachricht mit dem Text „Problem“ und „Fehler“ oder „etwas stimmt nicht“ erhalten, müssen Sie den Fehlerlistenbildschirm und den Einstellungsbildschirm rot einfärben. Als Nächstes beschreibe ich zwei Optionen zur Lösung des Problems. Die erste ist einfach, aber äußerst schwierig zu unterstützen, und die zweite ist wesentlich stabiler in der Unterstützung, erfordert aber bei der ersten Implementierung ein Kopfdrehen.
Gemeinsamer Bus
Alle Implementierungen des Musters beinhalten das Senden von Nachrichten bei Datenänderungen, das Abonnieren von Nachrichten und die weitere Verarbeitung in Methoden. Die Shared-Bus-Option enthält ein einzelnes Objekt (normalerweise ein Singleton), das Nachrichten an Empfänger versendet. Die Einfachheit der Implementierung ist wie folgt:
Das Objekt sendet eine abstrakte Nachricht an den gemeinsam genutzten Bus
Ein anderes Objekt, das den gemeinsam genutzten Bus abonniert hat, fängt die Nachricht ab und entscheidet, ob sie verarbeitet wird oder nicht.
Eine der von Apple verfügbaren Implementierungsoptionen (NSNotificationCenter-Subsystem) fügte den Abgleich des Nachrichtenheaders mit dem Namen der Methode hinzu, die vom Empfänger bei der Zustellung aufgerufen wird. Der größte Nachteil dieses Ansatzes – Wenn Sie die Nachricht weiter ändern, müssen Sie sich zunächst alle Orte merken und dann manuell bearbeiten, an denen sie verarbeitet und gesendet wird. Es handelt sich um eine schnelle Erstimplementierung, gefolgt von einem langen, komplexen Support, der eine Wissensbasis für den korrekten Betrieb erfordert.
Multicast-Delegierter
In dieser Implementierung erstellen wir die endgültige Multicast-Delegatenklasse; genau wie im Fall eines gemeinsam genutzten Busses können Objekte diese abonnieren, um „Nachrichten“ oder „Ereignisse“ zu empfangen, die Arbeit des Parsens und Filterns von Nachrichten ist jedoch anders nicht den Objekten zugeordnet. Stattdessen müssen Abonnentenklassen die Multicast-Methoden des Delegaten implementieren, mit denen er sie benachrichtigt. Dies wird durch die Verwendung von Delegate-Schnittstellen/-Protokollen implementiert. Wenn sich die allgemeine Schnittstelle ändert, wird die Anwendung nicht mehr erstellt. Zu diesem Zeitpunkt müssen alle Stellen für die Verarbeitung einer bestimmten Nachricht neu erstellt werden, ohne dass eine separate Wissensdatenbank verwaltet werden muss um mich an diese Orte zu erinnern. Der Compiler ist dein Freund. Dieser Ansatz erhöht die Produktivität des Teams, da keine Dokumentation geschrieben oder gespeichert werden muss und ein neuer Entwickler nicht versuchen muss, zu verstehen, wie eine Nachricht und ihre Argumente verarbeitet werden, sondern sie mit einer praktischen und verständlichen Benutzeroberfläche arbeiten , so wird das Dokumentationsparadigma durch Code umgesetzt. Der Multicast-Delegat selbst basiert auf dem Delegatenmuster, über das ich im nächsten Beitrag schreiben werde.
Das Proxy-Muster bezieht sich auf strukturelle Entwurfsmuster. Das Muster beschreibt die Technik der Arbeit mit einer Klasse über eine Klassenebene – Proxy. Ein Proxy ermöglicht es Ihnen, die Funktionalität der ursprünglichen Klasse zu ändern und dabei das ursprüngliche Verhalten beizubehalten und gleichzeitig die ursprüngliche Klassenschnittstelle beizubehalten. Stellen wir uns die Situation vor – Im Jahr 2015 beschließt eines der Länder Westeuropas, alle Anfragen an die Websites der Benutzer des Landes aufzuzeichnen, um Statistiken und ein tieferes Verständnis der politischen Gefühle der Bürger zu verbessern. Stellen wir uns den Pseudocode einer naiven Implementierung des Gateways vor, über das Bürger auf das Internet zugreifen:
class InternetRouter {
private let internet: Internet
init(internet: Internet) {
self.internet = internet
}
func handle(request: Request, from client: Client) -> Data {
return self.internet.handle(request)
}
}
Im obigen Code erstellen wir eine Internet-Router-Klasse mit einem Zeiger auf ein Objekt, das den Internetzugang bereitstellt. Wenn ein Kunde eine Website-Anfrage stellt, geben wir eine Antwort aus dem Internet zurück.
Mithilfe des Proxy-Musters und des Singleton-Antimusters fügen wir Funktionen zum Protokollieren des Clientnamens und der URL hinzu:
class InternetRouterProxy {
private let internetRouter: InternetRouter
init(internet: Internet) {
self.internetRouter = InternetRouter(internet: internet)
}
func handle(request: Request, from client: Client) -> Data {
Logger.shared.log(“Client name: \(client.name), requested URL: \(request.URL)”)
return self.internetRouter.handle(request: request, from: client)
}
}
Aufgrund der Beibehaltung der ursprünglichen InternetRouter-Schnittstelle in der Proxy-Klasse InternetRouterProxy reicht es aus, die Initialisierungsklasse von InternerRouter durch ihren Proxy zu ersetzen, es sind keine weiteren Änderungen an der Codebasis erforderlich.
Das Prototypmuster gehört zur Gruppe der generativen Designmuster. Nehmen wir an, wir entwickeln Dating-Apps Tender. Gemäß unserem Geschäftsmodell haben wir die Möglichkeit, Kopien Ihres eigenen Profils zu erstellen und dabei den Namen und die Reihenfolge der Fotos an einigen Stellen automatisch zu ändern. Dies wurde getan, damit der Benutzer die Möglichkeit hat, in der Anwendung mehrere Profile gleichzeitig mit unterschiedlichen Freunden zu verwalten. Durch Klicken auf die Schaltfläche zum Erstellen einer Kopie des Profils müssen wir das Kopieren des Profils, die automatische Generierung eines Namens und die Neusortierung der Fotos implementieren. Naive Pseudocode-Implementierung:
fun didPressOnCopyProfileButton() {
let profileCopy = new Profile()
profileCopy.name = generateRandomName()
profileCopy.age = profile.age
profileCopy.photos = profile.photos.randomize()
storage.save(profileCopy)
}
Stellen wir uns nun vor, dass andere Teammitglieder den Kopiercode kopiert oder von Grund auf neu erfunden haben und danach ein neues Feld hinzugefügt wurde – mag. In diesem Feld wird die Anzahl der Profil-Likes gespeichert. Jetzt müssen Sie *alle* Stellen, an denen das Kopieren erfolgt, manuell aktualisieren, indem Sie ein neues Feld hinzufügen. Es ist sehr zeitaufwändig und schwierig, den Code zu warten und zu testen. Um dieses Problem zu lösen, wurde das Prototype-Entwurfsmuster erfunden. Erstellen wir ein allgemeines Kopierprotokoll mit einer copy()-Methode, die eine Kopie eines Objekts mit den erforderlichen Feldern zurückgibt. Nach dem Ändern von Entitätsfeldern müssen Sie nur eine copy()-Methode aktualisieren, anstatt alle Stellen, die Kopiercode enthalten, manuell zu suchen und zu aktualisieren.
In diesem Artikel werde ich die Verwendung der Zustandsmaschine (State Machine) beschreiben und eine einfache Implementierung zeigen, eine Implementierung unter Verwendung des Zustandsmusters. Es ist erwähnenswert, dass es unerwünscht ist, das Statusmuster zu verwenden, wenn es weniger als drei Zustände gibt, weil Dies führt normalerweise zu einer unnötigen Komplexität der Codelesbarkeit und damit verbundenen Supportproblemen – Alles sollte in Maßen erfolgen.
MEAACT PHOTO / STUART PRICE.
Herr der Flaggen
Angenommen, wir entwickeln einen Videoplayer-Bildschirm für das Mediensystem eines Zivilflugzeugs. Der Player muss in der Lage sein, einen Videostream zu laden und abzuspielen, dem Benutzer das Stoppen des Downloadvorgangs, das Zurückspulen und die Ausführung anderer üblicher Vorgänge zu ermöglichen ein Spieler. Nehmen wir an, der Player hat den nächsten Teil des Videostreams zwischengespeichert, überprüft, ob genügend Teile für die Wiedergabe vorhanden sind, hat mit der Wiedergabe des Fragments für den Benutzer begonnen und fährt gleichzeitig mit dem Herunterladen des nächsten Teils fort. In diesem Moment spult der Benutzer zur Mitte des Videos zurück, d. h. Sie müssen jetzt die Wiedergabe des aktuellen Fragments stoppen und an einer neuen Position mit dem Laden beginnen. Es gibt jedoch Situationen, in denen dies nicht möglich ist – Der Benutzer kann die Wiedergabe des Videostreams nicht steuern, während ihm ein Video über Flugsicherheit gezeigt wird. Lassen Sie uns das isSafetyVideoPlaying-Flag überprüfen, um diese Situation zu überprüfen. Das System muss außerdem in der Lage sein, das aktuelle Video anzuhalten und über den Player eine Warnung des Schiffskapitäns und der Schiffsbesatzung zu senden. Fügen wir ein weiteres isAnnouncementPlaying-Flag hinzu. Außerdem besteht die Anforderung, die Wiedergabe nicht anzuhalten, während Hilfe zur Arbeit mit dem Player angezeigt wird. Ein weiteres Flag ist HelpPresenting.
Beispiel-Pseudocode für einen Mediaplayer:
class MediaPlayer {
public var isHelpPresenting = false
public var isCaching = false
public var isMediaPlaying: Bool = false
public var isAnnouncementPlaying = false
public var isSafetyVideoPlaying = false
public var currentMedia: Media = null
fun play(media: Media) {
if isMediaPlaying == false, isAnnouncementPlaying == false, isSafetyVideoPlaying == false {
if isCaching == false {
if isHelpPresenting == false {
media.playAfterHelpClosed()
}
else {
media.playAfterCaching()
}
}
}
fun pause() {
if isAnnouncementPlaying == false, isSafetyVideoPlaying == false {
currentMedia.pause()
}
}
}
Das obige Beispiel ist aufgrund der hohen Variabilität (Entropie) schwer zu lesen und schwer zu warten. Dieses Beispiel basiert auf meiner Erfahrung mit der Codebasis *vieler* Projekte, die keine Zustandsmaschine verwendeten. Jedes Kontrollkästchen muss speziell die Elemente der Schnittstelle und der Geschäftslogik der Anwendung „steuern“. Durch das Hinzufügen eines weiteren Kontrollkästchens muss der Entwickler in der Lage sein, sie zu jonglieren und alles mehrmals mit allen möglichen Optionen zu überprüfen. Durch Einsetzen in die Formel „2 ^ Anzahl der Kontrollkästchen“ erhalten Sie 2 ^ 6 = 64 Optionen für das Anwendungsverhalten für nur 6 Kontrollkästchen. Alle diese Kombinationen von Kontrollkästchen müssen manuell überprüft und verwaltet werden. Aus Sicht des Entwicklers sieht das Hinzufügen neuer Funktionen mit einem solchen System folgendermaßen aus: – Wir müssen die Möglichkeit hinzufügen, die Browserseite der Fluggesellschaft anzuzeigen, und sie sollte wie bei Filmen minimiert werden, wenn Besatzungsmitglieder etwas ankündigen. – Ok, ich werde es tun. (Oh verdammt, ich muss eine weitere Flagge hinzufügen und alle Stellen, an denen sich die Flaggen kreuzen, noch einmal überprüfen, das sind viele Dinge, die geändert werden müssen!)
Auch ein Schwachpunkt des Flaggensystems – Änderungen am Verhalten der Anwendung vornehmen. Es ist sehr schwer vorstellbar, wie man das Verhalten anhand von Flags schnell/flexibel ändern kann, wenn man nach der Änderung nur eines Flags alles noch einmal überprüfen muss. Dieser Entwicklungsansatz führt zu vielen Problemen, Zeit- und Geldverlusten.
Betreten Sie die Maschine
Wenn Sie sich die Flags genau ansehen, können Sie verstehen, dass wir tatsächlich versuchen, bestimmte Prozesse zu verarbeiten, die in der realen Welt auftreten. Wir listen sie auf: Normalmodus, Anzeige eines Sicherheitsvideos, Übertragung einer Nachricht des Kapitäns oder der Besatzungsmitglieder. Für jeden Prozess ist ein Regelwerk bekannt, das das Verhalten der Anwendung verändert. Gemäß den Regeln des State-Machine-Musters (State-Machine) werden wir alle Prozesse als Zustände in der Aufzählung auflisten, ein solches Konzept als Zustand zum Player-Code hinzufügen und zustandsbasiertes Verhalten implementieren, indem wir Kombinationen auf den Flags entfernen. Dadurch reduzieren wir die Testmöglichkeiten auf genau die Anzahl der Zustände.
Pseudocode:
enum MediaPlayerState {
mediaPlaying,
mediaCaching,
crewSpeaking,
safetyVideoPlaying,
presentingHelp
}
class MediaPlayer {
fun play(media: Media) {
media.play()
}
func pause() {
media.pause()
}
}
class MediaPlayerStateMachine {
public state: MediaPlayerState
public mediaPlayer: MediaPlayer
public currentMedia: Media
//.. init (mediaPlayer) etc
public fun set(state: MediaPlayerState) {
switch state {
case mediaPlaying:
mediaPlayer.play(currentMedia)
case mediaCaching, crewSpeaking,
safetyVideoPlaying, presentingHelp:
mediaPlayer.pause()
}
}
}
Der große Unterschied zwischen einem Flag-System und einer Zustandsmaschine ist der logische Zustandsschalttrichter in der set(state: ..)-Methode, der es Ihnen ermöglicht, das menschliche Verständnis des Zustands in Programmcode zu übersetzen, ohne sich mit Logik auseinandersetzen zu müssen Spiele zum Konvertieren von Flags in Zustände bei weiterer Codeunterstützung.
Musterstatus
Als nächstes werde ich den Unterschied zwischen der naiven Implementierung der Zustandsmaschine und dem Zustandsmuster zeigen. Stellen wir uns vor, wir müssten 10 Zustände hinzufügen. Dadurch wächst die Zustandsmaschinenklasse auf die Größe eines Gottobjekts, dessen Wartung schwierig und kostspielig sein wird. Natürlich ist diese Implementierung besser als die Flag-Implementierung (mit dem Flag-System erschießt sich der Entwickler zuerst selbst, und wenn nicht, dann hängt sich die Qualitätssicherung bei 2 ^ 10 = 1024 Variationen auf, aber wenn beide *nicht tun Beachten* Sie die Komplexität der Aufgabe, dann wird der Benutzer, dessen Anwendung einfach ist, bemerken, dass sie sich weigert, mit einer bestimmten Kombination von Flags zu arbeiten) Bei einer großen Anzahl von Zuständen ist die Verwendung des Zustandsmusters erforderlich. Fügen wir dem State-Protokoll eine Reihe von Regeln hinzu:
Lassen Sie uns die Implementierung des Regelsatzes in separate Zustände verschieben, zum Beispiel den Code für einen Zustand:
class CrewSpeakingState: State {
func playMedia(context: MediaPlayerContext) {
showWarning(“Can’ t play media - listen to announce!”)
}
func mediaCaching(context: MediaPlayerContext) {
showActivityIndicator()
}
func crewSpeaking(context: MediaPlayerContext) {
set(volume: 100)
}
func safetyVideoPlaying(context: MediaPlayerContext) {
set(volume: 100)
}
func presentHelp(context: MediaPlayerContext) {
showWarning(“Can’ t present help - listen to announce!”)
}
}
Als nächstes erstellen wir einen Kontext, mit dem jeder Zustand arbeiten wird, und integrieren die Zustandsmaschine:
final class MediaPlayerContext {
private
var state: State
public fun set(state: State) {
self.state = state
}
public fun play(media: Media) {
state.play(media: media, context: this)
}
…
остальные возможные события
}
Anwendungskomponenten arbeiten mit dem Kontext über öffentliche Methoden; Zustandsobjekte entscheiden selbst, von welchem Zustand aus sie über die Zustandsmaschine innerhalb des Kontexts wechseln. Daher haben wir die God-Object-Zerlegung implementiert. Die Aufrechterhaltung eines sich ändernden Zustands wird viel einfacher, da der Compiler Änderungen im Protokoll verfolgt und die Komplexität des Verständnisses von Zuständen aufgrund der Reduzierung der Anzahl der Codezeilen verringert und sich darauf konzentriert Lösung eines bestimmten Staatsproblems. Sie können jetzt auch die Arbeit in einem Team teilen und Teammitgliedern die Implementierung eines bestimmten Zustands übertragen, ohne sich Gedanken über die Notwendigkeit machen zu müssen, Konflikte zu „lösen“, was bei der Arbeit mit einer großen Zustandsmaschinenklasse der Fall ist.
Die Mustermethode bezieht sich auf Verhaltensentwurfsmuster. Das Muster beschreibt eine Möglichkeit, einen Teil der Logik einer Klasse bei Bedarf zu ersetzen, wobei der Gesamtteil für Nachkommen unverändert bleibt.
Angenommen, wir entwickeln eine Kundenbank, denken Sie über die Aufgabe nach, ein Autorisierungsmodul zu entwickeln – Der Benutzer muss sich mit abstrakten Anmeldedaten in die Anwendung einloggen können. Das Autorisierungsmodul muss plattformübergreifend sein, verschiedene Autorisierungstechnologien unterstützen und verschlüsselte Daten verschiedener Plattformen speichern. Um das Modul zu implementieren, wählen wir die plattformübergreifende Kotlin-Sprache. Mithilfe der abstrakten Klasse (Protokoll) des Autorisierungsmoduls schreiben wir eine Implementierung für das MyPhone-Telefon:
class MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage {
fun loginAndPassword() : Pair {
return Pair("admin", "qwerty65435")
}
}
class ServerApiClient {
fun authorize(authorizationData: AuthorizationData) : Unit {
println(authorizationData.login)
println(authorizationData.password)
println("Authorized")
}
}
class AuthorizationData {
var login: String? = null
var password: String? = null
}
interface AuthorizationModule {
abstract fun fetchAuthorizationData() : AuthorizationData
abstract fun authorize(authorizationData: AuthorizationData)
}
class MyPhoneAuthorizationModule: AuthorizationModule {
override fun fetchAuthorizationData() : AuthorizationData {
val loginAndPassword = MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage().loginAndPassword()
val authorizationData = AuthorizationData()
authorizationData.login = loginAndPassword.first
authorizationData.password = loginAndPassword.second
return authorizationData
}
override fun authorize(authorizationData: AuthorizationData) {
ServerApiClient().authorize(authorizationData)
}
}
fun main() {
val authorizationModule = MyPhoneAuthorizationModule()
val authorizationData = authorizationModule.fetchAuthorizationData()
authorizationModule.authorize(authorizationData)
}
Jetzt müssen wir für jedes Telefon/jede Plattform den Code zum Senden der Autorisierung an den Server duplizieren. Dies stellt einen Verstoß gegen das DRY-Prinzip dar. Das obige Beispiel ist sehr einfach, in komplexeren Klassen wird es noch mehr Duplikate geben. Um Codeduplizierungen zu vermeiden, sollten Sie das Template-Methodenmuster verwenden. Lassen Sie uns die gemeinsamen Teile des Moduls in unveränderliche Methoden verschieben und die Funktionalität der verschlüsselten Datenübertragung auf bestimmte Plattformklassen übertragen:
class MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage {
fun loginAndPassword() : Pair {
return Pair("admin", "qwerty65435")
}
}
class ServerApiClient {
fun authorize(authorizationData: AuthorizationData) : Unit {
println(authorizationData.login)
println(authorizationData.password)
println("Authorized")
}
}
class AuthorizationData {
var login: String? = null
var password: String? = null
}
interface AuthorizationModule {
abstract fun fetchAuthorizationData() : AuthorizationData
fun authorize(authorizationData: AuthorizationData) {
ServerApiClient().authorize(authorizationData)
}
}
class MyPhoneAuthorizationModule: AuthorizationModule {
override fun fetchAuthorizationData() : AuthorizationData {
val loginAndPassword = MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage().loginAndPassword()
val authorizationData = AuthorizationData()
authorizationData.login = loginAndPassword.first
authorizationData.password = loginAndPassword.second
return authorizationData
}
}
fun main() {
val authorizationModule = MyPhoneAuthorizationModule()
val authorizationData = authorizationModule.fetchAuthorizationData()
authorizationModule.authorize(authorizationData)
}
Das Brückenmuster bezieht sich auf strukturelle Entwurfsmuster. Sie können die Implementierung der Klassenlogik abstrahieren, indem Sie die Logik in eine separate abstrakte Klasse verschieben. Klingt einfach, oder?
Angenommen, wir implementieren einen Spam-Bot, der in der Lage sein sollte, Nachrichten an verschiedene Arten von Messenger zu senden. Wir implementieren es mithilfe eines gemeinsamen Protokolls:
protocol User {
let token: String
let username: String
}
protocol Messenger {
var authorize(login: String, password: String)
var send(message: String, to user: User)
}
class iSeekUUser: User {
let token: String
let username: String
}
class iSeekU: Messenger {
var authorizedUser: User?
var requestSender: RequestSender?
var requestFactory: RequestFactory?
func authorize(login: String, password: String) {
authorizedUser = requestSender?.perform(requestFactory.loginRequest(login: login, password: password))
}
func send(message: String, to user: User) {
requestSender?.perform(requestFactory.messageRequest(message: message, to: user)
}
}
class SpamBot {
func start(usersList: [User]) {
let iSeekUMessenger = iSeekU()
iSeekUMessenger.authorize(login: "SpamBot", password: "SpamPassword")
for user in usersList {
iSeekUMessennger.send(message: "Hey checkout demensdeum blog! http://demensdeum.com", to: user)
}
}
}
Stellen wir uns nun die Veröffentlichung eines neuen, schnelleren Protokolls zum Senden von Nachrichten für den iSekU-Messenger vor. Um ein neues Protokoll hinzuzufügen, müssen Sie die Implementierung des iSekU-Bots duplizieren und nur einen kleinen Teil davon ändern. Es ist nicht klar, warum dies getan werden sollte, wenn sich nur ein kleiner Teil der Klassenlogik geändert hat. Bei diesem Ansatz wird das DRY-Prinzip verletzt; bei Weiterentwicklung des Produkts macht sich die fehlende Flexibilität durch Fehler und Verzögerungen bei der Implementierung neuer Features bemerkbar. Verschieben wir die Logik des Protokolls in eine abstrakte Klasse und implementieren so das Bridge-Muster:
protocol User {
let token: String
let username: String
}
protocol Messenger {
var authorize(login: String, password: String)
var send(message: String, to user: User)
}
protocol MessagesSender {
func send(message: String, to user: User)
}
class iSeekUUser: User {
let token: String
let username: String
}
class iSeekUFastMessengerSender: MessagesSender {
func send(message: String, to user: User) {
requestSender?.perform(requestFactory.messageRequest(message: message, to: user)
}
}
class iSeekU: Messenger {
var authorizedUser: User?
var requestSender: RequestSender?
var requestFactory: RequestFactory?
var messagesSender: MessengerMessagesSender?
func authorize(login: String, password: String) {
authorizedUser = requestSender?.perform(requestFactory.loginRequest(login: login, password: password))
}
func send(message: String, to user: User) {
messagesSender?.send(message: message, to: user)
}
}
class SpamBot {
var messagesSender: MessagesSender?
func start(usersList: [User]) {
let iSeekUMessenger = iSeekU()
iSeekUMessenger.authorize(login: "SpamBot", password: "SpamPassword")
for user in usersList {
messagesSender.send(message: "Hey checkout demensdeum blog! http://demensdeum.com", to: user)
}
}
}
Einer der Vorteile dieses Ansatzes ist zweifellos die Möglichkeit, die Funktionalität der Anwendung durch das Schreiben von Plugins/Bibliotheken zu erweitern, die abstrahierte Logik implementieren, ohne den Code der Hauptanwendung zu ändern. Was ist der Unterschied zum Strategiemuster? Beide Muster sind sehr ähnlich, jedoch beschreibt Strategy das Umschalten von *Algorithmen*, während Bridge es Ihnen ermöglicht, große Teile *jeder komplexen Logik* zu wechseln.
Die Filmfirma Jah-Pictures drehte einen Dokumentarfilm über kommunistische Rastafarians aus Liberia mit dem Titel „Red Dawn of Marley“. Der Film ist sehr lang (8 Stunden) und interessant, aber vor seiner Veröffentlichung stellte sich heraus, dass in einigen Ländern Szenen und Phrasen aus dem Film als Ketzerei gelten und keine Vertriebslizenz erhalten. Die Produzenten des Films beschließen, manuell und automatisch Momente mit fragwürdigen Phrasen aus dem Film herauszuschneiden. Eine doppelte Kontrolle ist erforderlich, damit die Vertreter des Händlers in einigen Ländern nicht einfach erschossen werden, falls bei der manuellen Inspektion und Installation ein Fehler auftritt. Die Länder werden in vier Gruppen eingeteilt – Länder ohne Zensur, mit mäßiger, mittlerer und sehr strenger Zensur. Es wird beschlossen, neuronale Netze zu verwenden, um den Grad der Häresie im angesehenen Filmausschnitt zu klassifizieren. Für das Projekt werden sehr teure hochmoderne Neuronen angeschafft, auf unterschiedliche Zensurstufen trainiert, die Aufgabe des Entwicklers – Brechen Sie den Film in Fragmente auf und übertragen Sie sie durch eine Kette neuronaler Netze, von frei bis streng, bis eines von ihnen Häresie erkennt. Anschließend wird das Fragment zur manuellen Überprüfung zur weiteren Bearbeitung übertragen. Es ist unmöglich, alle Neuronen zu durchlaufen, weil Ihre Arbeit erfordert zu viel Rechenleistung (schließlich müssen wir immer noch für Strom bezahlen), es reicht aus, bei der ersten anzuhalten, die funktioniert. Naive Pseudocode-Implementierung:
import StateOfArtCensorshipHLNNClassifiers
protocol MovieCensorshipClassifier {
func shouldBeCensored(movieChunk: MovieChunk) -> Bool
}
class CensorshipClassifier: MovieCensorshipClassifier {
let hnnclassifier: StateOfArtCensorshipHLNNClassifier
init(_ hnnclassifier: StateOfArtCensorshipHLNNClassifier) {
self.hnnclassifier = hnnclassifier
}
func shouldBeCensored(_ movieChunk: MovieChunk) -> Bool {
return hnnclassifier.shouldBeCensored(movieChunk)
}
}
let lightCensorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("light"))
let normalCensorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("normal"))
let hardCensorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("hard"))
let classifiers = [lightCensorshipClassifier, normalCensorshipClassifier, hardCensorshipClassifier]
let movie = Movie("Red Jah rising")
for chunk in movie.chunks {
for classifier in classifiers {
if classifier.shouldBeCensored(chunk) == true {
print("Should censor movie chunk: \(chunk), reported by \(classifier)")
}
}
}
Im Allgemeinen ist die Lösung mit einem Array von Klassifikatoren jedoch nicht so schlecht! Stellen wir uns vor, dass wir kein Array erstellen können, wir haben die Möglichkeit, nur eine Klassifikatoreinheit zu erstellen, die bereits die Art der Zensur für ein Filmfragment bestimmt. Solche Einschränkungen sind möglich, wenn eine Bibliothek entwickelt wird, die die Funktionalität der Anwendung (Plugin) erweitert. Verwenden wir das Dekoratormuster – Fügen wir der Klassifikatorklasse einen Verweis auf den nächsten Klassifikator in der Kette hinzu und stoppen den Überprüfungsprozess bei der ersten erfolgreichen Klassifizierung. Daher implementieren wir das Chain of Responsibility-Muster:
import StateOfArtCensorshipHLNNClassifiers
protocol MovieCensorshipClassifier {
func shouldBeCensored(movieChunk: MovieChunk) -> Bool
}
class CensorshipClassifier: MovieCensorshipClassifier {
let nextClassifier: CensorshipClassifier?
let hnnclassifier: StateOfArtCensorshipHLNNClassifier
init(_ hnnclassifier: StateOfArtCensorshipHLNNClassifier, nextClassifier: CensorshipClassifiers?) {
self.nextClassifier = nextClassifier
self.hnnclassifier = hnnclassifier
}
func shouldBeCensored(_ movieChunk: MovieChunk) -> Bool {
let result = hnnclassifier.shouldBeCensored(movieChunk)
print("Should censor movie chunk: \(movieChunk), reported by \(self)")
if result == true {
return true
}
else {
return nextClassifier?.shouldBeCensored(movieChunk) ?? false
}
}
}
let censorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("light"), nextClassifier: CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("normal", nextClassifier: CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("hard")))))
let movie = Movie("Red Jah rising")
for chunk in movie.chunks {
censorshipClassifier.shouldBeCensored(chunk)
}
Das Decorator-Muster bezieht sich auf strukturelle Designmuster.
Der Dekorator wird als Alternative zur Vererbung verwendet, um die Funktionalität von Klassen zu erweitern. Je nach Produkttyp besteht die Aufgabe, die Funktionalität der Anwendung zu erweitern. Der Kunde benötigt drei Arten von Produkten – Einfach, professionell, ultimativ. Basic– zählt die Anzahl der Zeichen, Professional – Funktionen Basic + druckt Text in Großbuchstaben, Ultimate – Basic + Professional + druckt Text mit der Aufschrift ULTIMATE. Wir implementieren es mithilfe der Vererbung:
protocol Feature {
func textOperation(text: String)
}
class BasicVersionFeature: Feature {
func textOperation(text: String) {
print("\(text.count)")
}
}
class ProfessionalVersionFeature: BasicVersionFeature {
override func textOperation(text: String) {
super.textOperation(text: text)
print("\(text.uppercased())")
}
}
class UltimateVersionFeature: ProfessionalVersionFeature {
override func textOperation(text: String) {
super.textOperation(text: text)
print("ULTIMATE: \(text)")
}
}
let textToFormat = "Hello Decorator"
let basicProduct = BasicVersionFeature()
basicProduct.textOperation(text: textToFormat)
let professionalProduct = ProfessionalVersionFeature()
professionalProduct.textOperation(text: textToFormat)
let ultimateProduct = UltimateVersionFeature()
ultimateProduct.textOperation(text: textToFormat)
Jetzt besteht die Anforderung, das Produkt „Ultimate Light“ umzusetzen – Basic + Ultimate, jedoch ohne die Funktionen der Professional-Version. Das erste OH! passiert, weil… Sie müssen für eine so einfache Aufgabe eine separate Klasse erstellen und den Code duplizieren. Lassen Sie uns die Implementierung mithilfe der Vererbung fortsetzen:
Das Beispiel kann zur Verdeutlichung weiterentwickelt werden, aber schon jetzt ist die Komplexität der Unterstützung eines Systems auf Basis einer Vererbungsbasis sichtbar – umständlich und mangelnde Flexibilität. Ein Dekorator ist eine Reihe von Protokollen, die die Funktionalität beschreiben, eine abstrakte Klasse, die einen Verweis auf eine untergeordnete konkrete Instanz der Dekoratorklasse enthält, die die Funktionalität erweitert. Schreiben wir das obige Beispiel mit dem Muster um:
Jetzt können wir Variationen jeder Art von Produkt erstellen – Es reicht aus, die kombinierten Typen beim Anwendungsstart zu initialisieren. Das folgende Beispiel zeigt die Erstellung der Ultimate + Professional-Version:
Das Mediator-Muster bezieht sich auf Verhaltensdesignmuster.
Eines Tages erhalten Sie den Auftrag, eine Scherzanwendung zu entwickeln – Der Benutzer drückt eine Taste in der Mitte des Bildschirms und er ertönt das lustige Quaken einer Ente. Nach dem Hochladen in den App Store wird die Anwendung zum Hit: Alle quakten über Ihre Anwendung, Elon Musk quakt auf seinem Instagram beim nächsten Start eines Super-Hochgeschwindigkeitstunnels auf dem Mars, Hillary Clinton quält Donald Trump bei der Debatte und gewinnt die Wahlen in der Ukraine, Erfolg! Die naive Implementierung der Anwendung sieht folgendermaßen aus:
class DuckButton {
func didPress() {
print("quack!")
}
}
let duckButton = DuckButton()
duckButton.didPress()
Als nächstes entscheiden Sie sich, das Geräusch eines Hundegebells hinzuzufügen. Dazu müssen Sie zwei Schaltflächen zur Auswahl des Geräuschs anzeigen – mit einer Ente und einem Hund. Lassen Sie uns zwei Schaltflächenklassen erstellen: DuckButton und DogButton. Ändern Sie den Code:
class DuckButton {
func didPress() {
print("quack!")
}
}
class DogButton {
func didPress() {
print("bark!")
}
}
let duckButton = DuckButton()
duckButton.didPress()
let dogButton = DogButton()
dogButton.didPress()
Nach einem weiteren Erfolg fügen wir den Klang eines Schweinequietschens hinzu, jetzt gibt es drei Klassen von Tasten:
class DuckButton {
func didPress() {
print("quack!")
}
}
class DogButton {
func didPress() {
print("bark!")
}
}
class PigButton {
func didPress() {
print("oink!")
}
}
let duckButton = DuckButton()
duckButton.didPress()
let dogButton = DogButton()
dogButton.didPress()
let pigButton = PigButton()
pigButton.didPress()
Benutzer beschweren sich darüber, dass sich die Geräusche überlappen. Wir fügen eine Prüfung hinzu, um dies zu verhindern, und stellen gleichzeitig die Klassen einander vor:
class DuckButton {
var isMakingSound = false
var dogButton: DogButton?
var pigButton: PigButton?
func didPress() {
guard dogButton?.isMakingSound ?? false == false &&
pigButton?.isMakingSound ?? false == false else { return }
isMakingSound = true
print("quack!")
isMakingSound = false
}
}
class DogButton {
var isMakingSound = false
var duckButton: DuckButton?
var pigButton: PigButton?
func didPress() {
guard duckButton?.isMakingSound ?? false == false &&
pigButton?.isMakingSound ?? false == false else { return }
isMakingSound = true
print("bark!")
isMakingSound = false
}
}
class PigButton {
var isMakingSound = false
var duckButton: DuckButton?
var dogButton: DogButton?
func didPress() {
guard duckButton?.isMakingSound ?? false == false &&
dogButton?.isMakingSound ?? false == false else { return }
isMakingSound = true
print("oink!")
isMakingSound = false
}
}
let duckButton = DuckButton()
duckButton.didPress()
let dogButton = DogButton()
dogButton.didPress()
let pigButton = PigButton()
pigButton.didPress()
Aufgrund des Erfolgs Ihres Antrags beschließt die Regierung, ein Gesetz zu erlassen, nach dem das Quacksalbern, Bellen und Grunzen auf Mobilgeräten an den restlichen Wochentagen nur von 9:00 bis 15:00 Uhr erfolgen darf Zu diesem Zeitpunkt riskiert der Benutzer Ihrer Anwendung eine Gefängnisstrafe von 5 Jahren wegen obszöner Tonproduktion mit persönlichen elektronischen Geräten. Ändern Sie den Code:
import Foundation
extension Date {
func mobileDeviceAllowedSoundTime() -> Bool {
let hour = Calendar.current.component(.hour, from: self)
let weekend = Calendar.current.isDateInWeekend(self)
let result = hour >= 9 && hour <= 14 && weekend == false
return result
}
}
class DuckButton {
var isMakingSound = false
var dogButton: DogButton?
var pigButton: PigButton?
func didPress() {
guard dogButton?.isMakingSound ?? false == false &&
pigButton?.isMakingSound ?? false == false &&
Date().mobileDeviceAllowedSoundTime() == true else { return }
isMakingSound = true
print("quack!")
isMakingSound = false
}
}
class DogButton {
var isMakingSound = false
var duckButton: DuckButton?
var pigButton: PigButton?
func didPress() {
guard duckButton?.isMakingSound ?? false == false &&
pigButton?.isMakingSound ?? false == false &&
Date().mobileDeviceAllowedSoundTime() == true else { return }
isMakingSound = true
print("bark!")
isMakingSound = false
}
}
class PigButton {
var isMakingSound = false
var duckButton: DuckButton?
var dogButton: DogButton?
func didPress() {
guard duckButton?.isMakingSound ?? false == false &&
dogButton?.isMakingSound ?? false == false &&
Date().mobileDeviceAllowedSoundTime() == true else { return }
isMakingSound = true
print("oink!")
isMakingSound = false
}
}
let duckButton = DuckButton()
let dogButton = DogButton()
let pigButton = PigButton()
duckButton.dogButton = dogButton
duckButton.pigButton = pigButton
dogButton.duckButton = duckButton
dogButton.pigButton = pigButton
pigButton.duckButton = duckButton
pigButton.dogButton = dogButton
duckButton.didPress()
dogButton.didPress()
pigButton.didPress()
Plötzlich fängt die Taschenlampen-Anwendung an, unsere vom Markt zu verdrängen. Lassen wir uns davon nicht unterkriegen und fügen wir eine Taschenlampe hinzu, indem wir die „oink-oink“-Taste und die restlichen Tasten drücken:
import Foundation
extension Date {
func mobileDeviceAllowedSoundTime() -> Bool {
let hour = Calendar.current.component(.hour, from: self)
let weekend = Calendar.current.isDateInWeekend(self)
let result = hour >= 9 && hour <= 14 && weekend == false
return result
}
}
class Flashlight {
var isOn = false
func turn(on: Bool) {
isOn = on
}
}
class DuckButton {
var isMakingSound = false
var dogButton: DogButton?
var pigButton: PigButton?
var flashlight: Flashlight?
func didPress() {
flashlight?.turn(on: true)
guard dogButton?.isMakingSound ?? false == false &&
pigButton?.isMakingSound ?? false == false &&
Date().mobileDeviceAllowedSoundTime() == true else { return }
isMakingSound = true
print("quack!")
isMakingSound = false
}
}
class DogButton {
var isMakingSound = false
var duckButton: DuckButton?
var pigButton: PigButton?
var flashlight: Flashlight?
func didPress() {
flashlight?.turn(on: true)
guard duckButton?.isMakingSound ?? false == false &&
pigButton?.isMakingSound ?? false == false &&
Date().mobileDeviceAllowedSoundTime() == true else { return }
isMakingSound = true
print("bark!")
isMakingSound = false
}
}
class PigButton {
var isMakingSound = false
var duckButton: DuckButton?
var dogButton: DogButton?
var flashlight: Flashlight?
func didPress() {
flashlight?.turn(on: true)
guard duckButton?.isMakingSound ?? false == false &&
dogButton?.isMakingSound ?? false == false &&
Date().mobileDeviceAllowedSoundTime() == true else { return }
isMakingSound = true
print("oink!")
isMakingSound = false
}
}
let flashlight = Flashlight()
let duckButton = DuckButton()
let dogButton = DogButton()
let pigButton = PigButton()
duckButton.dogButton = dogButton
duckButton.pigButton = pigButton
duckButton.flashlight = flashlight
dogButton.duckButton = duckButton
dogButton.pigButton = pigButton
dogButton.flashlight = flashlight
pigButton.duckButton = duckButton
pigButton.dogButton = dogButton
pigButton.flashlight = flashlight
duckButton.didPress()
dogButton.didPress()
pigButton.didPress()
Als Ergebnis haben wir eine riesige Anwendung, die viel Copy-Paste-Code enthält, die Klassen darin sind durch einen toten Link miteinander verbunden – es gibt keine schwache Kopplung, ein solches Wunder ist sehr schwer zu warten und Änderungen in der Zukunft aufgrund der hohen Wahrscheinlichkeit, dass ein Fehler gemacht wird.
Verwenden Sie den Mediator
Fügen wir eine mittlere Mediatorklasse hinzu – ApplicationController. Diese Klasse sorgt für eine lose Kopplung von Objekten, gewährleistet die Trennung der Verantwortlichkeiten zwischen den Klassen und eliminiert doppelten Code. Schreiben wir um:
import Foundation
class ApplicationController {
private var isMakingSound = false
private let flashlight = Flashlight()
private var soundButtons: [SoundButton] = []
func add(soundButton: SoundButton) {
soundButtons.append(soundButton)
}
func didPress(soundButton: SoundButton) {
flashlight.turn(on: true)
guard Date().mobileDeviceAllowedSoundTime() &&
isMakingSound == false else { return }
isMakingSound = true
soundButton.didPress()
isMakingSound = false
}
}
class SoundButton {
let soundText: String
init(soundText: String) {
self.soundText = soundText
}
func didPress() {
print(soundText)
}
}
class Flashlight {
var isOn = false
func turn(on: Bool) {
isOn = on
}
}
extension Date {
func mobileDeviceAllowedSoundTime() -> Bool {
let hour = Calendar.current.component(.hour, from: self)
let weekend = Calendar.current.isDateInWeekend(self)
let result = hour >= 9 && hour <= 14 && weekend == false
return result
}
}
let applicationController = ApplicationController()
let pigButton = SoundButton(soundText: "oink!")
let dogButton = SoundButton(soundText: "bark!")
let duckButton = SoundButton(soundText: "quack!")
applicationController.add(soundButton: pigButton)
applicationController.add(soundButton: dogButton)
applicationController.add(soundButton: duckButton)
pigButton.didPress()
dogButton.didPress()
duckButton.didPress()
Viele Artikel über Anwendungsarchitekturen für Benutzeroberflächen beschreiben das MVC-Muster und seine Ableitungen. Das Modell wird für die Arbeit mit Geschäftslogikdaten verwendet. Die Ansicht oder Präsentation zeigt dem Benutzer Informationen in der Schnittstelle an bzw. sorgt für die Interaktion mit dem Benutzer. Der Controller ist ein Vermittler, der die Interaktion der Systemkomponenten sicherstellt.
The Strategy pattern allows you to select the type of algorithm that implements a common interface, right while the application is running. This pattern refers to the behavioral design patterns.
Suppose we are developing a music player with embedded codecs. The built-in codecs imply reading music formats without using external sources of the operating system (codecs), the player should be able to read tracks of different formats and play them. VLC player has such capabilities, it supports various types of video and audio formats, it runs on popular and not very operating systems.
Imagine what a naive player implementation looks like:
var player: MusicPlayer?
func play(filePath: String) {
let extension = filePath.pathExtension
if extension == "mp3" {
playMp3(filePath)
}
elseif extension == "ogg" {
playOgg(filePath)
}
}
func playMp3(_ filePath: String) {
player = MpegPlayer()
player?.playMp3(filePath)
}
func playOgg(_ filePath: String) {
player = VorbisPlayer()
player?.playMusic(filePath)
}
Next, we add several formats, which leads to the need to write additional methods.Plus, the player must support plug-in libraries, with new audio formats that will appear later.There is a need to switch the music playback algorithm, the Strategy pattern is used to solve this problem.
Let’s create a common protocol MusicPlayerCodecAlgorithm, write the implementation of the protocol in two classes MpegMusicPlayerCodecAlgorithm and VorbisMusicPlayerCodecAlgorithm, to play mp3 and ogg files with-but.Create a class MusicPlayer, which will contain a reference for the algorithm that needs to be switched, then by the file extension we implement codec type switching:
The above example also shows the simplest example of a factory (switching the codec type from the file extension) It is important to note that the Strategy strategy does not create objects, it only describes how to create a common interface for switching the family of algorithms.
In this article I will describe the Iterator pattern.
This pattern refers to the behavioral design patterns.
Print it
Suppose we need to print a list of tracks from the album “Procrastinate them all” of the group “Procrastinallica”.
The naive implementation (Swift) looks like this:
for i=0; i < tracks.count; i++ {
print(tracks[i].title)
}
Suddenly during compilation, it is detected that the class of the tracks object does not give the number of tracks in the count call, and moreover, its elements cannot be accessed by index. Oh…
Filter it
Suppose we are writing an article for the magazine “Wacky Hammer”, we need a list of tracks of the group “Djentuggah” in which bpm exceeds 140 beats per minute. An interesting feature of this group is that its records are stored in a huge collection of underground groups, not sorted by albums, or for any other grounds. Let’s imagine that we work with a language without functionality:
var djentuggahFastTracks = [Track]()
for track in undergroundCollectionTracks {
if track.band.title == "Djentuggah" && track.info.bpm == 140 {
djentuggahFastTracks.append(track)
}
}
Suddenly, a couple of tracks of the group are found in the collection of digitized tapes, and the editor of the magazine suggests finding tracks in this collection and writing about them. A Data Scientist friend suggests to use the Djentuggah track classification algorithm, so you don’t need to listen to a collection of 200 thousand tapes manually. Try:
var djentuggahFastTracks = [Track]()
for track in undergroundCollectionTracks {
if track.band.title == "Djentuggah" && track.info.bpm == 140 {
djentuggahFastTracks.append(track)
}
}
let tracksClassifier = TracksClassifier()
let bpmClassifier = BPMClassifier()
for track in cassetsTracks {
if tracksClassifier.classify(track).band.title == "Djentuggah" && bpmClassifier.classify(track).bpm == 140 {
djentuggahFastTracks.append(track)
}
}
Mistakes
Now, just before sending to print, the editor reports that 140 beats per minute are out of fashion, people are more interested in 160, so the article should be rewritten by adding the necessary tracks.
Apply changes:
var djentuggahFastTracks = [Track]()
for track in undergroundCollectionTracks {
if track.band.title == "Djentuggah" && track.info.bpm == 160 {
djentuggahFastTracks.append(track)
}
}
let tracksClassifier = TracksClassifier()
let bpmClassifier = BPMClassifier()
for track in cassetsTracks {
if tracksClassifier.classify(track).band.title == "Djentuggah" && bpmClassifier.classify(track).bpm == 140 {
djentuggahFastTracks.append(track)
}
}
The most attentive ones noticed an error; the bpm parameter was changed only for the first pass through the list. If there were more passes through the collections, then the chance of a mistake would be higher, that is why the DRY principle should be used. The above example can be developed further, for example, by adding the condition that you need to find several groups with different bpm, by the names of vocalists, guitarists, this will increase the chance of error due to duplication of code.
Behold the Iterator!
In the literature, an iterator is described as a combination of two protocols / interfaces, the first is an iterator interface consisting of two methods – next(), hasNext(), next() returns an object from the collection, and hasNext() reports that there is an object and the list is not over. However in practice, I observed iterators with one method – next(), when the list ended, null was returned from this object. The second is a collection that should have an interface that provides an iterator – the iterator() method, there are variations with the collection interface that returns an iterator in the initial position and in end – the begin() and end() methods are used in C ++ std.
Using the iterator in the example above will remove duplicate code, eliminate the chance of mistake due to duplicate filtering conditions. It will also be easier to work with the collection of tracks on a single interface – if you change the internal structure of the collection, the interface will remain old and the external code will not be affected.
Wow!
let bandFilter = Filter(key: "band", value: "Djentuggah")
let bpmFilter = Filter(key: "bpm", value: 140)
let iterator = tracksCollection.filterableIterator(filters: [bandFilter, bpmFilter])
while let track = iterator.next() {
print("\(track.band) - \(track.title)")
}
Changes
While the iterator is running, the collection may change, thus causing the iterator’s internal counter to be invalid, and generally breaking such a thing as “next object”. Many frameworks contain a check for changing the state of the collection, and in case of changes they return an error / exception. Some implementations allow you to remove objects from the collection while the iterator is running, by providing the remove() method in the iterator.
In diesem Beitrag werde ich das Muster „Snapshot“ beschreiben. oder “Memento”
Dieses Muster bezieht sich auf „Verhaltensmuster“. Designmuster.
Angenommen, wir entwickeln einen Grafikeditor und müssen die Möglichkeit hinzufügen, Aktionen bei einem Benutzerbefehl rückgängig zu machen. Es ist auch sehr wichtig, dass die Systemkomponenten bei der Implementierung dieses Musters keinen Zugriff auf den internen Status der zurückgesetzten „Aktionen“ haben; andere Systemkomponenten haben nur Zugriff auf das Snapshot-Objekt, ohne die Möglichkeit, Änderungen vorzunehmen seinen internen Zustand und stellt eine klare, einfache externe Schnittstelle bereit. Um dieses Problem zu lösen, wird das „Snapshot“-Muster verwendet. oder “Keeper”.
Beispiel für die Arbeit „Snapshot“; unten dargestellt:
Wenn Sie darauf klicken, erscheint ein Sprite. Wenn Sie auf den gewellten Pfeil klicken, wird die Aktion abgebrochen – Der Sprite verschwindet. Das Beispiel besteht aus drei Klassen:
Leinwand, auf der Sprites und die grafische Oberfläche angezeigt werden.
Bildschirm-Controller, er verarbeitet Klicks und steuert die Logik des Bildschirms.
Canvas-Zustände, die bei jeder Änderung bestehen bleiben, werden bei Bedarf mithilfe des Bildschirm-Controllers zurückgesetzt.
Im Kontext des Musters “Snapshot” Klassen sind:
Leinwand – Quelle: Die Zustände dieser Klasse werden als „Schnappschüsse“ gespeichert, für ein späteres Rollback auf Anfrage. Außerdem muss die Quelle in der Lage sein, den Status wiederherzustellen, wenn ein „Snapshot“ an sie übertragen wird.
Controller – Depotbank, diese Klasse weiß, wie und wann Zustände gespeichert/zurückgesetzt werden müssen.
Status – Snapshot, eine Klasse, die den Status der Quelle sowie Datumsinformationen oder einen Index speichert, anhand dessen die Rollback-Reihenfolge genau festgelegt werden kann.
Ein wichtiges Merkmal des Musters ist, dass nur die Quelle Zugriff auf die internen Felder des gespeicherten Zustands im Snapshot haben sollte. Dies ist notwendig, um Snapshots vor Änderungen von außen zu schützen (durch geschickte Entwickler, die unter Umgehung der Kapselung etwas ändern möchten). , die Systemlogik brechen). Um die Kapselung zu implementieren, werden integrierte Klassen verwendet, und in C++ nutzen sie die Möglichkeit, Freundklassen anzugeben. Persönlich habe ich eine einfache Version ohne Kapselung für Rise implementiert und bei der Implementierung für Swift Generic verwendet. In meiner Version – Memento gibt seinen internen Status nur an Entitäten desselben Klassenstatus weiter:
In diesem Beitrag beschreibe ich ein Designmuster namens „Besucher“. oder „Besucher“ Dieses Muster gehört zur Gruppe der Verhaltensmuster.
Lass uns ein Problem finden
Dieses Muster wird hauptsächlich verwendet, um die Beschränkung des Einzelversands in Sprachen mit früher Bindung zu umgehen.
Alice X von NFGPhoto (CC-2.0) Lassen Sie uns eine abstrakte Klasse/ein abstraktes Protokoll Band erstellen, eine Unterklasse von MurpleDeep erstellen und eine Besucherklasse mit zwei Methoden erstellen – Eine für die Ausgabe aller Nachkommen von Band an die Konsole, die zweite für die Ausgabe von MurpleDeep. Hauptsache, die Namen (Signaturen) der Methoden sind gleich und die Argumente unterscheiden sich nur je nach Klasse. Mithilfe der Zwischenausdruckmethode mit dem Band-Argument erstellen wir eine Instanz von Visitor und rufen die Visit-Methode für MurpleDeep auf. Unten ist der Code in Kotlin:
Die Ausgabe lautet: „Dies ist die Band-Klasse“
Wie ist das möglich?!
Warum das passiert, wird in vielen Artikeln mit klugen Worten beschrieben, auch auf Russisch, aber ich schlage vor, Sie stellen sich vor, wie der Compiler den Code sieht, vielleicht wird alles sofort klar:
Das Problem lösen
Es gibt viele Lösungen, um dieses Problem zu lösen. Als nächstes betrachten wir eine Lösung, die das Besuchermuster verwendet. Wir fügen der abstrakten Klasse/dem abstrakten Protokoll die Methode „accept“ mit dem Argument „Visitor“ hinzu, rufen „visitator.visit(this)“ innerhalb der Methode auf und fügen dann eine Überschreibung/Implementierung der Methode „accept“ zur Klasse „MurpleDeep“ hinzu, wodurch wir wiederum entschieden und ruhig gegen DRY verstoßen Besucher.visit(this).< br />Endgültiger Code:
In diesem Beitrag werde ich das Strukturmuster „Lightweight“ beschreiben. oder „opportunistisch“; (Fliegengewicht) Dieses Muster gehört zur Gruppe der Strukturmuster
Sehen wir uns unten ein Beispiel an, wie das Muster funktioniert:
Warum wird es benötigt? Um RAM zu sparen. Ich stimme zu, dass dies in Zeiten der weit verbreiteten Verwendung von Java (das umsonst CPU und Speicher verbraucht) nicht mehr so wichtig ist, aber es lohnt sich, es zu verwenden. Im obigen Beispiel werden nur 40 Objekte ausgegeben, aber wenn Sie die Anzahl auf 120.000 erhöhen, erhöht sich der Speicherverbrauch entsprechend. Schauen wir uns den Speicherverbrauch an, ohne das Flyweight-Muster im Chromium-Browser zu verwenden:
Ohne Verwendung eines Musters beträgt der Speicherverbrauch etwa 300 Megabyte.
Jetzt fügen wir der Anwendung ein Muster hinzu und sehen uns den Speicherverbrauch an:
Bei Verwendung des Musters beträgt der Speicherverbrauch ~200 Megabyte, sodass wir in der Testanwendung 100 Megabyte Speicher eingespart haben. Bei ernsthaften Projekten kann der Unterschied viel größer sein.
Wie funktioniert es?
Im obigen Beispiel zeichnen wir 40 Katzen, der Übersichtlichkeit halber also 120.000. Jede Katze wird als PNG-Bild in den Speicher geladen und dann in den meisten Renderings zum Rendern in eine Bitmap (eigentlich BMP) konvertiert. Dies geschieht aus Geschwindigkeitsgründen, da das Rendern eines komprimierten PNG sehr lange dauert. Ohne das Muster zu verwenden, laden wir 120.000 Bilder von Katzen in den RAM und zeichnen, aber wenn wir das Muster „leichtgewichtig“ verwenden, können wir es nicht verwenden. Wir laden eine Katze in den Speicher und zeichnen sie 120.000 Mal mit unterschiedlichen Positionen und unterschiedlicher Transparenz. Die ganze Magie besteht darin, dass wir beim Rendern Koordinaten und Transparenz getrennt vom Katzenbild implementieren. Das Rendern benötigt nur eine Katze und verwendet ein Objekt mit Koordinaten und Transparenz für das korrekte Rendern.
Wie sieht es im Code aus?
Im Folgenden finden Sie Beispiele für die Sprache Rise< /p>
Ohne Verwendung eines Musters:
Das Katzenbild wird für jedes Objekt in der Schleife separat geladen – catImage.
Muster verwenden:
Ein Bild einer Katze wird von 120.000 Objekten verwendet.
Wo wird es verwendet?
Wird in GUI-Frameworks verwendet, zum Beispiel Apples “Wiederverwendung” (Wiederverwendung) von UITableViewCell-Tabellenzellen, was die Einstiegshürde für Anfänger erhöht, die dieses Muster nicht kennen. Wird auch häufig in der Spieleentwicklung verwendet.
In dieser Notiz beschreibe ich meine Erfahrungen und die Erfahrungen meiner Kollegen bei der Arbeit mit dem Singleton-Muster (Singleton in der ausländischen Literatur) während der Arbeit an verschiedenen (erfolgreichen und weniger erfolgreichen) Projekten. Ich werde beschreiben, warum ich persönlich denke, dass dieses Muster nirgendwo verwendet werden kann, und ich werde auch beschreiben, welche psychologischen Faktoren im Team die Integration dieses Antimusters beeinflussen. Gewidmet allen gefallenen und verkrüppelten Entwicklern, die zu verstehen versuchten, warum alles damit begann, dass eines der Teammitglieder einen kleinen süßen Welpen mitbrachte, der leicht zu handhaben war und keine besondere Pflege und Kenntnisse erforderte, um ihn zu pflegen, und mit dem aufgezogenen Biest endete Ihr Projekt als Geisel zu nehmen, erfordert immer mehr Arbeitsstunden und frisst die Nerven des Benutzers, Ihr Geld und schafft absolut monströse Zahlen für die Beurteilung der Umsetzung scheinbar einfacher Dinge Dinge.
Die Geschichte spielt in einem alternativen Universum, alle Zufälle sind zufällig…
Streicheln Sie die Katze zu Hause mit Cat@Home
Jeder Mensch verspürt manchmal im Leben den unwiderstehlichen Wunsch, eine Katze zu streicheln. Analysten auf der ganzen Welt gehen davon aus, dass das erste Startup, das eine Anwendung für die Lieferung und Vermietung von Katzen entwickelt hat, äußerst beliebt sein wird und in naher Zukunft für Billionen Dollar von Moogle gekauft wird. Bald passiert das – Ein Mann aus Tjumen erstellt die Anwendung Cat@Home und wird bald zum Billionär, die Firma Moogle erhält eine neue Einnahmequelle und Millionen gestresster Menschen erhalten die Gelegenheit dazu Bestellen Sie eine Katze zum weiteren Bügeln und Beruhigen zu sich nach Hause.
Angriff der Klonkrieger
Ein äußerst reicher Zahnarzt aus Murmansk, Alexey Goloborodko, beeindruckt von einem Artikel über Cat@Home von Forbes, beschließt, dass er auch astronomisch reich sein möchte. Um dieses Ziel zu erreichen, findet er über seine Freunde eine Firma aus Goldfield – Wakeboard DevPops, ein Anbieter von Softwareentwicklungsdiensten, beauftragt das Unternehmen mit der Entwicklung eines Cat@Home-Klons.
Siegerteam
Das Projekt heißt Fur&Pure und wird einem talentierten Entwicklungsteam von 20 Leuten anvertraut; Konzentrieren wir uns als Nächstes auf ein mobiles Entwicklungsteam von 5 Personen. Jedes Teammitglied erhält seinen Teil der Arbeit, bewaffnet mit Agile und Scrum, das Team schließt die Entwicklung pünktlich (in sechs Monaten) ohne Fehler ab, veröffentlicht die Anwendung im iStore, wo sie von 100.000 Benutzern mit 5 bewertet wird, davon gibt es viele Kommentare darüber, wie großartig die Anwendung ist, wie ausgezeichneter Service (immerhin ein alternatives Universum). Die Katzen sind gebügelt, die App ist veröffentlicht, alles scheint gut zu laufen. Allerdings hat Moogle es nicht eilig, ein Startup für Billionen Dollar zu kaufen, denn in Cat@Home sind bereits nicht nur Katzen, sondern auch Hunde aufgetaucht.
Der Hund bellt, die Karawane zieht weiter
Der Eigentümer des Antrags entscheidet, dass es an der Zeit ist, Hunde zum Antrag hinzuzufügen, bittet das Unternehmen um eine Bewertung und erhält ungefähr mindestens sechs Monate Zeit, um Hunde zum Antrag hinzuzufügen. Tatsächlich wird die Anwendung erneut von Grund auf neu geschrieben. Während dieser Zeit wird Moogle Schlangen, Spinnen und Meerschweinchen zur Anwendung hinzufügen und Fur&Pur wird nur Hunde erhalten. Warum ist das passiert? Schuld daran ist der Mangel an flexibler Anwendungsarchitektur; einer der häufigsten Faktoren ist das Singleton-Anti-Pattern.
Was ist los?
Um eine Katze zu Hause zu bestellen, muss der Verbraucher eine Anfrage erstellen und diese an das Büro senden, wo das Büro die Anfrage bearbeitet und einen Kurier mit der Katze schickt. Der Kurier erhält bereits die Zahlung für die Dienstleistung. Einer der Programmierer beschließt, eine Klasse „Cat Application“ zu erstellen. mit den notwendigen Feldern bringt diese Klasse über einen Singleton in den globalen Anwendungsraum. Warum macht er das? Um Zeit zu sparen (einen Penny von einer halben Stunde), weil es einfacher ist, eine Anwendung öffentlich zu machen, als die Anwendungsarchitektur zu durchdenken und Abhängigkeitsinjektion zu verwenden. Dann greifen andere Entwickler auf dieses globale Objekt zurück und binden ihre Klassen daran. Zum Beispiel greifen alle Bildschirme selbst auf das globale Objekt „Cat Request“ zu. und Daten zur Anwendung anzeigen. Als Ergebnis wird eine solche monolithische Anwendung getestet und freigegeben. Alles scheint in Ordnung zu sein, aber plötzlich erscheint ein Kunde mit der Anforderung, Hundewünsche in den Antrag aufzunehmen. Das Team beginnt verzweifelt abzuschätzen, wie viele Komponenten im System von dieser Änderung betroffen sein werden. Am Ende der Analyse stellt sich heraus, dass 60 bis 90 % des Codes wiederholt werden müssen, um der Anwendung beizubringen, nicht nur „Request For Cat“ zu akzeptieren. Aber auch „Bewerbung für einen Hund“ ist es in diesem Stadium bereits sinnlos, die Hinzufügung weiterer Tiere zu bewerten, um mit mindestens zwei zurechtzukommen.
So verhindern Sie Singleton
Machen Sie in der Phase der Anforderungserfassung zunächst ausdrücklich deutlich, dass eine flexible, erweiterbare Architektur erforderlich ist. Zweitens lohnt es sich, nebenbei eine unabhängige Prüfung des Produktcodes durchzuführen und dabei zwingend Schwachstellen zu recherchieren. Wenn Sie Entwickler sind und Singletons lieben, empfehle ich Ihnen, zur Besinnung zu kommen, bevor es zu spät ist, sonst sind schlaflose Nächte und ausgefranste Nerven garantiert. Wenn Sie an einem Legacy-Projekt arbeiten, das viele Singletons enthält, versuchen Sie, diese oder das Projekt so schnell wie möglich zu entfernen. Sie müssen vom Anti-Pattern von Singletons-globalen Objekten/Variablen zur Abhängigkeitsinjektion wechseln – das einfachste Entwurfsmuster, bei dem alle erforderlichen Daten in der Initialisierungsphase an eine Instanz einer Klasse übergeben werden, ohne dass eine weitere Bindung an den globalen Raum erforderlich ist.
We use cookies on our website. By clicking “Accept”, you consent to the use of ALL the cookies. Мы используем куки на сайте. Нажимая "ПРИНЯТЬ" вы соглашаетесь с этим.
This website uses cookies to improve your experience while you navigate through the website. Out of these, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may affect your browsing experience.
Necessary cookies are absolutely essential for the website to function properly. This category only includes cookies that ensures basic functionalities and security features of the website. These cookies do not store any personal information.
Any cookies that may not be particularly necessary for the website to function and is used specifically to collect user personal data via analytics, ads, other embedded contents are termed as non-necessary cookies. It is mandatory to procure user consent prior to running these cookies on your website.