So schreiben Sie schnell und einfach DSL in Ruby

Ursprünglicher Autor: Nikhil Mathew
  • Übersetzung
  • Tutorial
Dieser Text ist eine Übersetzung eines Artikels aus dem offiziellen ZenPayroll-Blog. Obwohl ich mit dem Autor in einigen Punkten nicht einverstanden bin, können der in diesem Artikel gezeigte allgemeine Ansatz und die in diesem Artikel gezeigten Methoden für eine breite Palette von Personen nützlich sein, die in Ruby schreiben. Ich entschuldige mich im Voraus dafür, dass einige bürokratische Begriffe falsch übersetzt werden konnten. Nachfolgend sind meine Notizen und Kommentare kursiv hervorgehoben.

In ZenPayroll versuchen wir, die Komplexität der anstehenden Aufgabe zu verbergen. Die Gehaltsabrechnung war traditionell ein bürokratisches Hornissennest, und die Implementierung einer modernen und bequemen Lösung in einer derart unfreundlichen Atmosphäre ist eine attraktive technische Aufgabe, die ohne
Automatisierung nur sehr schwer zu lösen ist .

ZenPayroll schafft jetzt einen landesweiten Service (bereits in 24 Staaten implementiert), was bedeutet, dass wir viele Anforderungen erfüllen, die für jeden Staat einzigartig sind. Zuerst ist uns aufgefallen, dass wir viel Zeit damit verbringen, Vorlagencode zu schreiben, anstatt uns darauf zu konzentrieren, was jeden Zustand einzigartig macht. Bald wurde uns klar, dass wir dieses Problem lösen können, indem wir die Schaffung von DSL nutzen , um den Entwicklungsprozess zu beschleunigen und zu vereinfachen.

In diesem Artikel erstellen wir ein DSL, das so nah wie möglich an dem ist, was wir selbst verwenden.

Wann brauchen wir DSL?


Das Schreiben von DSL ist ein enormer Arbeitsaufwand und kann Ihnen nicht immer bei der Lösung des Problems helfen. In unserem Fall überwogen jedoch die Vorteile die Nachteile:

  1. Der gesamte spezifische Code wird an einem Ort gesammelt.
    In unserer Rails-Anwendung gibt es mehrere Modelle, in denen wir Code implementieren müssen, der für jeden Status spezifisch ist. Wir müssen Formulare, Tabellen und obligatorische Informationen in Bezug auf Mitarbeiter, Unternehmen, Ablagepläne und Steuersätze erstellen. Wir leisten Zahlungen an Regierungsbehörden, übermitteln generierte Formulare, berechnen Einkommenssteuern und vieles mehr. Die DSL-Implementierung ermöglicht es uns, den gesamten shat-spezifischen Code an einem Ort zu sammeln.
  2. Standardisierung der Staaten.
    Anstatt jeden neuen Zustand von Grund auf neu zu erstellen, können wir mit DSL die Erstellung von Zuständen automatisieren und gleichzeitig jeden Zustand flexibel konfigurieren.
  3. Reduzieren Sie die Anzahl der Stellen, an denen Sie einen Fehler machen können.
    Mit einem DSL-System, das Klassen und Methoden für uns erstellt, reduzieren wir den Code für das Boilerplate und haben weniger Stellen, an denen Entwickler eingreifen. Indem wir das DSL qualitativ testen und vor falschen Eingabedaten schützen, verringern wir die Wahrscheinlichkeit eines Fehlers erheblich.
  4. Möglichkeit der schnellen Expansion.
    Wir schaffen ein Framework, das die Umsetzung einzigartiger Anforderungen für neue Staaten erleichtert. DSL ist eine Reihe von Tools, mit denen wir Zeit sparen und die Entwicklung vorantreiben können.

DSL schreiben


In diesem Artikel werden wir uns auf die Erstellung eines DSL konzentrieren, mit dem wir Firmenidentifikationsnummern und Abrechnungsparameter (zur Berechnung der Steuern) speichern können. Obwohl dies nur ein kurzer Überblick darüber ist, was DSL uns bieten kann, ist es dennoch eine vollständige Einführung in das Thema. Unser endgültiger Code, der mit dem generierten DSL geschrieben wurde, sieht ungefähr so ​​aus:

StateBuilder.build('CA') do
  company do
    edd { format '\d{3}-\d{4}-\d' }
    sos { format '[A-Z]\d{7}' }
  end
  employee do
    filing_status { options ['Single', 'Married', 'Head of Household'] }
    withholding_allowance { max 99 }
    additional_withholding { max 10000 }
  end
end  

Großartig! Dies ist sauberer, verständlicher und aussagekräftiger Code, der eine Schnittstelle verwendet, die zur Lösung unseres Problems entwickelt wurde. Fangen wir an.

Parameterdefinition


Lassen Sie uns zunächst entscheiden, was wir als Ergebnis erhalten möchten. Erste Frage: Welche Informationen möchten wir speichern?

In jedem Bundesstaat müssen sich Unternehmen bei den örtlichen Behörden registrieren lassen. Bei der Registrierung in den meisten Bundesstaaten erhalten Unternehmen Identifikationsnummern, mit denen sie Steuern zahlen und Dokumente einreichen müssen. Auf Unternehmensebene müssen wir in der Lage sein, unterschiedliche Identifikationsnummern für verschiedene Staaten zu speichern.

Die Quellensteuer berechnet sich nach der Höhe der Leistungen, die der Arbeitnehmer erhält. Dies sind die Mengen, die auf den W-4-Formularen für jeden Staat definiert sind. Für jeden Staat werden viele Fragen gestellt, um die Steuersätze zu bestimmen: Ihr Steuerzahlerstatus, damit verbundene Leistungen, Invaliditätsleistungen und mehr. Für Mitarbeiter benötigen wir eine flexible Methode, um unterschiedliche Attribute für jedes Bundesland zu definieren und die Steuersätze korrekt zu berechnen.

Die DSL, die wir schreiben werden, verarbeitet Firmenidentifikationsnummern und grundlegende Gehaltsinformationen für Mitarbeiter. Als nächstes verwenden wir dieses Tool, um Kalifornien zu beschreiben. Da Kalifornien einige zusätzliche Bedingungen hat, die bei der Berechnung der Gehälter berücksichtigt werden müssen, werden wir uns auf diese konzentrieren, um zu zeigen, wie DSL entwickelt werden kann.

Ich stelle einen Link zu einer einfachen Rails-Anwendung bereit, damit Sie die in diesem Artikel beschriebenen Schritte ausführen können.

Die folgenden Modelle werden in der Anwendung verwendet:

  • Gesellschaft. Beschreibt das Wesen eines Unternehmens. Speichert Informationen über den Namen, den Typ und das Gründungsdatum.
  • Mitarbeiter Beschreibt einen Mitarbeiter, der für ein Unternehmen arbeitet. Es speichert Informationen über den Namen, die Zahlungen und das Datum der Beschäftigung.
  • CompanyStateField. Jedes Unternehmen ist mehreren CompanyStateField zugeordnet , in denen jeweils bestimmte Informationen zum Unternehmen und zu einem bestimmten Bundesstaat gespeichert sind, z. B. eine Identifikationsnummer. In Kalifornien muss ein Arbeitgeber zwei Nummern haben: eine EDD-Nummer (Employment Development Department) und eine SoS-Nummer (State Secretariat). Weitere Informationen zu diesem Thema finden Sie hier .
  • EmployeeStateField. Jeder Mitarbeiter ist vielen EmployeeStateField zugeordnet , in denen jeweils zustandsspezifische Mitarbeiterinformationen gespeichert sind. Dies sind Informationen, die auf Formularen des Staates W-4 zu finden sind, z. B. Steuerabzüge oder der Status des Steuerpflichtigen. Kalifornien DE4 erfordert Steuerabzüge , einbehaltene Beträge in US-Dollar und den Status eines Steuerpflichtigen (ledig, verheiratet, Haushaltsvorstand).

Wir erstellen Vererbungsmodelle aus CompanyStateField- und EmployeeStateField- Modellen , die dieselben Tabellen wie die Basisklassen verwenden ( Vererbung einzelner Tabellen ). Auf diese Weise können wir ihre landesspezifischen Erben identifizieren und nur eine Tabelle zum Speichern von Daten für alle diese Modelle verwenden. Zu diesem Zweck enthalten beide Tabellen serialisierte Hashes, mit denen wir bestimmte Daten speichern. Obwohl es nicht möglich ist, Abfragen auf der Grundlage dieser Daten durchzuführen, können wir die Datenbank nicht mit nicht verwendeten Spalten aufblasen.
Hinweis übersetzer. Bei Verwendung von Postgres können diese Daten in nativ unterstütztem JSON gespeichert werden.

Unsere Anwendung ist für die Zusammenarbeit mit den Bundesstaaten vorbereitet, und jetzt sollte unser DSL bestimmte Klassen erstellen, die die für Kalifornien erforderliche Funktionalität implementieren.

Was wird uns helfen?


In der Metaprogrammierung kann sich Ruby in seiner ganzen Pracht zeigen. Wir können Methoden und Klassen direkt zur Laufzeit erstellen sowie eine Vielzahl von Metoprogrammierungsmethoden verwenden, was das Erstellen von DSL in Ruby zu einem Vergnügen macht. Rails selbst ist ein
DSL zum Erstellen von Webanwendungen, und ein großer Teil seiner „Magie“ basiert auf Ruby-Metaprogrammierungsfunktionen. Im Folgenden werde ich eine kurze Liste von Methoden und Objekten geben, die für die Metaprogrammierung nützlich sind.

Blöcke


Mit Hilfe von Blöcken können wir Code gruppieren und als Argument an eine Methode übergeben. Sie können mit dem Konstrukt do end oder mit geschweiften Klammern beschrieben werden. Beide Möglichkeiten sind identisch.
Hinweis übersetzer. Entsprechend dem akzeptierten Stil wird die Syntax do end in mehrzeiligen Konstruktionen und geschweifte Klammern in einzeiligen Konstruktionen verwendet. Es gibt auch einige Unterschiede (dank mudasobwa ), die in diesem Fall nicht von Bedeutung sind, aber Ihnen viel Spaß beim Debuggen von Minuten bereiten können.
Originalkommentar wiederhergestellt
Mit Hilfe von Blöcken können wir Code gruppieren und als Argument an eine Methode übergeben. Sie können mit dem Konstrukt do end oder mit geschweiften Klammern beschrieben werden. Beide Möglichkeiten sind identisch.
Hinweis übersetzer. Entsprechend dem akzeptierten Stil wird die Syntax do end in mehrzeiligen Konstruktionen und geschweifte Klammern in einzeiligen Konstruktionen verwendet.


Sie sind beide falsch :)

Eigentlich gibt es einen Unterschied, und es kann zu einem Fehler im Code kommen, von dem es leicht ist, grau zu werden, der aber äußerst schwer zu fassen ist, wenn Sie nicht wissen, was los ist. Siehe:
require 'benchmark'
puts Benchmark.measure { "a"*1_000_000 }
# => 0.000000   0.000000   0.000000 (  0.000427)
puts Benchmark.measure do
  "a"*1_000_000
end
# => LocalJumpError: no block given (yield)
# =>     from IRRELEVANT_PATH_TO_RVM/lib/ruby/2.0.0/benchmark.rb:281:in `measure'
# =>     from (irb):9


Cool, was?

Überlegen Sie, bevor Sie klicken:
Aufgrund der unterschiedlichen Priorität der Operatoren wird der Code des zweiten Beispiels tatsächlich in der folgenden Reihenfolge ausgeführt:
(puts Benchmark.measure) do
  # irrelevant code
end



Korrigieren Sie bitte den Hinweis im Code. Leute lesen :)

Mit ziemlicher Sicherheit haben Sie sie verwendet, wenn Sie eine Methode wie die folgende verwendet haben :
[1,2,3].each { |number| puts number*2 }

Dies ist eine großartige Sache, um DSLs zu erstellen, da sie es uns ermöglichen, Code in einem Kontext zu erstellen und in einem anderen auszuführen. Dies gibt uns die Möglichkeit, eine lesbare DSL zu erstellen, indem Methodendefinitionen in andere Klassen übernommen werden. Wir werden später viele Beispiele dafür sehen.

senden


Mit der send- Methode können wir Objektmethoden (auch private) aufrufen und dabei den Methodennamen als Symbol übergeben. Dies ist nützlich, um Methoden aufzurufen, die normalerweise innerhalb einer Klassendefinition aufgerufen werden, oder um Variablen für dynamische Methodenaufrufe zu interpolieren.

define_method


In Ruby können wir mit define_method Methoden erstellen, ohne die normale Prozedur für die Beschreibung einer Klasse zu verwenden. Als Argumente werden ein String, der der Name der Methode ist, und ein Block verwendet, der ausgeführt wird, wenn die Methode aufgerufen wird.

instance_eval


Dies ist das, was Sie beim Erstellen eines DSL benötigen, ähnlich wie bei Blöcken. Es nimmt einen Block und führt ihn im Kontext des Empfängerobjekts aus. Zum Beispiel:

class MyClass
  def say_hello
    puts 'Hello!'
  end
end
MyClass.new.instance_eval { say_hello } # => 'Hello!'

In diesem Beispiel enthält der Block einen Aufruf der say_hello- Methode , obwohl es in seinem Kontext keine solche Methode gibt. Die von MyClass.new zurückgegebene Klasseninstanz ist der Empfänger für instance_eval, und der Aufruf von say_hello erfolgt in seinem Kontext.

class MyOtherClass
  def initialize(&block)
    instance_eval &block
  end
  def say_goodbye
    puts 'Goodbye'
  end
end
MyOtherClass.new { say_goodbye } # => 'Goodbye!'

Wir beschreiben erneut einen Block, der eine Methode aufruft, die in ihrem Kontext nicht definiert ist. Dieses Mal übergeben wir den Block an den Konstruktor der MyOtherClass- Klasse und führen ihn im Selbstkontext des Empfängers aus, der eine Instanz von MyOtherClass ist . Großartig!

method_missing


Dies ist die Magie, mit der find_by_ * in Rails funktioniert . Jeder Aufruf einer undefinierten Methode gelangt zu method_missing , das den Namen der aufgerufenen Methode und alle an sie übergebenen Argumente erhält. Dies ist eine weitere großartige Sache für DSL, da Sie damit Methoden dynamisch erstellen können, wenn wir nicht wissen, wie sie tatsächlich aufgerufen werden können. Dies gibt uns die Möglichkeit, eine sehr flexible Syntax zu erstellen.

Design und Implementierung von DSL


Nachdem wir einige Kenntnisse über unsere Toolbox haben, ist es an der Zeit, darüber nachzudenken, wie wir unser DSL sehen möchten und wie sie damit weiterarbeiten werden. In diesem Fall werden wir „rückwärts“ arbeiten: Anstatt mit der Erstellung von Klassen und Methoden zu beginnen, werden wir die perfekte Syntax entwickeln und alles andere darauf aufbauen. Wir betrachten diese Syntax als eine Skizze dessen, was wir erhalten möchten. Werfen wir einen Blick darauf, wie alles am Ende aussehen sollte:

StateBuilder.build('CA') do
  company do
    edd { format '\d{3}-\d{4}-\d' }
    sos { format '[A-Z]\d{7}' }
  end
  employee do
    filing_status { options ['Single', 'Married', 'Head of Household'] }
    withholding_allowance { max 99 }
    additional_withholding { max 10000 }
  end
end  

Teilen wir es in Teile und schreiben wir nach und nach den Code, der unser DSL in die Klassen und Methoden einteilt, die wir zur Beschreibung Kaliforniens benötigen.


Wenn Sie mir mit dem bereitgestellten Code folgen möchten, können Sie Schritt-0 der Git-Prüfung ausführen und den Code während des Lesevorgangs zusammen mit mir hinzufügen.


Unser DSL, den wir StateBuilder nannten , ist eine Klasse. Wir beginnen die Erstellung jedes Zustands, indem wir die Methode der Build- Klasse mit der Abkürzung des Zustandsnamens und des Blocks aufrufen, der ihn als Parameter beschreibt. In diesem Block können wir die Methoden aufrufen, die wir Unternehmen und Mitarbeiter aufrufen , und jedem von ihnen einen eigenen Konfigurationsblock übergeben, in dem unsere Spezialmodelle ( CompanyStateField :: CA und EmployeeStateField :: CA ) konfiguriert werden.

# app/states/ca.rb
StateBuilder.build('CA') do
  company do
    # Конфигурируем CompanyStateField::CA
  end
  employee do
    # Конфигурируем EmployeeStateField::CA
  end
end  

Wie bereits erwähnt, ist unsere Logik in der StateBuilder- Klasse enthalten . Wir nennen Block an übertragen self.build im Rahmen der neuen Instanz StateBuilder , so das Unternehmen und die Mitarbeiter müssen identifiziert werden und jeder von ihnen sollte einen Block als Argument nehmen. Beginnen wir mit der Entwicklung, indem wir eine Klassen-CD erstellen, die diesen Bedingungen entspricht.

# app/models/state_builder.rb
class StateBuilder
  def self.build(state, &block)
    # Если не передан блок, выбрасываем исключение
    raise "You need a block to build!" unless block_given?
    StateBuilder.new(state, &block)
  end
  def initialize(state, &block)
    @state = state
    # Выполняем код переданного блока в контексте этого экземпляра StateBuilder
    instance_eval &block
  end
  def company(&block)
    # Конфигурируем CompanyStateField::CA
  end
  def employee(&block)
    # Конфигурируем EmployeeStateField::CA
  end
end  

Jetzt haben wir eine Basis für unseren StateBuilder . Da die Methoden company und employee die Klassen CompanyStateField :: CA und EmployeeStateField :: CA definieren, legen wir fest, wie die Blöcke aussehen sollen, die an diese Methoden übergeben werden. Wir müssen jedes Attribut definieren, über das unsere Modelle verfügen, sowie einige Informationen zu diesen Attributen. Das Schöne an der Erstellung Ihres eigenen DSL ist, dass wir nicht die Standard-Rails-Syntax für Get- und Setter-Methoden sowie Validierungen verwenden müssen. Implementieren wir stattdessen die zuvor beschriebene Syntax.
Hinweis übersetzer. Ein kontroverser Gedanke. Ich würde immer noch versuchen, den Syntaxzoo innerhalb der Anwendung zu minimieren, obwohl dies auf eine gewisse Code-Redundanz zurückzuführen ist.


Es ist Zeit, eine Git-Prüfung Schritt-1 durchzuführen .


Für kalifornische Unternehmen müssen wir zwei ID-Nummern aufbewahren: eine Nummer, die vom kalifornischen Arbeitsministerium (EDD) ausgestellt wurde, und eine Nummer, die vom Staatssekretariat (SoS) ausgestellt wurde.

Das Format der EDD-Nummer lautet "### - #### - #", und das Format der SoS-Nummer lautet "@ #######", wobei @ "ein beliebiger Buchstabe" und # "eine beliebige Zahl" bedeutet.

Im Idealfall sollten wir den Namen unseres Attributs als Namen der Methode verwenden, die als Parameter einem Block übergeben werden soll, der das Format dieses Feldes bestimmt (Es scheint, dass die Zeit für method_missing gekommen ist !).
Hinweis übersetzer. Vielleicht stimmt etwas nicht mit mir, aber die Syntax ist wie
field name, params
Es erscheint mir verständlicher und logischer als vom Autor vorgeschlagen (vergleiche mit Standardmigrationen). Wenn Sie die Syntax des Autors verwenden, ist es auf den ersten Blick nicht selbstverständlich, dass in die Blöcke, die das Unternehmen oder den Mitarbeiter beschreiben, Namen eingetragen werden dürfen, und Sie erhalten auch einen hervorragenden Granatwerfer, mit dem Sie in das Bein schießen können (siehe unten).
Lassen Sie uns schreiben, wie die Aufrufe dieser Methoden für EDD- und SoS-Nummern aussehen werden.

#app/states/ca.rb
StateBuilder.build('CA') do
  company do
    edd { format '\d{3}-\d{4}-\d' }
    sos { format '[A-Z]\d{7}' }
  end
  employee do
    # Конфигурируем EmployeeStateField::CA
  end
end  

Beachten Sie, dass wir hier bei der Beschreibung des Blocks die Syntax von do end in geschweifte Klammern geändert haben , das Ergebnis sich jedoch nicht geändert hat. Wir übergeben den ausführbaren Codeblock weiterhin an die Funktion. Führen wir nun ein ähnliches Verfahren für Mitarbeiter durch.

Gemäß dem kalifornischen Steuervergünstigungszertifikat werden Mitarbeiter nach ihrem Steuerzahlerstatus, der Anzahl der Vergünstigungen und anderen zusätzlich einbehaltenen Beträgen gefragt. Der Status des Steuerzahlers kann ledig, verheiratet oder Familienoberhaupt sein. Steuergutschriften sollten 99 nicht überschreiten, und für die zusätzlich einbehaltenen Beträge sollten maximal 10.000 US-Dollar festgelegt werden. Beschreiben wir sie nun so, wie wir es für die Unternehmensbereiche getan haben.

#app/states/ca.rb
StateBuilder.build('CA') do
  company do
    edd { format '\d{3}-\d{4}-\d' }
    sos { format '[A-Z]\d{7}' }
  end
  employee do
    filing_status { options ['Single', 'Married', 'Head of Household'] }
    withholding_allowance { max 99 }
    additional_withholding { max 10000 }
  end
end  

Jetzt haben wir die endgültige Implementierung für Kalifornien. Unsere DSL beschreibt die Attribute und Validierungen für CompanyStateField :: CA und EmployeeStateField :: CA mithilfe unserer benutzerdefinierten Syntax.

Für uns bleibt nur die Übersetzung unserer Syntax in Klassen, Getter / Setter und Validierungen. Lassen Sie uns die Methoden company und employee in der StateBuilder- Klasse implementieren und den Arbeitscode abrufen .


Dritter Teil des Marleson-Balletts: Git-Checkout Schritt-2


Wir implementieren unsere Methoden und Validierungen, indem wir definieren, was mit den einzelnen Blöcken in den Methoden StateBuilder # company und StateBuilder # employee zu tun ist . Gehen wir ähnlich vor wie bei der Definition von StateBuilder : Erstellen Sie einen Container, der diese Methoden enthält, und führen Sie den übergebenen Block mit instance_eval in seinem Kontext aus.

Rufen Sie unsere Container StateBuilder :: CompanyScope und StateBuilder :: EmployeeScope auf und erstellen Sie in StateBuilder Methoden, die diese Klassen instanziieren.

#app/models/state_builder.rb
class StateBuilder
  def self.build(state, &block)
    # Если не передан блок, выбрасываем исключение
    raise "You need a block to build!" unless block_given?
    StateBuilder.new(state, &block)
  end
  def initialize(state, &block)
    @state = state
    # Выполняем код переданного блока в контексте этого экземпляра StateBuilder
    instance_eval &block
  end
  def company(&block)
    StateBuilder::CompanyScope.new(@state, &block)
  end
  def employee(&block)
    StateBuilder::EmployeeScope.new(@state, &block)
  end
end  


#app/models/state_builder/company_scope.rb
class StateBuilder
  class CompanyScope
    def initialize(state, &block)
      @klass = CompanyStateField.const_set state, Class.new(CompanyStateField)
      instance_eval &block
    end
  end
end  


#app/models/state_builder/employee_scope.rb
class StateBuilder
  class EmployeeScope
    def initialize(state, &block)
      @klass = EmployeeStateField.const_set state, Class.new(EmployeeStateField)
      instance_eval &block
    end
  end
end  

Mit const_set definieren wir Unterklassen von CompanyStateField und EmployeeStateField mit dem Namen unseres Bundesstaates. Dadurch werden die Klassen CompanyStateField :: CA und EmployeeStateField :: CA erstellt , die jeweils von ihrem jeweiligen übergeordneten Element erben.

Jetzt können wir uns auf den letzten Schritt konzentrieren: die Blöcke, die an jedes unserer erstellten Attribute übergeben wurden ( sos , edd , additional_witholding , etc.). Sie werden im Kontext von CompanyScope und EmployeeScope ausgeführt . Wenn wir jedoch versuchen, unseren Code jetzt auszuführen, erhalten wir Fehler beim Aufrufen unbekannter Methoden.

Wir werden die Methode method_missing verwenden , um diese Fälle zu behandeln. Im aktuellen Zustand können wir davon ausgehen, dass jede aufgerufene Methode der Name des Attributs ist und die an sie übergebenen Blöcke beschreiben, wie wir es konfigurieren möchten. Dies gibt uns die „magische“ Möglichkeit, die erforderlichen Attribute zu definieren und
in der Datenbank zu speichern.

Achtung! Wenn Sie method_missing so verwenden, dass es keine Situation gibt, in der super aufgerufen werden kann, kann dies zu unerwartetem Verhalten führen. Tippfehler werden schwer zu verfolgen sein, da sie alle in method_missing fallen werden . Stellen Sie sicher, dass Sie Optionen erstellen, bei denen method_missing- Aufrufe super sind, wenn Sie etwas schreiben, das auf diesen Prinzipien basiert.
Hinweis übersetzer. Im Allgemeinen ist es am besten, die Verwendung von method_missing zu minimieren, da dies das Programm sehr verlangsamt. In diesem Fall ist dies nicht kritisch, da der gesamte Code nur beim Starten der Anwendung ausgeführt wird


Wir definieren die Methode method_missing und übergeben diese Argumente an den letzten Container, den wir erstellen - AttributesScope . Dieser Container ruft store_accessor auf und erstellt Validierungen basierend auf den Blöcken, die wir an ihn übergeben.

#app/models/state_builder/company_scope.rb
class StateBuilder
  class CompanyScope
    def initialize(state, &block)
      @klass = CompanyStateField.const_set state, Class.new(CompanyStateField)
      instance_eval &block
    end
    def method_missing(attribute, &block)
      AttributesScope.new(@klass, attribute, &block)
    end
  end
end  


#app/models/state_builder/employee_scope.rb
class StateBuilder
  class EmployeeScope
    def initialize(state, &block)
      @klass = EmployeeStateField.const_set state, Class.new(EmployeeStateField)
      instance_eval &block
    end
    def method_missing(attribute, &block)
      AttributesScope.new(@klass, attribute, &block)
    end
  end
end  

Nun jedes Mal , wenn wir ein Verfahren in einem Block rufen Unternehmen in app / Staaten / ca.rb, wird er in eine bestimmte Funktion , die wir fallen method_missing . Das erste Argument ist der Name der aufgerufenen Methode und der Name des zu definierenden Attributs. Wir erstellen eine neue Instanz von AttributesScope und übergeben ihr die zu ändernde Klasse, den Namen des zu definierenden Attributs und den Block, der das Attribut konfiguriert. In AttributesScope rufen wir store_accessor auf , das die Getter und Setter für das Attribut definiert und einen serialisierten Hash zum Speichern der Daten verwendet.

class StateBuilder
  class AttributesScope
    def initialize(klass, attribute, &block)
      klass.send(:store_accessor, :data, attribute)
      instance_eval &block
    end
  end
end  

Wir müssen auch die Methoden definieren, die wir in den Blöcken aufrufen, die die Attribute ( Format , Max , Optionen ) konfigurieren und sie in Validatoren umwandeln . Dazu konvertieren wir die Aufrufe dieser Methoden in die von Rails erwarteten Validierungsaufrufe.

class StateBuilder
  class AttributesScope
    def initialize(klass, attribute, &block)
      @validation_options = []
      klass.send(:store_accessor, :data, attribute)
      instance_eval &block
      klass.send(:validates, attribute, *@validation_options)
    end
    private
    def format(regex)
      @validation_options << { format: { with: Regexp.new(regex) } }
    end
    def max(value)
      @validation_options << {
        numericality: {
          greater_than_or_equal_to: 0,
          less_than_or_equal_to: value
        }
      }
    end
    def options(values)
      @validation_options << { inclusion: { in: values } }
    end
  end
end  

Unser DSL ist bereit für den Kampf. Wir haben das CompanyStateField :: CA- Modell zum Speichern und Validieren von EDD- und SoS-Nummern sowie das EmployeeStateField :: CA- Modell zum Speichern und Validieren von Steueranreizen, Steuerzahlerstatus und zusätzlichen Mitarbeitergebühren erfolgreich identifiziert . Trotz der Tatsache, dass unser DSL für die
Automatisierung ganz einfacher Dinge entwickelt wurde, kann jede seiner Komponenten problemlos erweitert werden. Wir können DSL problemlos um neue Hooks erweitern, weitere Methoden in Modellen definieren und diese basierend auf der jetzt implementierten Funktionalität weiterentwickeln.

Unsere Implementierung reduziert Wiederholungen und Boilerplate-Code im Backend merklich, erfordert jedoch weiterhin, dass jeder Status seine eigenen clientseitigen Ansichten hat. Wir haben unsere interne Entwicklung erweitert, um die Kundenseite für neue Bundesstaaten einzubeziehen. Wenn Interesse an den Kommentaren besteht, werde ich einen weiteren Beitrag verfassen, in dem dargelegt wird, wie dies für uns funktioniert.

Dieser Artikel zeigt nur einen Teil davon, wie wir unser eigenes DSL als Erweiterungstool verwenden. Solche Tools haben sich als äußerst nützlich bei der Ausweitung unseres Gehaltsabrechnungsdienstes auf den Rest der USA erwiesen. Wenn Sie an solchen Aufgaben interessiert sind, können wir zusammenarbeiten !

Viel Spaß beim Metaprogrammieren!

Jetzt auch beliebt: