In diesem Beitrag beschreibe ich den Aufbau eines Projekts, das aus mehreren Bibliotheken besteht, mit Emscripten. Derzeit unterstützt Emscripten den Aufbau gemeinsam genutzter Bibliotheken nicht, daher besteht der erste Schritt darin, alle Bibliotheken von „Shared“ auf „Static“ zu übertragen. Emscripten arbeitet mit seinen eigenen Include-Dateien, daher muss das Problem mit der Sichtbarkeit von Header-Dateien gelöst werden, indem ich einen Symlink aus dem Systemverzeichnis an die Emscripten-Toolchain weitergeleitet habe:
Wenn Sie CMake verwenden, müssen Sie SHARED->STATIC in der Datei CMakeLists.txt der Methode add_library ändern. Mit den folgenden Befehlen können Sie eine Bibliothek/Anwendung für weitere statische Verknüpfungen erstellen:
emcmake cmake .
emmake make
Als nächstes müssen Sie die Hauptanwendung erstellen und beim Verknüpfen *.a-Bibliotheksdateien angeben. Ich konnte keinen relativen Pfad angeben; der Build wurde erst korrekt abgeschlossen, nachdem die vollständigen Pfade in der Datei CMakeLists.txt angegeben wurden:
Ich habe vor kurzem beschlossen, alle Teile des FlameSteelFramework in separate gemeinsam genutzte Bibliotheken zu verwandeln. Anschließend zeige ich ein Beispiel einer CMakeLists.txt-Datei für FlameSteelCore:
Befehle, die CMake ausführt: Sammelt alle Dateien mit der Erweiterung *.cpp aus dem Verzeichnis src/FlameSteelCore/ in eine gemeinsam genutzte Bibliothek, kopiert alle Header mit der Erweiterung *.h von src/FlameSteelCore nach include/FlameSteelFramework (in meinem Fall). Dies ist /usr/local/include/FlameSteelFramework), kopiert die gemeinsam genutzte Bibliothek in das Verzeichnis lib (/usr/local/lib) Nach der Installation kann es erforderlich sein, den LD-Cache zu aktualisieren – sudo ldconfig. Um auf Ubuntu zu erstellen und zu installieren (wenn Sie über die richtige Build-Toolchain verfügen), führen Sie einfach die folgenden Befehle aus:
cmake . && make && sudo make install
Um den Installationsprozess zu testen, übergebe ich das Make-Präfix an den lokalen Ordner makeInstallTestPlayground:
cmake -DCMAKE_INSTALL_PREFIX:PATH=/home/demensdeum/makeInstallTestPlayground . && make && make install
Vor nicht allzu langer Zeit bin ich auf ein interessantes Projekt namens Cling gestoßen, einen C++-Sprachinterpreter, der unter anderem interaktiv von der Konsole aus arbeiten kann. Sie können das Projekt unter folgendem Link ansehen: https://github.com/root -project/ cling Die Installation für Ubuntu ist einfach – Laden Sie das Archiv für die erforderliche Version herunter, entpacken Sie es, gehen Sie in den Ordner „bin“ und führen Sie „cling“ im Terminal aus. Unten finden Sie ein Beispiel für das Laden der Bibliothek FlameSteelCore, das Initialisieren des Objekts und das Drucken der ID:
Eine interessante Funktion von Emscripten: Wenn Sie eine Spielschleife über emscripten_set_main_loop starten, sollten Sie bedenken, dass die Ausnahmebehandlung über try Catch direkt in der Schleifenmethode erneut hinzugefügt werden muss, weil Laufzeit verliert Try-Catch-Block von außen. Am einfachsten ist es, den Fehlertext über den Browser per Javascript-Benachrichtigung anzuzeigen:
Die Standardimplementierung von Regex kann eine error_complexity-Ausnahme auslösen, wenn sie den regulären Ausdruck für zu komplex hält. Dies geschieht in der aktuellen Implementierung von emscripten, daher empfehle ich Ihnen, Tests zum Parsen durch reguläre Ausdrücke zu implementieren oder Regex-Implementierungen von Drittanbietern zu verwenden.
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.“
Heute gibt es eine Release-Version des Spiels Death-Mask, das zusammen mit der Flame Steel Engine, dem Flame Steel Engine Game Toolkit und anderen Bibliotheken von Grund auf neu erstellt wurde.
In dem Spiel schlüpfen Sie in die Rolle eines Mannes namens Revil-Razorback, der ein Artefakt in Besitz nehmen möchte, das endloses Leben verleiht – Maske des Todes. Wenn Sie auf der Suche herumlaufen, werden Sie viele, viele Male durch Drohnen sterben, bis Sie finden, wonach Sie suchen.
Aktualisierungen der 3D-Modelle und kleinere Gameplay-Änderungen sind in naher Zukunft möglich.
Totenmaske – Cyber-Fantasy-Abenteuer in einem endlosen Techno-Labyrinth. Bereite dich auf den Tod vor!
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 Spiel Death-Mask geht in den öffentlichen Beta-Status (Wild Beta) Der Hauptmenübildschirm des Spiels wurde neu gestaltet, eine Ansicht der blauen Zone des Techno-Labyrinths wurde hinzugefügt, mit angenehmer Musik im Hintergrund.
Als nächstes plane ich, den Gameplay-Controller zu überarbeiten, flüssige Bewegungen wie in alten Shootern, hochwertige 3D-Modelle von Kisten, Waffen, Feinden und die Möglichkeit hinzuzufügen, mich nicht nur über Portale in andere Ebenen des Techno-Labyrinths zu bewegen ( Aufzüge, Türen, Stürze durch Löcher im Boden, Löcher in Wänden), werde ich der Umgebung des generierten Labyrinths etwas Abwechslung verleihen. Ich werde auch an der Spielbalance arbeiten. Die Skelettanimation wird als Polierphase vor der Veröffentlichung hinzugefügt.< /p>
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.
In diesem Artikel werde ich mein Verständnis der Skelettanimation beschreiben, die in allen modernen 3D-Engines zur Animation von Charakteren, Spielumgebungen usw. verwendet wird. Ich beginne die Beschreibung mit dem greifbarsten Teil – – Vertex-Shader, denn der gesamte Berechnungspfad, egal wie komplex er auch sein mag, endet mit der Übergabe der vorbereiteten Daten zur Anzeige an den Vertex-Shader.
Die Skelettanimation geht nach der Verarbeitung auf der CPU in den Vertex-Shader. Ich möchte Sie an die Formel für Scheitelpunkte ohne Skelettanimation erinnern: gl_Position = projectMatrix * viewMatrix * modelMatrix * vertex; Für diejenigen, die nicht verstehen, wie diese Formel zustande kam, können Sie meinen Artikel lesen, der das Prinzip der Arbeit mit Matrizen zur Anzeige von 3D-Inhalten im Kontext von OpenGL beschreibt. Im Übrigen – Formel zur Implementierung einer Skelettanimation: ” vec4animierterVertex = bone0matrix * vertex * bone0weight +” “bone1matrix * vertex * bone1weight +” “bone2matrix * vertex * bone2weight +” “bone3matrix * vertex * bone3weight;\n” ” gl_Position = projectMatrix * viewMatrix * modelMatrix * animatedVertex;\n”
Das heißt, wir multiplizieren die endgültige Knochentransformationsmatrix mit dem Scheitelpunkt und mit dem Gewicht dieser Matrix relativ zum Scheitelpunkt. Jeder Scheitelpunkt kann durch 4 Knochen animiert werden, die Stärke des Aufpralls wird durch den Knochengewichtsparameter reguliert, die Summe der Aufschläge sollte gleich eins sein. Was tun, wenn weniger als 4 Knochen den Scheitelpunkt betreffen? Wir müssen das Gewicht zwischen ihnen aufteilen und die Auswirkung des Rests auf Null setzen. Mathematisch wird die Multiplikation einer Gewichtung mit einer Matrix „Matrix-Skalar-Multiplikation“ genannt. Durch Multiplikation mit einem Skalar können Sie die Wirkung der Matrizen auf den resultierenden Scheitelpunkt zusammenfassen.
Die Knochentransformationsmatrizen selbst werden als Array übertragen. Darüber hinaus enthält das Array Matrizen für das gesamte Modell als Ganzes und nicht für jedes Netz einzeln.
Aber für jeden Scheitelpunkt werden die folgenden Informationen separat übertragen: – Index der Matrix, die den Scheitelpunkt beeinflusst – Gewicht der Matrix, das den Scheitelpunkt beeinflusst Es wird mehr als ein Knochen übertragen, meist wird die Wirkung von 4 Knochen am Scheitelpunkt genutzt. Außerdem muss die Summe der Gewichte der 4 Würfel immer gleich eins sein. Schauen wir uns als Nächstes an, wie es im Shader aussieht. Matrix-Array: “uniform mat4 bonesMatrices[kMaxBones];”
Informationen über die Wirkung von 4 Knochen auf jedem Scheitelpunkt: “Attribut vec2 bone0info;” “Attribut vec2 bone1info;” “Attribut vec2 bone2info;” “attribute vec2 bone3info;”
vec2 – In der X-Koordinate speichern wir den Index des Knochens (und konvertieren ihn im Shader in int), in der Y-Koordinate speichern wir das Gewicht des Aufpralls des Knochens auf den Scheitelpunkt. Warum müssen Sie diese Daten in einem zweidimensionalen Vektor übertragen? Weil GLSL die Übergabe von C-lesbaren Strukturen mit gültigen Feldern an den Shader nicht unterstützt.
Im Folgenden gebe ich ein Beispiel für den Erhalt der notwendigen Informationen aus einem Vektor für die weitere Substitution in die animierteVertex-Formel:
Jetzt sollte die auf der CPU gefüllte Vertex-Struktur wie folgt aussehen: x, y, z, u, v, Bone0index, Bone0weight, Bone1index, Bone1weight, Bone2index, Bone2weight, Bone3index, Bone3weight
Die Vertex-Pufferstruktur wird beim Laden des Modells einmal gefüllt, aber Transformationsmatrizen werden bei jedem Rendering-Frame von der CPU an den Shader übertragen.
In den verbleibenden Teilen beschreibe ich das Prinzip der Animationsberechnung auf der CPU. Bevor ich sie an den Vertex-Shader übertrage, beschreibe ich den Baum der Knochenknoten und gehe durch die Hierarchie Animation-Modell-Knoten-Netz, Matrix Interpolation.
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.
Neuer nicht permanenter Abschnitt „Entwicklertagebücher“; oder Dev Diary im fremden Stil. Die Entwicklung des Spiels Death-Mask ist in vollem Gange, so das Engine-Logo wurde für Flame Steel Engine-Spiele 2019 hinzugefügt, Bildschirm zur Auswahl der anfänglichen Karte nach Insel (grün, rot, schwarz, weiß), Ausgabe von Texturen für Wände, Decke, Boden des Labyrinths, vergrößerte Größe des Spielbereichs. p>
Karte der Stadt der Roten Zone
Als nächstes planen wir, 3D-Modelle für die Umgebung anstelle von Sprites im Doom-Stil hinzuzufügen, und wir planen, Modelle für Waffen, Kisten, Feinde und Freunde hinzuzufügen. Im Gameplay ist geplant, Währung, Geschäfte, die Möglichkeit, Teile der Spielkarte zu kaufen, die interessante Orte mit Beute anzeigen, und den möglichen Standort der „Todesmaske“ hinzuzufügen. Ich möchte auch die Möglichkeit hinzufügen, Begleiter für die Wanderung durch das Cyberlabyrinth anzuheuern. Verfolgen Sie die Nachrichten.
Swift mit den notwendigen Bibliotheken für die Ausführung unter Ubuntu 18.10 erstellen. Neueste verfügbare Version auf der Apple-Website – für Ubuntu 18.04. Basierend auf der Zusammenstellung von der offiziellen Website mit zusätzlichen Bibliotheken von Ubuntu 18.04. Außerdem wurde ein Beispielskript zum Hinzufügen von PATH und LD_LIBRARY_PATH für das Bash-Terminal hinzugefügt: http://www.mediafire.com/file/lrs74mvoj3fti03/swift4.2.3.ubuntu.18.10.x86_64.tar.gz/file
Ich präsentiere Ihnen eine reine deklarative Programmiersprache – Zakaz. Die Hauptidee der neuen Sprache – Die Anwendung enthält in freier Form geschriebene Ausführungsbefehle, die von „Ausführern“ ausgeführt werden müssen. Wenn kein “Performer” Wenn der Befehl nicht ausgeführt werden kann, stoppt die Programmausführung. Anwendungen werden technische Spezifikationen (tez) genannt und müssen die Erweiterung .tez haben. Die Zakaz-Syntax erfordert zwei Regeln:
Jeder Befehl beginnt in einer neuen Zeile
Jeder Befehl muss in einer formalen Sprache geschrieben sein, die für Menschen verständlich ist
Beispiel Hello World.tez:
Zeigen Sie den Text „Hello World“ auf dem Bildschirm anZeigen Sie den Text "Zakaz 'tez' example" auf dem Bildschirm an
Ein Beispiel für eine Spezifikation, die eine Beschreibung des Funktionsprinzips und das Öffnen der Website http://demensdeum.com im Firefox-Browser anzeigt
Text "Website-Demo anzeigen" auf dem Bildschirm anzeigenShow "Sie müssen Firefox auf Ihrem System installiert haben, um dieses 'tez' auszuführen, und es sollte über \"system\"C-Funktion" Text auf dem BildschirmShow "Außerdem sollte \"FirefoxPerformer\" Zakaz Runtime zugewiesen, bitte überprüfen Sie das Handbuch Für weitere Informationen“ Text auf dem BildschirmWebsite mit Adresse "http://demensdeum.com" in Firefox anzeigen
Sie müssen das obige Beispiel zusammen mit dem „Executor“ ausführen. FirefoxPerformer, der den neuesten Befehl zum Rendern einer Site über Firefox verarbeiten kann
Um Ihren Executor zu implementieren, müssen Sie ihn als dynamische Bibliothek mithilfe der abstrakten Klasse ZakazRuntime::Performer implementieren und ihn zusammen mit einem intelligenten Zeiger von der globalen Funktionsmethode createPerformer() zurückgeben. Sie können die FirefoxPerformer-Implementierung als Beispiel verwenden.
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.