Lösen von Datentypproblemen in Ruby oder Machen Sie Daten wieder zuverlässig

In diesem Artikel möchte ich darüber sprechen, welche Probleme mit Datentypen in Ruby auftreten, auf welche Probleme ich gestoßen bin, wie sie gelöst werden können und wie sichergestellt werden kann, dass auf die Daten, mit denen wir arbeiten, vertraut werden kann.

Bild

Zuerst müssen Sie entscheiden, welche Datentypen es sind. Ich sehe eine sehr erfolgreiche Definition des Begriffs, die im HaskellWiki zu finden ist .
In diesen Typen beschreiben Sie die Daten, mit denen Ihr Programm arbeiten wird.
Aber was ist falsch an Datentypen in Ruby? Um das Problem umfassend zu beschreiben, möchte ich einige Gründe hervorheben.

Grund 1. Probleme von Ruby selbst


Wie Sie wissen, verwendet Ruby eine strikte dynamische Typisierung mit Unterstützung der sogenannten. Ente tippen . Was bedeutet das?

Starke Typisierung erfordert explizites Casting und erzeugt dieses Casting nicht alleine, wie dies beispielsweise in JavaScript der Fall ist. Daher schlägt die folgende Codeliste in Ruby fehl:

1 + '1' - 1 #=> TypeError (String can't be coerced into Integer) 

Bei der dynamischen Typisierung findet die Typprüfung zur Laufzeit statt, sodass wir die Variablentypen nicht angeben und dieselbe Variable zum Speichern von Werten verschiedener Typen verwenden können:

 x = 123 x = "123" x = [1, 2, 3] 

Die folgende Aussage wird normalerweise als Erklärung für den Begriff „Ententypisierung“ gegeben: Wenn sie wie eine Ente aussieht, wie eine Ente schwimmt und wie eine Ente quakt, dann ist dies höchstwahrscheinlich eine Ente. Das heißt, Das Entenschreiben, das sich auf das Verhalten von Objekten stützt, bietet uns zusätzliche Flexibilität beim Schreiben unserer Systeme. Im folgenden Beispiel ist der Wert für uns beispielsweise nicht der Typ des collection , sondern seine Fähigkeit, auf blank? Nachrichten zu antworten blank? und map :

 def process(collection) return if collection.blank? collection.map { |item| do_something_with(item) } end 

Die Fähigkeit, solche „Enten“ zu erschaffen, ist ein sehr mächtiges Werkzeug. Wie jedes andere leistungsstarke Tool erfordert es jedoch große Sorgfalt bei der Verwendung. Dies kann durch die Forschung von Rollbar überprüft werden , bei der mehr als 1000 Schienenanwendungen analysiert und die häufigsten Fehler identifiziert wurden. Und 2 der 10 häufigsten Fehler hängen genau damit zusammen, dass das Objekt nicht auf eine bestimmte Nachricht antworten kann. Daher reicht es möglicherweise nicht aus, das Verhalten des Objekts zu überprüfen, das uns die Ententypisierung in vielen Fällen gibt.

Wir können beobachten, wie die Typprüfung in der einen oder anderen Form zu dynamischen Sprachen hinzugefügt wird:

  • TypeScript bietet JavaScript-Entwicklern eine Typprüfung
  • Typhinweise wurden in Python 3 hinzugefügt
  • Dialyzer leistet gute Arbeit bei der Typprüfung für Erlang / Elixir
  • Steep und Sorbet fügen in Ruby 2.x eine Typprüfung hinzu

Bevor wir jedoch über ein anderes Tool sprechen, mit dem Sie in Ruby effizienter mit Typen arbeiten können, schauen wir uns zwei weitere Probleme an, für die ich eine Lösung finden möchte.

Grund 2. Das allgemeine Problem von Entwicklern in verschiedenen Programmiersprachen


Erinnern wir uns an die Definition der Datentypen, die ich ganz am Anfang des Artikels angegeben habe:
In diesen Typen beschreiben Sie die Daten, mit denen Ihr Programm arbeiten wird.
Das heißt, Typen sollen uns helfen, Daten aus unserem Themenbereich zu beschreiben, in dem unsere Systeme arbeiten. Anstatt jedoch mit den Datentypen zu arbeiten, die wir aus unserem Themenbereich erstellt haben, verwenden wir primitive Typen wie Zahlen, Zeichenfolgen, Arrays usw., die nichts über unseren Themenbereich aussagen. Dieses Problem wird normalerweise als primitive Besessenheit (Besessenheit mit primitiven) klassifiziert.

Hier ist ein typisches Beispiel für primitive Besessenheit:

 price = 9.99 # vs Money = Struct.new(:amount_cents, :currency) price = Money.new(9_99, 'USD') 

Anstatt den Datentyp für die Arbeit mit Geld zu beschreiben, werden häufig reguläre Zahlen verwendet. Und diese Zahl sagt, wie alle anderen primitiven Typen auch, nichts über unseren Themenbereich aus. Meiner Meinung nach ist dies das größte Problem bei der Verwendung von Grundelementen anstelle der Erstellung eines eigenen Typsystems, in dem diese Typen Daten aus unserem Themenbereich beschreiben. Wir selbst lehnen die Vorteile ab, die wir durch die Verwendung von Typen erzielen können.

Ich werde gleich auf diese Vorteile eingehen, nachdem ich ein anderes Thema behandelt habe, das uns unser bevorzugtes Ruby on Rails-Framework beigebracht hat. Dank dessen sind die meisten hier sicher zu Ruby gekommen.

Grund 3. Das Problem, an das uns das Ruby on Rails-Framework gewöhnt hat


Ruby on Rails, oder besser gesagt das darin integrierte ActiveRecord ORM-Framework, hat uns gelehrt, dass Objekte, die sich in einem ungültigen Zustand befinden, normal sind. Meiner Meinung nach ist dies alles andere als die beste Idee. Und ich werde versuchen, es zu erklären.

Nehmen Sie dieses Beispiel:

 class App < ApplicationRecord validates :platform, presence: true end app = App.new app.valid? # => false 

Es ist nicht schwer zu verstehen, dass das app Objekt einen ungültigen Status hat: Die Validierung des App Modells erfordert, dass die Objekte dieses Modells ein platform haben und unser Objekt dieses Attribut leer hat.

Versuchen wir nun, dieses Objekt in einem ungültigen Zustand an einen Dienst zu übergeben, der das App Objekt als Argument erwartet und abhängig vom platform dieses Objekts einige Aktionen ausführt:

 class DoSomethingWithAppPlatform # @param [App] app # # @return [void] def call(app) # do something with app.platform end end DoSomethingWithAppPlatform.new.call(app) 

In diesem Fall würde sogar die Typprüfung bestehen. Da dieses Attribut für das Objekt jedoch leer ist, ist nicht klar, wie der Dienst diesen Fall behandeln wird. Da wir Objekte in einem ungültigen Zustand erstellen können, verurteilen wir uns in jedem Fall dazu, Fälle, in denen ein ungültiger Zustand in unser System gelangt ist, ständig zu behandeln.

Aber lassen Sie uns über ein tieferes Problem nachdenken. Warum überprüfen wir im Allgemeinen die Gültigkeit der Daten? In der Regel, um sicherzustellen, dass kein ungültiger Status in unsere Systeme gelangt. Wenn es so wichtig ist, sicherzustellen, dass ein ungültiger Status nicht zulässig ist, warum lassen wir dann zu, dass Objekte mit einem ungültigen Status erstellt werden? Insbesondere, wenn es sich um so wichtige Objekte wie das ActiveRecord-Modell handelt, das häufig auf die Stammgeschäftslogik verweist. Meiner Meinung nach klingt das nach einer sehr schlechten Idee.

Wenn wir also alle oben genannten Punkte zusammenfassen, treten beim Arbeiten mit Daten in Ruby / Rails die folgenden Probleme auf:

  • Die Sprache selbst verfügt über einen Mechanismus zur Überprüfung des Verhaltens, jedoch nicht über Daten
  • Wie Entwickler in anderen Sprachen tendieren wir dazu, primitive Datentypen zu verwenden, anstatt ein Typensystem für unseren Themenbereich zu erstellen
  • Rails hat uns daran gewöhnt, dass das Vorhandensein von Objekten in einem ungültigen Zustand normal ist, obwohl eine solche Lösung eine ziemlich schlechte Idee zu sein scheint

Wie können diese Probleme gelöst werden?


Ich möchte eine der Lösungen für die oben beschriebenen Probleme anhand eines Beispiels für die Implementierung realer Funktionen in Appodeal betrachten. Bei der Erfassung von Statistiken zu den Statistiken der täglich aktiven Benutzer (im Folgenden: DAU) für Anwendungen, die Appodeal zur Monetarisierung verwenden, kamen wir zu ungefähr der folgenden Datenstruktur, die wir erfassen müssen:

 DailyActiveUsersData = Struct.new( :app_id, :country_id, :user_id, :ad_type, :platform_id, :ad_id, :first_request_date, keyword_init: true ) 

Diese Struktur hat dieselben Probleme, über die ich oben geschrieben habe:

  • Eine Typprüfung fehlt vollständig, was unklar macht, welche Werte die Attribute dieser Struktur annehmen können
  • Es gibt keine Beschreibung der in dieser Struktur verwendeten Daten, und anstelle der für unsere Domäne spezifischen Typen werden Grundelemente verwendet
  • Struktur kann in einem ungültigen Zustand existieren

Um diese Probleme zu lösen, haben wir uns für die dry-types und dry-struct . dry-types ist ein einfaches und erweiterbares Typsystem für Ruby, das zum Gießen, Anwenden verschiedener Einschränkungen, Definieren komplexer Strukturen usw. nützlich ist. dry-struct ist eine Bibliothek, die auf dry-types aufbaut und ein praktisches DSL zum Definieren typisierter Strukturen bietet. Klassen.

Um die Daten unseres Themenbereichs zu beschreiben, die in der Struktur zum Sammeln von DAUs verwendet werden, wurde das folgende Typsystem erstellt:

 module Types include Dry::Types.module AdTypeId = Types::Strict::Integer.enum(AD_TYPES.invert) EntityId = Types::Strict::Integer.constrained(gt: 0) PlatformId = Types::Strict::Integer.enum(PLATFORMS.invert) Uuid = Types::Strict::String.constrained(format: UUID_REGEX) Zero = Types.Constant(0) end 

Jetzt haben wir eine Beschreibung der Daten erhalten, die in unserem System verwendet werden und die wir in der Struktur verwenden können. Wie Sie sehen können, haben die Typen EntityId und Uuid einige Einschränkungen, und die aufzählbaren Typen AdTypeId und PlatformId können nur Werte aus einem bestimmten Satz haben. Wie arbeite ich mit diesen Typen? Betrachten Sie die PlatformId als Beispiel:

 #     enumerable- PLATFORMS = { 'android' => 1, 'fire_os' => 2, 'ios' => 3 }.freeze #       , #     Types::PlatformId[1] == Types::PlatformId['android'] #    ,    #   ,     Types::PlatformId['fire_os'] # => 2 #     ,   Types::PlatformId['windows'] # => Dry::Types::ConstraintError 

Also, mit den Typen selbst herausgefunden. Wenden wir sie nun auf unsere Struktur an. Als Ergebnis haben wir Folgendes erhalten:

 class DailyActiveUsersData < Dry::Struct attribute :app_id, Types::EntityId attribute :country_id, Types::EntityId attribute :user_id, Types::EntityId attribute :ad_type, (Types::AdTypeId ǀ Types::Zero) attribute :platform_id, Types::PlarformId attribute :ad_id, Types::Uuid attribute :first_request_date, Types::Strict::Date end 

Was sehen wir jetzt in der Datenstruktur für DAU? Durch die Verwendung von dry-types und dry-struct wir die Probleme dry-struct die mit der fehlenden Überprüfung des Datentyps und der fehlenden Datenbeschreibung verbunden sind. Jetzt kann jede Person, die sich diese Struktur und die Beschreibung der darin verwendeten Typen angesehen hat, verstehen, welche Werte jedes Attribut annehmen kann.

Was das Problem mit Objekten im ungültigen Zustand dry-struct erspart uns dry-struct Folgendes: Wenn wir versuchen, die Struktur mit ungültigen Werten zu initialisieren, wird eine Fehlermeldung angezeigt. Und in den Fällen, in denen die Richtigkeit der Daten von entscheidender Bedeutung ist (und im Fall der DAU-Erfassung ist dies bei uns der Fall), ist es meiner Meinung nach viel besser, eine Ausnahme zu erhalten, als später zu versuchen, mit ungültigen Daten umzugehen. Wenn der Testprozess für Sie gut etabliert ist (und dies ist bei uns genau der Fall), erreicht der Code, der solche Fehler erzeugt, mit hoher Wahrscheinlichkeit einfach nicht die Produktionsumgebung.

Neben der Unfähigkeit, Objekte in einem ungültigen Zustand zu initialisieren, erlaubt die dry-struct auch nicht, Objekte nach der Initialisierung zu ändern. Dank dieser beiden Faktoren erhalten wir die Garantie, dass sich die Objekte solcher Strukturen in einem gültigen Zustand befinden und Sie sich überall in Ihrem System sicher auf diese Daten verlassen können.

Zusammenfassung


In diesem Artikel habe ich versucht, die Probleme zu beschreiben, die bei der Arbeit mit Daten in Ruby auftreten können, und über die Tools zu sprechen, mit denen wir diese Probleme lösen. Und dank der Implementierung dieser Tools habe ich absolut aufgehört, mir Sorgen um die Richtigkeit der Daten zu machen, mit denen wir arbeiten. Ist das nicht perfekt? Ist dies nicht der Zweck eines Instruments - unser Leben in einem bestimmten Aspekt zu erleichtern? Und meiner Meinung nach machen dry-types und dry-struct ihre Arbeit perfekt!

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


All Articles