Ich hatte die Gelegenheit, über das Design von Rails-Anwendungen nachzudenken, und werde daher zusammenfassen, was ich dabei dachte.
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.
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.
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. )
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)
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.
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.)
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.)
Das Folgende ist kein CA-Lehrbuch, aber ich denke darüber nach, es zu tun, um die Entwicklung zu beschleunigen.
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.
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.
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.
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