Software-Designmuster: Pragmatische Erkenntnisse aus dem Schreiben von Produktionscode

Software-Entwurfsmuster

Als unerfahrener, aber eifriger Entwickler in einem dynamischen Startup kann es sich anfühlen, als stünde man direkt vor El Capitan, wenn man gleichzeitig die Codebasis erlernen und produktionsreifen, wartbaren Code schreiben muss. Bei Rescale haben Entwickler großen Spielraum bei der Softwaregestaltung, um bestimmte Funktionen zu implementieren. Kürzlich haben wir die Möglichkeit implementiert, persistente Cluster zu erstellen, die mehrere Jobs verarbeiten können. Die Wahl des optimalen Designmusters, das skalierbar, wartbar und auf die Anwendungsarchitektur abgestimmt ist, erfordert ein gewisses Maß an Sorgfalt. Ich werde meine Erfahrungen bei der Arbeit an dieser großartigen Funktion teilen.
Zur kurzen Wiederholung: Ein „Job“ im Rescale-Jargon ist die Kombination aus Software, beispielsweise Tensorflow für maschinelles Lernen oder BLAST+ für genetische Alignment-Abfragen (neben zahlreichen anderen von Rescale angebotenen Softwarepaketen), einem Hardware-Cluster (z. B. einer C4-Instanz von AWS) und einem jobspezifischen Befehl. Vor der Einführung der persistenten Cluster-Funktion wurde der Cluster im typischen „einfachen“ Anwendungsfall nach Abschluss des Jobs heruntergefahren. Mit der persistenten Cluster-Funktion kann ein Benutzer nun einen Cluster nach seinen genauen Anforderungen erstellen, mehrere Jobs im Laufe des Tages ausführen und ihn nach Abschluss wieder abbauen.   
Dieses Feature umfasste neue API-Endpunkte, die mit dem Python-Django-Framework erstellt wurden, sowie mehrere Änderungen an der Benutzeroberfläche (UI), die mit dem Angular-Framework vorgenommen wurden. Eine der Herausforderungen bei der Benutzeroberfläche bestand darin, die Clusterlistenansicht (als „clusterList View“ bezeichnet) wiederzuverwenden, die als Angular-Direktive (als „clusterList Directive“ bezeichnet) gekapselt ist und unten dargestellt ist.
1-Neuskalierung-Cluster2 Angular-Direktive
Ein entsprechender Controller und eine Vorlage für diese Direktive vervollständigen die Kapselung. Angular-Direktiven ermöglichen die Wiederverwendbarkeit durch die Kapselung der HTML-Vorlage, der Ansichtslogik und der Geschäftslogik. Die Hauptaufgabe dieser Direktive besteht darin, eine Liste von Clustern darzustellen. Sie enthält auch Funktionen zum Löschen von Clustern. Der Controller für diese Ansicht umfasst Funktionen zur Paginierung und zum Navigieren zu einem bestimmten Cluster für detaillierte Informationen.
Unten sehen Sie die clusterList-Direktive, die in der Ansicht „Hardwareeinstellungen“ wiederverwendet wird (die selbst eine eigene gekapselte Direktive ist – die sogenannte „hardwareSettings-Direktive“). In der Ansicht „Hardwareeinstellungen“ kann ein Benutzer entweder einen persistenten Cluster auswählen (den er zuvor hochgefahren hat) oder einen neuen Cluster erstellen (wie im vorherigen Workflow).  
3. Neuskalierungsauftrag
Diese Ansicht bestand aus den folgenden Anweisungen (Attribute wurden der Kürze halber entfernt).
4 Clusterlisten-Direktive
Die Entscheidung zum Softwaredesign konzentrierte sich darauf, wie die Auswahl eines persistenten Clusters aus der clusterList-Direktive dem für die HardwareSettings-Ansicht zuständigen Controller mitgeteilt werden soll. Der HardwareSettings-Controller enthält eine Logik zum Festlegen der Hardwareeinstellungen für das Job-Objekt. Mit der Funktion „Persistente Cluster“ stammen diese Hardwareeinstellungen entweder aus einem persistenten Cluster oder aus einem neu generierten Cluster.

Publish-Subscribe ('pub-sub') vs. Abhängigkeitsinjektion
Mit Angular können die Funktionen $emit und $on auf $rootscope verwendet werden, um eine anwendungsweite Pub-Sub-Architektur zu erstellen.
5 rootscope
Der obige Code überträgt eine Nachricht mit dem Thema „clusterSelected“ zusammen mit dem Clusterobjekt an den Eventbus innerhalb von $rootscope. Andere Teile der Anwendung können diesen Eventbus nutzen, indem sie in $rootscope injizieren und das Thema mit $on abonnieren.
6 rootscope 2
Das Pub-Sub-Designmuster bietet eine elegante Möglichkeit zur netzwerkübergreifenden Kommunikation (in diesem Fall über Direktivengrenzen hinweg) und wird häufig für die Kommunikation über Netzwerkgrenzen hinweg verwendet. Die einfache Implementierung in Angular und die „einfach machen, dass es funktioniert“-Mentalität bei der Arbeit mit einem Zeitplan haben mich als das passende Designmuster überzeugt.    
Eine weitere Designentscheidung, die ich getroffen habe, war die Wiederverwendung des Clusterlisten-Controllers („clusterListCtrl“) in der Ansicht der Hardwareeinstellungen:
7 Clusterlisten-Controller
Mit dieser Entscheidung habe ich clusterListCtrl um zusätzliche Funktionen erweitert, um das in der HardwareSettings-Ansicht gewünschte Verhalten zu erreichen. Dadurch wurde der Controller unnötig aufgebläht und zusätzlich an die Direktive gekoppelt. Ich präsentierte meine Designentscheidungen Alex Kudlick, dem Mitentwickler dieser Funktion und erfahrenen Entwickler mit einer Vorliebe für Softwareentwicklungswissen. Wir diskutierten die Vorteile von Pub-Sub gegenüber Dependency Injection.

Abhängigkeitsspritze
Die Abhängigkeitsinjektion, die eng mit der Inversion of Control verwandt ist, ist ein Muster, bei dem der Abhängigkeitsfluss umgekehrt wird. Im Gegensatz dazu wird im Pub-Sub-Beispiel eine Abhängigkeit von allen Abonnenten erstellt, die das Thema „clusterSelected“ abhören, das von der clusterList-Direktive veröffentlicht wird, wenn ein Klick erkannt wird – der Abhängigkeitsfluss fließt von der Quelle nach außen.
Bei der Abhängigkeitsinjektion wird ein Klickhandler „on-cluster-click“ als Attribut in die ClusterList-Direktive injiziert oder übergeben, wie unten gezeigt.
Ansicht der 8 Hardwareeinstellungen
Dieses Attribut ist nun eine Eigenschaft im isolierten Bereich der ClusterList-Direktive. Der Controller für die HardwareSettings-Ansicht enthält eine Funktion im $scope namens toggleClusterSelected, die die Logik für die Auswahl eines persistenten Clusters enthält. Wenn die ClusterList-Direktive einen Klick erkennt, wird die Funktion toggleClusterSelected aufgerufen.
Die Wiederverwendbarkeit der ClusterList-Direktive mit Abhängigkeitsinjektion wird jetzt anhand der Art und Weise veranschaulicht, wie die ClusterList-Direktive Klicks in der ClusterList-Ansicht verarbeitet.
9 Clusterlistenansicht
Der Controller für die ClusterList-Ansicht enthält die Funktion „goToCluster“, die aufgerufen wird, wenn der Klickhandler in dieser Ansicht ausgeführt wird. Das Obige veranschaulicht die Verwendung der Abhängigkeitsinjektion. Durch die Bindung verschiedener Ausdrücke (die Methoden „toggleClusterSelected“ und „goToCluster“) an den Klickhandler kann die ClusterList-Direktive je nach Kontext, in dem sie verwendet wird, unterschiedliches Verhalten aufweisen.  

Lernen
Mit der Pub-Sub-Architektur ist die Fähigkeit, ein Ereignis anwendungsweit zu übertragen und so über Grenzen hinweg zu kommunizieren, in Angular elegant und einfach zu bewerkstelligen – sie erfüllt auch das Ideal der losen Kopplung. Bei der Verwendung von $rootScope.$emit wurden Nachrichten über $rootScope, den übergeordneten Bereich einer Angular-Anwendung, übertragen. Dies führte zu einem Problem, da Listener in verschiedenen Teilen der Anwendung (es gab Abonnenten für „clusterSelected“ im HardwareSettings-Controller und auch im ClusterList-Controller) sich gegenseitig störten. Beispielsweise löste in der HardwareSettings-Ansicht ein Klick auf die Clusterliste goToCluster (unerwünscht) und toggleClusterSelected (erwünschtes Verhalten) aus. goToCluster setzte sich jedoch durch, und somit wurde das richtige Verhalten nicht erreicht. Dieses Problem wurde durch die Verwendung von $scope.$emit und $scope.$on umgangen, wodurch die Nachrichtenübermittlung auf den relevanten Bereich statt auf den anwendungsweiten $rootScope beschränkt wurde. Es besteht jedoch weiterhin die Möglichkeit von Nebeneffekten und Störungen, wenn mehrere Abonnenten auf dieselbe Nachricht reagieren.  
Ein weiterer Nachteil war, dass Komponenten, die die Nachricht „clusterSelected“ abonniert hatten – der HardwareSettings-Controller und der ClusterList-Controller – eine implizite Abhängigkeit von der ClusterList-Direktive bildeten. Ein zukünftiger Entwickler müsste den Hauptteil der Controller untersuchen, um festzustellen, ob eine Abhängigkeit von der Nachricht „clusterSelected“ besteht. Bei zwei Abonnenten ist dies möglicherweise kein großes Problem, aber mit zunehmender Anzahl an Abonnenten kann dieser Ansatz aufwändig zu pflegen sein und zu fehlerhaftem Code führen.
Bei der Abhängigkeitsinjektion sind die Abhängigkeiten explizit. Ein Entwickler kann entweder die Attribute im Direktiven-Markup scannen oder den isolierten Bereich in der Direktive scannen, um eine vollständige Liste der Abhängigkeiten zu erhalten.    
10 Clusterlistenansicht2
Darüber hinaus ist die Direktive durch Angabe der entsprechenden Abhängigkeiten und Ausdrucksbindungen in verschiedenen Kontexten wiederverwendbar. Ich habe gelernt, dass man sich als Softwareentwickler Zeit nehmen muss, um die Konsequenzen einer bestimmten Designentscheidung zu durchdenken – mit Blick auf Wartbarkeit und Skalierbarkeit, und dass letztendlich explizit besser ist als implizit.

Autorin

  • Adam McKenzie

    Als CTO ist Adam für die Leitung der HPC- und Kundenerfolgsteams verantwortlich. Adam begann seine Karriere bei Boeing, wo er sieben Jahre lang an der 787 arbeitete und Struktur- und Softwareentwicklungsprojekte zur Konstruktion, Analyse und Optimierung der Tragflächen leitete. Adam hat einen Bachelor of Science in Maschinenbau mit Auszeichnung von der Oregon State University.