[RUBY] Überlegungen zu Schienen und sauberer Architektur

Ich hatte die Gelegenheit, über das Design von Rails-Anwendungen nachzudenken, und werde daher zusammenfassen, was ich dabei dachte.

Hintergrund

Ein Unternehmen, das hauptsächlich Rails verwendet, hat beschlossen, eine neue Webanwendung basierend auf 0 zu erstellen. Das Unternehmen führt eine ziemlich große App auf Rails aus, und Rails selbst verfügt über genügend Wissen.

Andererseits bin ich der Meinung, dass die traditionelle Rails-Entwicklungsmethode die Grenzen einer groß angelegten Entwicklung hat, und es besteht auch der Wunsch, diese Gelegenheit zu nutzen, um die Prcatice des Designs zu erkunden.

Der Autor hat ein gewisses Vertrauen in das Design der Anwendung, aber die Sprachen, die ich normalerweise verwende, sind hauptsächlich Scala und NodeJS (TypeScript). Ich habe viele Male mit Rails entwickelt und bin nicht besonders unpraktisch in der Entwicklung, aber ich weiß nicht viel über die detaillierten Macken des Frameworks.

Diese Anforderung berücksichtigt nicht, Rails selbst durch eine andere Sprache oder ein anderes Framework zu ersetzen, um die Fähigkeiten des ursprünglichen Teams zu nutzen.

Der Hauptzweck dieses Dokuments besteht darin, zu prüfen, wie Rails genutzt und moderne Designtechniken integriert werden können.

Grundlegende Designrichtlinie

Unterlegen Sie Clean Architecture.

Clean Architecture (CA) ist eine Entwurfsmethode, die in den letzten Jahren schnell populär geworden ist, und viele Ingenieure haben sie in Japan als Ergebnis der Veröffentlichung der japanischen Version des Buches erwähnt.

Der Autor selbst hat Erfahrung in der Entwicklung basierend auf einigen CAs (einige wurden von ihm selbst entworfen und einige basieren auf menschlichem Design), und ich finde diese Methode sehr cool.

Auf der anderen Seite habe ich auch einige Probleme, wenn ich CA in das vorhandene Webanwendungsframework einbaue, und ich dachte, ich würde gerne mein eigenes Gewinnmuster finden, indem ich es in Betracht ziehe, und habe mich daher entschlossen, dies zu dokumentieren. Es ist eines der Motive.

Übrigens gibt es zum Zeitpunkt des Schreibens dieses Abschnitts noch einige unangenehme Punkte, daher hoffe ich, dass sie durch Schreiben organisiert werden können.

Inkompatibilität zwischen CA und Rails

Wie wenden Sie die Essenz von CA auf Rails an? Ich begann über den Vorschlag nachzudenken, aber bevor ich ihn wusste, stieß ich auf seine Unvereinbarkeit.

Dies ist eine Selbstverständlichkeit, wenn Sie darüber nachdenken.

Dies liegt daran, dass Clean Architecture auf "** eine universelle Entwurfsmethode abzielt, die unabhängig vom verwendeten Framework funktioniert ", während Anwendungsframeworks wie Rails " vom Framework abhängen". Dies liegt daran, dass das Ziel darin besteht, die Entwicklungskosten zu minimieren ** ".

Mit anderen Worten, die Richtung, die Sie von Anfang an anstreben, ist genau umgekehrt und kann daher nicht kompatibel sein.

Insbesondere ist Rails ein Monster-Framework, das die "Minimierung der Entwicklungskosten durch Abhängigkeit vom Framework" bis zum Äußersten verfolgt, sodass die Kompatibilität als die schlechteste bezeichnet werden kann.

Die Lösung für dieses Problem in CA besteht darin, dass es in konzentrischen Kreisen (Entity und Usecase) implementiert werden sollte, ohne die Framework-Funktionalität zu verwenden. Tatsächlich haben die Projekte, an denen ich beteiligt war, diesen Ansatz gewählt (wenn nicht vollständig).

Sie können den gleichen Ansatz mit Rails verfolgen, aber ich kann nicht ehrlich sagen, dass es gut ist, da es die Güte von Rails völlig beeinträchtigt.

Wenn der Rails Way einen Prozess ausführt, der nur drei Zeilen erfordert, sind möglicherweise Dutzende von Codezeilen in mehreren Dateien gemäß der CA-Methode erforderlich. Lohnt sich der Preis also? Ist zweifelhaft.

Ich meine, wenn Sie sich selbst entwickeln, wählen Sie in diesem Fall definitiv Rails Way. .. .. ..

Vielleicht geht es vielen Rails-Programmierern auf der Welt genauso. Wie erwartet ist ein vollständig CA-konformer Ansatz für Rails-Ingenieure wahrscheinlich nicht akzeptabel.

Mit anderen Worten, die Schlussfolgerung dieses Abschnitts lautet, dass wir in diesem Fall über einen neuen Weg nachdenken müssen, um die Essenz von CA zu integrieren und gleichzeitig die Güte von Rails beizubehalten. (Wenn Sie die CA-konforme Methode anwenden, brauchen Sie meiner Meinung nach einen starken Grund, um sie zu überzeugen, aber ich kann keinen Moment darüber nachdenken. Wie oben erwähnt, sind die Ziele unterschiedlich. Führen Sie daher die Vorteile in jeder Achse auf. Aber ich denke nicht, dass es eine Art religiöser Krieg ist und ich kann den anderen überzeugen, und ich persönlich denke nicht, dass einer richtig und der andere falsch ist. )

CA Entity und ActiveRecord

Rails ist Active Record. Die Bequemlichkeit von Rails beruht hauptsächlich auf der Existenz von ActiveRecord, und es ist keine Übertreibung zu sagen, dass die Verwendung von Rails keinen Sinn macht, wenn Sie ActiveRecord nicht verwenden.

Laut CA sollte Entity unabhängig von ActiveRecord neu definiert werden, aber in Rails halte ich es nicht für eine gute Idee, ActiveRecord hier zu entfernen.

In Ruby, einer dynamisch typisierten Sprache, können keine Schnittstellen erstellt werden, und nur der DuckTyping-Newsletter behandelt Objekte. Selbst wenn Sie ein einfaches Modell neu definieren, ist es wahrscheinlich, dass es irgendwann zu einem Objekt wird, das mit ActiveRecord ausgetauscht werden kann. Korrekt. .. ..

Wenn das als Argument an die Methode übergebene Objekt funktioniert, unabhängig davon, ob es sich um eine CA-Entität oder ActiveRecord handelt, und es keine Möglichkeit gibt, es einzuschränken, kann die Neudefinition eine entmutigende Aufgabe sein. Ich befürchte, dass.

Wenn ja, wäre es meiner Meinung nach eine Ameise, das ActiveRecord-Modell zu verwenden, da es für Entity ist, das derzeit eine einfache Tabellenzuordnung durchführen kann.

Ich halte es jedoch nicht für eine gute Idee, Methoden zu definieren, die in direktem Zusammenhang mit der Geschäftslogik in ActiveRecord stehen. Wenn Sie solche Methoden definieren müssen, definieren Sie sie neu.

Ich denke auch, dass es besser ist, Dinge neu zu definieren, die in mehreren Tabellen in der Datenbank gespeichert sind, wie z. B. Rechnungen, aber als Geschäftseinheiten untrennbar miteinander verbunden sind.

Die Grenze zwischen dem, was neu definiert werden muss und dem, was nicht, ist nicht eindeutig. Derzeit ist es jedoch in Ordnung, ActiveRecord zunächst direkt zu verwenden und mit der Regel der Neudefinition zu beginnen, wenn Sie dies benötigen. Ich werde. (Dies ist eine Entscheidung, die auf der geringen Anzahl von Projektmitgliedern zu Beginn basiert. Ich denke, es ist besser, von Anfang an etwas sorgfältiger über ein Projekt mit vielen Mitgliedern nachzudenken, aber es hat überhaupt nichts mit einem so großen Projekt zu tun. Hier gibt es keine weiteren Überlegungen.)

Bei der Neudefinition ist es nicht erforderlich, ActiveRecord vollständig zu entfernen. Es reicht aus, es in einer Umbruchform zu definieren. Zum Beispiel so

#Rechnung
class Bill

  initialize(ar_model)
    @ar_model = ar_model
  end

  def corp_name
    @ar_model.corp_name
  end

  #Abrechnung
  def bill_details
    #Ich möchte Relation nicht entlarven_ein
    #Ändern Sie es gegebenenfalls so, dass es der im Konstruktor neu definierten Entität zugeordnet wird.
    @ar_model.bill_details.to_a 
  end
end

Das Problem ist, dass ActiveRecord viele Nebenwirkungsmethoden (Speichern, Aktualisieren usw.) hat, die von überall aufgerufen werden können, aber vorerst "Verbieten Sie die Verwendung von Nebenwirkungsmethoden aus anderen Dateien als dem Repository". Ich denke, es reicht aus, Regeln aufzustellen. (Dies ist auch eine Wahl für eine kleine Anzahl von Personen. Ich denke, es ist möglich, diese Regel mit Lint zu überprüfen, unabhängig davon, ob sie tatsächlich durchgeführt wird oder nicht.)

Um ehrlich zu sein, glaube ich nicht, dass ich meine Meinung ändern könnte, aber vorerst ist der Start so. (Ausgelassen für kleine Gruppen oder weniger)

Verzeichnisaufbau

Die Verzeichnisstruktur, als ich die CA-Struktur in einem anderen Projekt als Rails erstellt habe, war wie folgt.

- domain
  - <Domainname 1>
    - <Entitätsdatei>
    - <Entitätsdatei>
  - <Domainname 2>
    - <Entitätsdatei>
    - <Entitätsdatei>
  - ...
- repository
  - <Domainname 1>
    - <Repository-Datei>
  - <Domainname 2>
    - ...
- usecase
  - <Domainname 1>
    - <Usecase-Datei>
    - <Usecase-Datei>
  - <Domainname 2>
    - ...

Der Domänenname ist ein Name, der den von der Anwendung behandelten Bereich weitgehend voneinander trennt, z. B. "Benutzer" oder "Bestellung".

Für Scala-Projekte ist diese Konfiguration von Bedeutung, da Teilprojekte auch verwendet werden können, um Domänenschichtdateien absolut unabhängig von Anwendungsfällen und Repositorys zu machen.

Aber was ist mit Rails?

Leider scheint Rails solche Abhängigkeiten nicht einschränken zu können.

Es gibt ein weiteres Problem. Rails empfiehlt dringend, die Benennung gemäß Autoload vorzunehmen. In der obigen Konfiguration lautet der Modulname + Klassenname

Entity, Repository und Usecase landen alle im selben Namespace. Das ist nicht sehr gut.

Geh noch einen Schritt weiter,

Für Japanischsprachige ist es jedoch natürlicher, dass die Hauptmodifikation an erster Stelle steht

Ist bequemer.

Aus diesem Grund denke ich, dass die Verzeichnisstruktur wie folgt aussehen sollte.

- app
  - domain
    - <Domainname 1>
      - entity
        - <Entitätsdatei>
      - repository
        - <Repository-Datei>
      - usecase
        - <Usecase-Datei>
    - <Domainname 2>

Unter der Annahme, dass diese Richtlinie eingehalten wird, sind die Probleme und Antworten, die im Voraus aufgetreten sind, wie folgt.

Allgemeine Namen wie "Benutzer" und "Reihenfolge" sind Modulnamen der obersten Ebene.

Ich denke, es ist ziemlich gut, weil es leicht zu verstehen ist, dass der zu behandelnde Bereichsname auf der obersten Ebene angezeigt wird.

In Rails ist es jedoch üblich, ActiveRecord direkt unter Modellen zu definieren. Da "Benutzer" und "Bestellung" wahrscheinlich so viele Namen wie möglich haben, wird die ActiveRecord-Seite unter dem Modul auf "AR :: Benutzer" usw. gesetzt. Muss platziert werden.

Es gibt auch einen Vorschlag, die Domänendefinition auf "Domäne :: Benutzer :: Entität :: Benutzer" zu setzen, aber es ist nicht "** Domänenbenutzer ", sondern " Domäne ist Benutzer **". Ich denke, dass es als Modifikation überflüssig ist. (Ich persönlich schätze das Gefühl, dass es gut auf Japanisch passt. Es sei denn, es gibt ausländische Mitglieder.)

Ich denke auch, dass es vorteilhaft ist, eine Benennung zu haben, die zeigt, dass das ActiveRecord-Modell dazu gehört. Daher ist es besser, die Benennung auf der ActiveRecord-Seite zu ändern, wenn dies möglich ist.

Übrigens habe ich im vorherigen Abschnitt geschrieben, dass "ActiveRecord direkt als Entität der Domäne verwendet werden kann", aber ActiveRecord selbst wird wie zuvor unter Modelle gestellt, wobei der Schwerpunkt auf der Auflistbarkeit liegt. (Zum Zeitpunkt des Schreibens denke ich, dass es besser ist, einen Thin Wrapper immer als Domain / Entity zu definieren ... aber ich werde beim Erstellen darüber nachdenken.)

Usecase kann über mehrere Domänenobjekte hinweg funktionieren

Genau. Ist es nicht okay?

Ich denke, Entität und Repository sollten innerhalb dieser Domäne abgeschlossen sein, aber ich denke nicht, dass es ein Problem mit Usecase gibt, das mit mehreren Domänen umgeht.

Wenn es als Speicherort für die Datei nicht richtig aussieht, können Sie eine andere Domäne ohne Entität oder Repository definieren und nur Usecase einfügen.

Es ist eine Schande, aber ich denke, es ist in Ordnung, das Entity-Modul derselben Domain in das Repository aufzunehmen. Aber Usecase ist nicht gut. (Wenn Sie verschiedene Repositorys im Konstruktor übergeben, gibt es meines Erachtens fast keine Szene, in der der Name der Änderung des Modulnamens angezeigt wird.)

Vereinfachung

Das Folgende ist kein CA-Lehrbuch, aber ich denke darüber nach, es zu tun, um die Entwicklung zu beschleunigen.

Die Entitätserfassung durch ID kann eine Modulfunktion des Domänenmoduls sein

So was

module Order extend self

  order_repository
    @order_repository ||= Order::Repository::OrderRepository.new
  end

  module_function
  def get_order_by_id(order_id)
    order_repository.get(order_id)
  end
end

Es ist eine Regel, dass Sie bei Bedarf eine Verknüpfung erstellen können, da Sie in bestimmten Situationen die Entitätserfassung verwenden möchten, indem Sie auch beim Debuggen eine ID angeben. (Es wird nicht immer für alle Entitäten erstellt.)

Wenn der Prozess, der "GET / XXXXs /: id" von Routen entspricht, rechtzeitig ist, ist es meines Erachtens nicht erforderlich, Usecase zu definieren, sondern in den meisten Fällen die Berechtigungsprüfung (im Fall der obigen Bestellung selbst) (Unsichtbar außer bei Bestellung) ist erforderlich, daher ist in diesem Fall Usecase erforderlich.

Definieren Sie usecase als Modulfunktion des Domänenmoduls

Im Allgemeinen verwendet Usecase verschiedene Repository als Argumente in seinem Konstruktor. Dies liegt daran, dass es schwierig ist, einen Test zu schreiben, wenn das Repository zum Zeitpunkt des Tests nicht ersetzt wird.

Es ist jedoch mühsam, das Repository jedes Mal anzugeben, wenn Sie Usecase von Controller verwenden. Erstellen Sie daher im Domänenmodul eine Verknüpfung dafür.

So was

module Order extend self

  order_repository
    @order_repository ||= Order::Repository::OrderRepository.new
  end

  module_function
  def create_order_usecase
    Order::Usecase::CreateOrder.new(
      order_repository
    )
  end
end

Die Benutzerseite

  val res = Order.create_order_usecase.run(...)

Der Inhalt, den Sie tun möchten, ist klar.

Ich denke, es ist in Ordnung, die Standardargumente auf der Usecase-Seite anzugeben, aber das ist Geschmackssache.

Prüfung

Um ehrlich zu sein, habe ich nicht viel Wissen über das Testen mit Rails und tappe. Auch in Bezug auf die Arbeitskräfte kann ich es mir nicht leisten, einen Test zu schreiben, daher denke ich, dass dies eine Form des Schreibens sein wird, teilweise dort, wo es benötigt wird.

Also im Wesentlichen,

Solange Sie einen Test mit dem Schwerpunkt ** schreiben können, können Sie später tatsächlich einen Test schreiben. Wenn das Repository ein einfacher Wrapper für ActiveRecord ist, sollte der Test die niedrigste Priorität haben. (Wenn es von einem externen Dienst abhängt, ist ein separater Test oder Mock erforderlich.)

Das Testen der Entität sollte kein Problem sein. Für Usecase kann das Schreiben eines Tests für einige Dinge problematisch sein, im Allgemeinen jedoch für Usecase

--validated --Validierung der Eingabeparameter --collect --sammelt die erforderliche Entität aus dem Repository --execute - Führt den Prozess aus, den Sie mit Usecase ausführen möchten

Es besteht oft aus 3 Schritten, und Sie möchten möglicherweise nur den Ausführungsteil testen. Ich denke, es wäre gut, wenn Sie nur den Ausführungsteil nach Bedarf testen könnten.

So was

class Domain1::Usecase::HogeHogeUsecase

  initialize(domain1_repository)
    @domain1_repository = domain1_repository
  end

  def run(params)
    error = validate(params)
    return ErrorCase.new(error) if error

    [v1, v2, error] = collect(params)
    return ErrorCase.new(error) if error

    return execute(v1, v2)
  end

  def validate(params)
    ...
  end

  def collect(params)
    ...
  end

  def execute(v1, v2)
    ...
  end
end

Sie sollten jetzt in der Lage sein, Tests zu schreiben, ohne sich um den Status der persistierten Daten sorgen zu müssen.

Zusammenfassung

Bisher habe ich zusammengefasst, woran ich in den frühen Entwicklungsstadien gedacht habe. Ist es nicht die größte Ernte, die die Organisation in mir gemacht hat, indem sie sie geschrieben hat?

Meine Rails-Leistung ist nicht hoch und ich denke, es gibt einige Vor- und Nachteile. Aber es spielt keine Rolle. Das Design vor Ort muss nicht von allen akzeptiert werden.

Ich denke jedoch, dass es ausreicht, wenn die Mitglieder des Teams die Idee teilen, dass dieses Projekt "mit dieser Art von Richtlinien erstellt" wird und die Konsistenz erhalten bleibt.

Die richtige Antwort zu suchen, ist eine Aufgabe im Bereich der Akademiker, und ich denke, wenn ein Ingenieur auf dem Gebiet danach fragt, bleibt er einfach stecken.

Es ist unmöglich, alles positiv zu nehmen, aber ich hoffe, es gibt etwas Hilfreiches.

Während des Herstellungsprozesses werde ich es erneut hinzufügen, wenn es etwas zu ändern gibt. (Da dieses Dokument genau das Dokument ist, um Teammitgliedern "Ich mache diese Richtlinie" mitzuteilen.)

Recommended Posts

Überlegungen zu Schienen und sauberer Architektur
Überlegungen zu Klassen und Instanzen
Über Schienen 6
Informationen zum Rails-Routing
Über Rails Controller
[Rails / Active Record] Über den Unterschied zwischen create und create!
Ein Memo über den Fluss von Rails und Vue
[Rails] Ich habe den Unterschied zwischen Ressourcen und Ressourcen untersucht
[Rails] Informationen zu Migrationsdateien
[Rails] Über aktiven Hash
Schienen gültig und ungültig?
Entwerfen und implementieren Sie ein Block Breaking-Spiel mit einer sauberen Architektur
Über Bean und DI
Über Klassen und Instanzen
Informationen zur Versionsspezifikation für Schienen
Entwerfen und implementieren Sie ein Block Breaking-Spiel mit einer sauberen Architektur
Über Weiterleiten und Weiterleiten
Informationen zu Serializable und serialVersionUID
[Schienen] Standardwerte festlegen
Ein Memorandum über Tabellendatentypen und -befehle (Rails)
Schienen Starke Parameter
[Anfänger] Über Rails Session
Über für Anweisung und wenn Anweisung
Über synchronisierte und Wiedereintrittssperre
Rails Posts und User Linkage
[Schienen] erfordern Methode und Genehmigungsmethode
Rails Tutorial Records und Memorandum # 0
Schienenpfad und URL-Methoden
Schienen sind schwierig und schmerzhaft!
Informationen zum Benennen von Rails-Modellmethoden
[Java] Über String und StringBuilder
Ungefähr der gleiche und der gleiche Wert
[Rails] Informationen zur Struktur des scss-Ordners
Schienen sind schwierig und schmerzhaft! Ⅱ
Über Klassen und Instanzen (Evolution)
Über die Zupfmethode und die ID-Methode
[Rails] Informationen zum Rspec-Antworttest
[Rails] strftime dies und das
Über Java-Paket und Import
Über Ruby und Objektmodell
Überlegungen zur Zeitmethode
Informationen zu Ruby-Klassen und -Instanzen
Rails-Webserver und Anwendungsserver
Über Instanzvariablen und attr_ *
Über Rails Scraping-Methode Mechanisieren
[Buchbesprechung] Saubere Architektur Struktur und Design der Software von Meistern gelernt