
Teilen Sie:
Ben ist ein Entwickler im zweiten Beruf, der zuvor ein Jahrzehnt in den Bereichen Erwachsenenbildung, Community-Organisation und Non-Profit-Management tätig war. Er arbeitete als Anwalt für Entwickler bei Vonage. Er schreibt regelmäßig über die Überschneidung von Gemeindeentwicklung und Technologie. Ursprünglich aus Südkalifornien stammend und lange Zeit in New York City ansässig, wohnt Ben jetzt in der Nähe von Tel Aviv, Israel.
Erkenntnisse aus der Integration der statischen Typüberprüfung in Ruby
Nexmo bietet SDKs in einer Vielzahl von Sprachen an, um die Entwicklergemeinde bei der Arbeit mit unserem vielfältigen API-Angebot zu unterstützen. Es ist durchaus möglich, direkt mit jeder REST-API über HTTP-Aufrufe zu interagieren, die ein Entwickler selbst erstellt. Durch die Nutzung eines SDKs kann ein Entwickler seine Ziele jedoch schneller und mit weniger Aufwand erreichen.
Die Aufgabe, jedes SDK zu entwickeln und zu verbessern, nehmen wir als Team sehr ernst. Über die SDKs werden monatlich Dutzende Millionen API-Aufrufe von Nutzern aus aller Welt getätigt, die an Initiativen arbeiten, die von kleinen Hobbyprojekten bis hin zu multinationalen Unternehmensinfrastrukturen reichen.
Aus diesem Grund suchen wir ständig nach Möglichkeiten, die Erfahrung der Entwickler mit jedem SDK zu verbessern. Jedes SDK ist bestrebt, die Ziele zu erfüllen, die in der Spezifikation der Server-Bibliothek unter Berücksichtigung der einzigartigen Beschränkungen jeder Sprache.
Einer der Allgemeinen Grundsätze der Spezifikation ist der folgende:
Unsere Bibliotheken sollten eindeutig sein.
Dies bedeutet, dass idealerweise jede Klasse, Methode, Konstante usw. definiert und ihr Wert und ihre Parameter den Entwicklern, die auf das SDK angewiesen sind, bekannt sein sollten. Expliziter Code lässt sich leichter einbinden, was wiederum dazu führt, dass der Code einfacher zu debuggen und unvermeidliche Probleme zu beheben sind, wenn sie auftreten.
Um auf dieses Ziel hinzuarbeiten, wird im Ruby SDK zu arbeiten, haben wir damit begonnen, statische Typüberprüfung in unsere Codebasis einzubauen, indem wir die Sorbet Typüberprüfungs-Gem. Die v6.3.0-Version des SDK enthält die Installation und Initialisierung des Gems sowie Methodensignaturen für die Klasse SMS.
Gab es lehrreiche Momente auf dem Weg zu diesem Prozess? Das Refactoring einer dynamisch typisierten Codebasis in einer Sprache, die traditionell immer dynamisch typisiert war, bringt einige Beobachtungen hervor, die es wert sind, mit anderen geteilt zu werden. Während der Arbeit an der Version 6.3.0 sind wir auf die folgenden zwei Perlen gestoßen:
Über die Schnittstelle nachdenken
Das Ruby SDK nutzt die private und protected Schlüsselwörter, um zwischen verschiedenen Komponenten der Bibliotheksarchitektur zu unterscheiden. Code, der nicht innerhalb eines der beiden vorgenannten Schlüsselwörter definiert ist, ist Teil der öffentlichen Schnittstelle.
Was bedeutet das praktisch für Sie als Nutzer des SDK? Es ist aus mehreren Gründen wichtig, diese Unterscheidungen zu treffen.
Der Code, der innerhalb der public Schnittstelle definierter Code ist Code, von dem Sie als Benutzer erwarten können, dass er stabil bleibt, und jedes Refactoring sollte keine brechenden Änderungen an diesem Code mit sich bringen, ohne dass die Benutzer darüber informiert werden und die semantische Version geändert wird. Die Klassen und Methoden, die innerhalb der public Schnittstelle des SDK sind die Mechanismen, auf die Sie sich direkt verlassen, um Ihre Arbeit in Ihren Anwendungsfällen zu erledigen. Dies ist Code, mit dem Sie direkt interagieren, indem Sie ihn in Ihren Methodenaufrufen namentlich aufrufen, d.h. client.sms.send.
Wenn wir nun unsere Verwendung der private und protected zu überprüfen, müssen wir verstehen, wann wir das eine oder das andere verwenden sollten.
Klassischerweise haben Rubyisten nicht viel Zeit damit verbracht, sich über diese Unterscheidungen Gedanken zu machen. Tatsächlich wurde ihre Verwendung von vielen eher als "gute Praxis" angesehen und nicht als ein "Muss", wie es in anderen Sprachen wie Java der Fall ist. Schließlich ermöglicht die Verwendung der #send Methode kann ein Entwickler die Schnittstellendefinition ohnehin umgehen und direkt auf die darin definierten Methoden zugreifen. Wenn wir jedoch beginnen, die statische Typisierung in Ruby zu integrieren, gewinnen diese Schnittstellendefinitionen an Bedeutung und erfordern von uns eine genauere Anwendung.
In Ruby ist der Unterschied zwischen private und protected darin, ob auf eine Methode außerhalb des Bereichs der Klasse, in der sie definiert wurde, zugegriffen werden kann. Schauen wir uns ein Beispiel an, das das private Schlüsselwort:
class MyExample
def public_method
puts "This is public"
end
private
def private_method
puts "This is private"
end
endIn dem obigen Beispiel kann ich die #private_method von innerhalb der MyExample Klasse aufrufen, aber wenn ich eine andere Klasse hätte, selbst wenn sie von MyExamplegeerbt hat, wäre die Methode für sie nicht verfügbar. Wenn ich zum Beispiel eine Klasse habe, die wie folgt definiert ist:
class MySecondExample < MyExample
end
Die private Methode MyExample.private_method wäre nicht für den Bereich der Klasse MySecondExample Klasse. Dies gilt auch dann, wenn die zweite Klasse eine Unterklasse der MyExample Klasse ist.
Dagegen sind Methoden, die mit dem Schlüsselwort protected Schlüsselwortes definierten Methoden für Unterklassen zugänglich sind, die von der übergeordneten Klasse erben. Wenn also das private Schlüsselwort im obigen Beispiel als protectedumgewandelt würde, dann wären die darin enthaltenen Methoden im MySecondExample Klassenbereich zugänglich.
Unabhängig davon, ob die Methode innerhalb der protected oder private Bereich liegt, vermittelt es den Entwicklern, die das SDK verwenden, die Botschaft, dass diese Methoden ohne große Vorankündigung für die Außenwelt geändert werden können. Jede Änderung an ihnen sollte sich nicht auf das öffentliche Verhalten der Anwendung auswirken. Falls doch, stellt sich die Frage, ob diese Methode wirklich in eine nicht-öffentliche Schnittstelle gehört.
Als wir mit der Integration der statischen Typprüfung durch das Sorbet-Gem begannen, war eines der ersten Probleme, auf das wir stießen, dass die Typprüfung Fehler meldete, dass Methoden nicht erreicht werden konnten.
Zum Beispiel nutzt die SMS Klasse, wie auch viele andere Klassen im SDK, die Vorteile der Nexmo::Namespace#request Methode, um die Anfrage an die API zu senden. Aufgrund der inhärenten Flexibilität bei der Strenge der Schnittstellendefinitionen in Ruby, hinderte die Tatsache, dass diese Methode unter dem private Schlüsselwort definiert war und in einer Unterklasse verwendet wurde, hinderte sie nicht daran, tatsächlich so ausgeführt zu werden, wie sie konzipiert war. Da diese Methode jedoch in einer Unterklasse verwendet wurde, sollte sie nach den besten Konventionen des Schnittstellendesigns implizit innerhalb des protected Schlüsselwort definiert werden. Bevor wir also die Schnittstelle zu protected umdefiniert hatten, meldete die Typprüfung den folgenden Fehler:
lib/nexmo/sms.rb:109: Method request does not exist on Nexmo::SMS https://srb.help/7003Eine der hilfreichen Funktionen von Sorbet ist, dass jedem Fehler eine URL angehängt ist, die auf die Dokumentation für diesen Fehlercode verweist. In diesem Fall heißt es in der Dokumentation zu Fehler 7003: This error indicates a call to a method we believe does not exist (a la Ruby’s NoMethodError exception). In der Dokumentation finden Sie Beispiele für problematischen Code und Möglichkeiten, diesen zu beheben, sowie weitere Erläuterungen zum Fehler. In unserem Fall glaube ich, dass der zweite Grund, warum Sorbet diesen Fehler auslösen könnte, auf unseren Code zutrifft:
Selbst wenn die Methode zum Zeitpunkt der Ausführung vorhanden ist, kann Sorbet einen Fehler melden, da die Methode nicht immer vorhanden ist.
Eine Methode, die innerhalb der private Schnittstelle definiert ist, ist für alles außerhalb des Bereichs der Klasse, in der sie definiert wurde, unsichtbar. Es ist zwar möglich, die Flexibilität von Ruby zu nutzen, um die Methode trotzdem aufzurufen, aber das ändert nichts an ihrer Unsichtbarkeit. Daher besteht Sorbet darauf, dass der Code an der Stelle sichtbar ist, von der aus er aufgerufen wird. Dies gewährleistet eine eindeutige Codebasis.
Befolgen Sie jede Methode bis zum Ende
Das zweite Juwel, das wir bei der Einführung von Sorbet in die Codebasis entdeckten, war das gründliche Durchdenken aller Implikationen jeder Methode, die im Code aufgerufen und verwendet wird.
Auch wenn eine Anwendung gut konzipiert ist, kann es vorkommen, dass einige Punkte aus unserem Blickfeld verschwinden. Es kann ein ernsthafter Versuch unternommen werden, sowohl die Erfolgs- als auch die Misserfolgswege der Anwendung zu testen. Der Code ist so aufgebaut, dass er die meisten der häufig auftretenden Randfälle bewältigen kann, aber dennoch kann es zu unbeabsichtigten Folgen kommen.
Ein Bereich, in dem dies auftrat, war der Standardrückgabewert für das Abrufen eines Objekts aus einem Parameter-Hash. Der Code rief #unicode?eine kleine Methode auf, die prüft, ob der Wert des Objekts im Unicode-Format vorliegt oder nicht:
if unicode?(params[:text]) && params[:type] != 'unicode'
...
private
def unicode?(text)
!GSM7.encoded?(text)
end
Die Methode #unicode? Methode würde je nach dem Wert des Parameters einen booleschen Wert zurückgeben. Was geschieht jedoch, wenn kein :text Objekt innerhalb der Parameter gibt? Die Möglichkeit, dass das passiert, ist während der Implementierung unglaublich gering, aber aus der Perspektive des Codes hat es dennoch Auswirkungen auf den Rückgabewert der #unicode? Methode aus.
Simulieren wir diese Aktion ohne Wert für params[:text] simulieren, sehen wir uns an, was zurückgegeben wird:
params[:text]
=> nil
Ruby gibt nil zurück, wenn der Schlüssel nicht innerhalb eines Hashes gefunden werden kann. Dies würde also erklären, warum Sorbet einen Fehler zurückgibt, wenn diese Methode typgeprüft wird. Die Methodensignatur, die für die #unicode? Methode erstellt wurde, besagt:
sig { params(text: String).returns(T::Boolean) }Die obige Signatur deklariert, dass die Methode eine Eingabe vom Typ String Typs annimmt und einen Wert eines Boolean Typs zurückgibt. In dem Fall jedoch, in dem der Parameter nil ist, wird die Eingabe nil und nicht ein String.
An diesem Punkt gibt es mehrere Möglichkeiten. Eine Möglichkeit wäre, die Methodensignatur so umzuschreiben, dass sie Nilable Parameter als Methodeneingabe zuzulassen. Dies würde das technische Problem beseitigen. Das zugrundeliegende architektonische Problem, das Sorbet aufgedeckt hat, wäre damit allerdings nicht beseitigt.
Der Code will keine Situation, in der die Eingabe nil niemals. Wenn der Parameter nilist, dann ist etwas falsch. In diesem Fall sollte der Code dem Benutzer einen Fehler anzeigen, anstatt normal weiterzuarbeiten. Dieser Fehler wird die Erfahrung bei der Verwendung des SDKs verbessern, da er den Entwicklern hilft, Fehler in ihrem Code schneller und früher in ihrem iterativen Prozess zu erkennen, zu diagnostizieren und zu behandeln.
Da es hier darum geht, das der Architektur zugrunde liegende Problem und nicht das Symptom anzugehen, besteht die Lösung darin, eine Methode zu verwenden, die nicht zurückgibt nil zurückgibt, wenn kein Wert geliefert wird. Der Aufruf der Parameterdaten wird umstrukturiert zu:
params.fetch(:text)Die #fetch Methode, wie sie in den Ruby-API-Dokumente löst eine KeyError Ausnahme, wenn der Objektschlüssel nicht gefunden werden kann:
KeyError (key not found: :text)Wenn diese Ausnahme an den Benutzer zurückgegeben wird, ist sie informativ und kann dazu beitragen, den Code in einem frühen Stadium seiner Entwicklung zu verbessern.
Nächste Schritte
Bevor wir uns auf den Weg gemacht haben, statische Typüberprüfung in unser Ruby-SDK einzubauen, haben wir uns viel über die Vor- und Nachteile einer solchen Vorgehensweise unterhalten. Eine Unbekannte, bevor wir den Weg einschlugen, war die Frage, ob dies zu konkreten Vorteilen in unserer SDK-Entwicklung führen würde, und welche das sein würden. Zum jetzigen Zeitpunkt ist die Antwort auf diese unbekannte Frage ein klares Ja.
Die Einführung der statischen Typisierung in unserer Ruby-Codebasis hat dazu beigetragen, unsere Arbeit als Entwickler des SDK auf ein tiefes Nachdenken über die Auswirkungen jeder Designentscheidung, Methodenverwendung und mehr zu konzentrieren. Wir haben einen gründlichen Überprüfungsprozess für jede Pull-Anfrage in unserem Team eingeführt. Wir nutzen die Vorteile von automatisierten Integrationstests und erstellen Tests, die Erfolgs- und Fehlerrouten abdecken. Die Hinzufügung der statischen Typisierung ist eine neue Ebene zur Sicherstellung der Codequalität und der positiven Erfahrung der Entwickler.
Die statische Typisierung in Ruby oder einer anderen dynamisch typisierten Sprache bringt auch paradigmatische Veränderungen in der Art und Weise mit sich, wie der Code geschrieben wird. Sie erzwingt eine Standardisierung, wo vorher viel mehr Flexibilität herrschte. Dieser Punkt ist in der Ruby-Gemeinschaft sehr umstritten. Was ist der bevorzugte Ansatz? Vielleicht liegt die Antwort auf diese Kontroverse irgendwo in der Mitte zwischen den beiden Extremen. Ein gewisses Maß an Flexibilität bewahrt die Magie von Ruby, während eine zunehmende Standardisierung und Konvention die Wahrscheinlichkeit verringert, dass Bugs oder bisher unentdeckte Randfälle erst viel später im Prozess entdeckt werden.
Was das Nexmo Ruby SDK betrifft, so werden wir in den nächsten Monaten weiterhin schrittweise Typen in die Codebasis implementieren. Das Ziel ist es, eine 100% typisierte Codebasis zu erreichen und dies schrittweise.
Nexmo Ruby ist Open-Source und wir freuen uns über Beiträge! Wenn du dich beteiligen möchtest, findest du uns auf GitHub
Teilen Sie:
Ben ist ein Entwickler im zweiten Beruf, der zuvor ein Jahrzehnt in den Bereichen Erwachsenenbildung, Community-Organisation und Non-Profit-Management tätig war. Er arbeitete als Anwalt für Entwickler bei Vonage. Er schreibt regelmäßig über die Überschneidung von Gemeindeentwicklung und Technologie. Ursprünglich aus Südkalifornien stammend und lange Zeit in New York City ansässig, wohnt Ben jetzt in der Nähe von Tel Aviv, Israel.