Orthodoxes Backend



Das moderne Backend ist vielfältig, befolgt jedoch einige unausgesprochene Regeln. Viele von uns, die Serveranwendungen entwickeln, sind mit allgemein anerkannten Ansätzen konfrontiert, wie beispielsweise Clean Architecture, SOLID, Persistence Ignorance, Dependency Injection und anderen. Viele der Attribute der Serverentwicklung sind so abgedroschen, dass sie keine Fragen aufwerfen und gedankenlos verwendet werden. Sie reden viel über einige, benutzen sie aber nie. Die Bedeutung des Rests wird entweder falsch interpretiert oder verzerrt. Der Artikel befasst sich mit dem Aufbau einer einfachen, völlig typischen Backend-Architektur, die nicht nur den Grundsätzen berühmter Programmiertheoretiker entspricht, sondern diese auch in gewissem Maße verbessert.

Allen gewidmet, die nicht an Programmieren ohne Schönheit denken und Schönheit inmitten von Absurdität nicht akzeptieren.

Domänenmodell


In der Modellierung sollte die Softwareentwicklung in einer idealen Welt beginnen. Aber wir sind alle nicht perfekt, wir reden viel darüber, aber wir machen alles wie gewohnt. Oft liegt der Grund in der Unvollkommenheit vorhandener Werkzeuge. Und um ehrlich zu sein, unsere Faulheit und Angst, Verantwortung zu übernehmen, um von "Best Practices" wegzukommen. In einer unvollkommenen Welt beginnt die Softwareentwicklung bestenfalls mit dem Gerüstbau, und im schlimmsten Fall wird nichts mit der Leistungsoptimierung getan. Trotzdem möchte ich die harten Beispiele „herausragender“ Architekten verwerfen und über gewöhnlichere Dinge spekulieren.

Wir haben also eine technische Aufgabe und sogar ein Benutzeroberflächendesign (oder nicht, wenn die Benutzeroberfläche nicht bereitgestellt wird). Der nächste Schritt besteht darin, die Anforderungen im Domänenmodell widerzuspiegeln. Zu Beginn können Sie der Übersichtlichkeit halber ein Diagramm von Modellobjekten skizzieren:



Dann beginnen wir in der Regel, das Modell auf die Mittel seiner Implementierung zu projizieren - eine Programmiersprache, einen objektrelationalen Konverter (Object-Relational Mapper, ORM) oder auf ein komplexes Framework wie ASP.NET MVC oder Ruby on Rails, mit anderen Worten - Schreiben Sie Code. In diesem Fall folgen wir dem Pfad des Frameworks, der meiner Meinung nach im auf dem Modell basierenden Entwicklungsrahmen nicht korrekt ist, egal wie bequem es zunächst erscheinen mag. Hier machen Sie eine große Annahme, die anschließend die Vorteile der domänenbasierten Entwicklung zunichte macht. Als eine freiere Option, die nicht durch den Umfang eines Werkzeugs beschränkt ist, würde ich vorschlagen, nur syntaktische Werkzeuge einer Programmiersprache zum Erstellen eines Objektmodells eines Themenbereichs zu verwenden. In meiner Arbeit benutze ich verschiedene Programmiersprachen - C #, JavaScript, Ruby. Das Schicksal hat entschieden, dass die Ökosysteme Java und C # meine Inspiration sind, JS mein Haupteinkommen ist und Ruby die Sprache ist, die ich mag. Daher werde ich weiterhin einfache Beispiele in Ruby zeigen: Ich bin überzeugt, dass dies Entwicklern in anderen Sprachen keine Probleme bereiten wird, diese zu verstehen. Portieren Sie das Modell also in die Invoice-Klasse in Ruby:

class Invoice attr_reader :amount, :date, :created_at, :paid_at def initialize(attrs, payment_service) @created_at = DateTime.now @paid_at = nil @amount = attrs[:amount] @date = attrs[:date] @subscription = attrs[:subscription] @payment_service = payment_service end def pay credit_card = @subscription.customer.credit_card amount = @subscription.plan.price @payment_service.charge(credit_card, amount) @paid_at = DateTime.now end end 

Das heißt Wir haben eine Klasse, deren Konstruktor einen Hash von Attributen, Objektabhängigkeiten und Initialisierungen seiner Felder akzeptiert, und eine Bezahlmethode, die den Status des Objekts ändern kann. Alles ist sehr einfach. Jetzt überlegen wir nicht, wie und wo wir dieses Objekt anzeigen und speichern werden. Es existiert einfach, wir können es erstellen, seinen Zustand ändern, mit anderen Objekten interagieren. Bitte beachten Sie, dass der Code keine fremden Artefakte wie BaseEntity und anderen Müll enthält, der nicht mit dem Modell zusammenhängt. Es ist sehr wichtig. Übrigens können wir zu diesem Zeitpunkt bereits mit der Entwicklung durch Testen (TDD) beginnen, wobei Stub-Objekte anstelle von Abhängigkeiten wie payment_service verwendet werden:

 RSpec.describe Invoice do before :each do @payment_service = double(:payment_service) allow(@payment_service).to receive(:charge) @amount = 100 @credit_card = CreditCard.new({...}) @customer = Customer.new({credit_card: @credit_card, ...}) @subscription = Subscription.new({customer: customer, ...}) @invoice = Invoice.new({amount: @amount, date: DateTime.now, @subscription: subscription}, payment_service) end describe 'pay' do it "charges customer's credit card" do expect(@payment_service).to receive(:charge).with(@credit_card, @amount) @invoice.pay end it 'makes the invoice paid' do expect(@invoice.paid_at).not_to be_nil @invoice.pay end end end 

oder spielen Sie sogar mit dem Modell im Interpreter (irb für Ruby), bei dem es sich möglicherweise um die, wenn auch nicht sehr benutzerfreundliche, Benutzeroberfläche handelt:

 irb > invoice = Invoice.new({amount: @amount, date: DateTime.now, @subscription: subscription}, payment_service) irb > invoice.pay 

Warum ist es in dieser Phase so wichtig, „fremde Artefakte“ zu vermeiden? Tatsache ist, dass das Modell keine Ahnung haben sollte, wie es gespeichert wird oder ob es überhaupt gespeichert wird. Letztendlich kann für einige Systeme das Speichern von Objekten direkt im Speicher durchaus geeignet sein. Zum Zeitpunkt der Modellierung müssen wir dieses Detail vollständig abstrahieren. Dieser Ansatz wird als Persistenz-Ignoranz bezeichnet. Es sollte betont werden, dass wir die Probleme bei der Arbeit mit dem Repository nicht ignorieren, egal ob es sich um eine relationale oder eine andere Datenbank handelt. Wir vernachlässigen nur die Details der Interaktion mit ihr in der Modellierungsphase. Persistenz-Ignoranz bedeutet die absichtliche Eliminierung von Mechanismen für die Arbeit mit dem Status des Modells sowie aller Arten von Metadaten, die mit diesem Prozess zusammenhängen, aus dem Modell selbst. Beispiele:

 #  class User < Entity #     table :users #     # mapping  field :name, type: 'String' #   def save ... end end user = User.load(id) #     user.save #     

 #  class User #   ,      attr_accessor :name, :lastname end user = repo.load(id) #     repo.save(user) #     

Dieser Ansatz beruht auch auf fundamentalen Gründen - der Einhaltung des Grundsatzes der alleinigen Verantwortung (Prinzip der Einzelverantwortung, S in SOLID). Wenn das Modell zusätzlich zu seiner Funktionskomponente die Parameter für die Zustandserhaltung beschreibt und sich auch mit seiner Erhaltung und Belastung befasst, hat es offensichtlich zu viele Verantwortlichkeiten. Der resultierende und nicht der letzte Vorteil von Persistence Ignorance ist die Möglichkeit, das Speicherwerkzeug und sogar die Art des Speichers selbst während des Entwicklungsprozesses zu ersetzen.

Model-View-Controller


Das MVC-Konzept ist in der Entwicklungsumgebung verschiedener, nicht nur Server-, Anwendungen in verschiedenen Sprachen und Plattformen so beliebt, dass wir nicht mehr darüber nachdenken, was es ist und warum es überhaupt benötigt wird. Ich habe die meisten Fragen aus dieser Abkürzung heißt "Controller". Unter dem Gesichtspunkt der Organisation der Struktur des Codes ist es eine gute Sache, Aktionen auf dem Modell zu gruppieren. Der Controller sollte jedoch überhaupt keine Klasse sein, sondern vielmehr ein Modul, das Methoden für den Zugriff auf das Modell enthält. Sollte es überhaupt einen Ort geben, an dem man sein kann? Als Entwickler, der dem Pfad von .NET -> Ruby -> Node.js folgte, wurde ich einfach von den JS (ES5) -Controllern berührt, die im Rahmen von express.js implementiert werden. Mit der Fähigkeit, die den Controllern zugewiesene Aufgabe in einem funktionaleren Stil zu lösen, schreiben die Entwickler, wie verhext, den magischen „Controller“ immer wieder. Warum ist ein typischer Controller schlecht?

Ein typischer Controller ist eine Reihe von Methoden, die nicht eng miteinander verwandt sind, sondern nur durch eine einzige Methode vereint werden - eine bestimmte Essenz des Modells. und manchmal nicht nur eine, schlimmer. Jede einzelne Methode kann unterschiedliche Abhängigkeiten erfordern. Mit Blick auf die Zukunft stelle ich fest, dass ich die Praxis der Abhängigkeitsinversion (Abhängigkeitsinversion, D in SOLID) unterstütze. Daher muss ich diese Abhängigkeiten irgendwo außerhalb initialisieren und an den Controller-Konstruktor übergeben. Wenn ich zum Beispiel ein neues Konto erstelle, muss ich Benachrichtigungen an den Buchhalter senden, für den ich einen Benachrichtigungsdienst benötige. Bei anderen Methoden ist dies nicht erforderlich:

 class InvoiceController def initialize(invoice_repository, notification_service) @repository = invoice_repository @notification_service = notification_service end def index @repository.get_all end def show(id) @repository.get_by_id(id) end def create(data) @repository.create(data) @notification_service.notify_accountant end end 

Hier bittet die Idee darum, Methoden für die Arbeit mit dem Modell in separate Klassen zu unterteilen, und warum nicht?

 class ListInvoices def initialize(invoice_repository) @repository = invoice_repository end def call @repository.get_all end end class CreateInvoice def initialize(invoice_repository, notification_service) @repository = invoice_repository @notification_service = notification_service end def call @repository.create(data) @notification_service.notify_accountant end end 

Nun, anstelle des Controllers gibt es jetzt eine Reihe von „Funktionen“ für den Zugriff auf das Modell, die übrigens auch beispielsweise über Dateisystemverzeichnisse strukturiert werden können. Jetzt müssen Sie diese Methoden nach außen "öffnen", d. H. organisiere so etwas wie einen Router. Als Person, die mit allen Arten von DSL (Domain-Specific Language) versucht ist, würde ich eine visuellere Beschreibung der Anweisungen für eine Webanwendung bevorzugen als Tricks in Ruby oder einer anderen Allzwecksprache zum Festlegen von Routen:

 `HTTP GET /invoices -> return all invoices` `HTTP POST /invoices -> create new invoice` 

oder zumindest

 `HTTP GET /invoices -> ./invoices/list_invoices` `HTTP POST /invoices -> ./invoices/create` 

Dies ist einem typischen Router sehr ähnlich, mit dem einzigen Unterschied, dass er nicht mit den Controllern, sondern direkt mit den Aktionen im Modell interagiert. Es ist klar, dass wir uns um die Serialisierung und Deserialisierung von Objekten und vielem mehr kümmern müssen, wenn wir JSON senden und empfangen möchten. Auf die eine oder andere Weise können wir die Controller loswerden, einen Teil ihrer Verantwortung auf die Verzeichnisstruktur und den fortgeschritteneren Router verlagern.

Abhängigkeitsinjektion


Ich habe absichtlich einen "fortgeschritteneren Router" geschrieben. Damit der Router mithilfe des Abhängigkeitsinjektionsmechanismus auf deklarativer Ebene wirklich den Ablauf von Aktionen auf dem Modell zulassen kann, sollte er im Inneren wahrscheinlich recht komplex sein. Das allgemeine Schema seiner Arbeit sollte ungefähr so ​​aussehen:



Wie Sie sehen können, ist mein gesamter Router mit Abhängigkeitsinjektion unter Verwendung eines IoC-Containers durchsetzt. Warum ist das überhaupt nötig? Das Konzept der „Abhängigkeitsinjektion“ geht auf die Abhängigkeitsinversionstechnik zurück, mit der die Konnektivität von Objekten reduziert werden soll, indem die Abhängigkeitsinitialisierung aus dem Anwendungsbereich verschoben wird. Ein Beispiel:

 class Repository; end #  (   ) class A def initialize @repo = Repository.new end end #  (   ) class A def initialize(repo) @repo = repo end end 

Dieser Ansatz hilft denjenigen, die Test-Driven Development verwenden, sehr. Im obigen Beispiel können wir einfach einen Stub im Konstruktor anstelle des realen Repository-Objekts platzieren, das seiner Schnittstelle entspricht, ohne das Objektmodell zu „hacken“. Dies ist nicht der einzige DI-Bonus: Bei korrekter Anwendung bringt dieser Ansatz viel angenehme Magie in Ihre Anwendung, aber das Wichtigste zuerst. Die Abhängigkeitsinjektion ist ein Ansatz, mit dem Sie die Abhängigkeitsinversionstechnik in eine vollständige Architekturlösung integrieren können. Das Implementierungstool ist normalerweise ein IoC- (Inversion of Control) -Container. Es gibt Unmengen von wirklich coolen IoC-Containern in der Java- und .NET-Welt, davon gibt es Dutzende. In JS und Ruby gibt es leider keine geeigneten Optionen für mich. Insbesondere habe ich mir Trockenbehälter ( Trockenbehälter ) angesehen. So würde meine Klasse aussehen:

 class Invoice include Import['payment_service'] def pay credit_card = @subscription.customer.credit_card amount = @subscription.plan.price @payment_service.charge(credit_card, amount) end end 

Anstelle der schlanken Verwendung des Konstruktors belasten wir die Klasse durch die Einführung eigener Abhängigkeiten, die uns im Anfangsstadium von einem sauberen und unabhängigen Modell abbringen. Nun, etwas, und das Modell sollte überhaupt nichts über IoC wissen! Dies gilt für Aktionen wie CreateInvoice. Für den gegebenen Fall bin ich in meinen Tests bereits verpflichtet, IoC als etwas Unveräußerliches zu verwenden. Das ist völlig falsch. Anwendungsobjekte sollten größtenteils nicht über die Existenz von IoC informiert sein. Nachdem ich viel gesucht und nachgedacht hatte, skizzierte ich mein IoC , was nicht so aufdringlich wäre.

Modell speichern und laden


Persistenz Ignoranz erfordert einen unauffälligen Objekttransformator. In diesem Artikel meine ich die Arbeit mit einer relationalen Datenbank. Die wichtigsten Punkte gelten für andere Arten von Speichern. Ein objektrelationaler Konverter - ORM (Object Relational Mapper) - wird als ähnlicher Konverter für relationale Datenbanken verwendet. In der Welt von .NET und Java gibt es eine Fülle wirklich leistungsfähiger ORM-Tools. Alle von ihnen haben einige oder andere kleinere Mängel, vor denen Sie Ihre Augen schließen können. In JS und Ruby gibt es keine guten Lösungen. Alle binden das Modell auf die eine oder andere Weise starr an das Framework und zwingen sie, fremde Elemente zu deklarieren, ganz zu schweigen von der Unanwendbarkeit von Persistence Ignorance. Wie im Fall von IoC dachte ich darüber nach, ORM selbst zu implementieren. Dies ist der Stand der Dinge in Ruby. Ich habe nicht alles von Grund auf neu gemacht, sondern eine einfache ORM-Fortsetzung zugrunde gelegt, die unauffällige Tools für die Arbeit mit verschiedenen relationalen DBMS bietet. Zunächst war ich an der Möglichkeit interessiert, Abfragen in Form von regulärem SQL auszuführen und am Ausgang ein Array von Strings (Hash-Objekten) zu erhalten. Es blieb nur, Ihren Mapper zu implementieren und Persistence Ignorance bereitzustellen. Wie bereits erwähnt, möchte ich keine Zuordnungsfelder in das Domänenmodell mischen. Daher implementiere ich Mapper so, dass eine separate Konfigurationsdatei im Typformat verwendet wird:

 entity Invoice do field :amount field :date field :start_date field :end_date field :created_at field :updated_at reference :user, type: User reference :subscription, type: Subscription end 

Persistenz Ignoranz ist recht einfach mit einem externen Objekt vom Typ Repository zu implementieren:

 repository.save(user) 

Wir werden jedoch noch weiter gehen und das Muster der Arbeitseinheit implementieren. Dazu müssen Sie das Konzept einer Sitzung hervorheben. Eine Sitzung ist ein Objekt, das im Laufe der Zeit existiert und in dem eine Reihe von Aktionen für das Modell ausgeführt werden, bei denen es sich um eine einzelne logische Operation handelt. Im Verlauf einer Sitzung können Modellobjekte geladen und geändert werden. Am Ende der Sitzung wird der Transaktionsstatus des Modells gespeichert.
Beispiel für eine Arbeitseinheit:

 user = session.load(User, id: 1) plan = session.load(Plan, id: 1) subscription = Subscription.new(user, plan) session.attach(subscription) invoice = Invoice.new(subscription) session.attach(invoice) # ... # -       if Date.today.yday == 1 subscription.comment = 'New year offer' invoice.amount /= 2 end session.flush 

Infolgedessen werden 2 Anweisungen in der Datenbank anstelle von 4 ausgeführt, und beide werden innerhalb derselben Transaktion ausgeführt.

Und dann erinnere dich plötzlich an die Repositories! Hier gibt es ein Gefühl von Deja Vu, wie bei den Controllern: Ist das Repository nicht dieselbe rudimentäre Einheit? Mit Blick auf die Zukunft werde ich antworten - ja, das ist es. Der Hauptzweck des Repositorys besteht darin, die Ebene der Geschäftslogik vor der Interaktion mit dem realen Speicher zu schützen. Im Kontext relationaler Datenbanken bedeutet dies beispielsweise, dass SQL-Abfragen direkt in den Geschäftslogikcode geschrieben werden. Dies ist zweifellos eine sehr vernünftige Entscheidung. Aber zurück zu dem Moment, als wir den Controller losgeworden sind. Aus Sicht von OOP ist das Repository im Wesentlichen derselbe Controller - die gleichen Methoden, nicht nur für die Verarbeitung von Anforderungen, sondern auch für die Arbeit mit dem Repository. Das Repository kann auch in Aktionen unterteilt werden. Bei allen Angaben unterscheiden sich diese Aktionen in keiner Weise von dem, was wir anstelle des Controllers vorgeschlagen haben. Das heißt, wir können Repository und Controller zugunsten einer einzigen einheitlichen Aktion ablehnen!

 class LoadPlan def initialize(session) @session = session end def call sql = <<~SQL SELECT p.* AS ENTITY plan FROM plans p WHERE p.id = 1 SQL @session.fetch(Plan, sql) end end 

Sie haben wahrscheinlich bemerkt, dass ich SQL anstelle einer Art Objektsyntax verwende. Das ist Geschmackssache. Ich bevorzuge SQL, weil es eine Abfragesprache ist, eine Art DSL für die Arbeit mit Daten. Es ist klar, dass es immer einfacher ist, Plan.load (id) zu schreiben als das entsprechende SQL, aber dies ist für triviale Fälle. Wenn es um etwas komplexere Dinge geht, wird SQL zu einem sehr willkommenen Werkzeug. Manchmal verflucht man ein anderes ORM, wenn man versucht, es wie reines SQL zu machen. "Ich würde in ein paar Minuten schreiben." Für diejenigen, die Zweifel haben, empfehle ich einen Blick in die MongoDB-Dokumentation , in der die Erklärungen in einer SQL-ähnlichen Form gegeben werden, die sehr lustig aussieht! Daher ist die Schnittstelle für Abfragen in ORM JetSet , die ich für meine Zwecke geschrieben habe, SQL mit minimalen Imprägnierungen wie „AS ENTITY“. Übrigens verwende ich in den meisten Fällen keine Modellobjekte, verschiedene DTOs usw. zum Anzeigen von Tabellendaten - ich schreibe einfach eine SQL-Abfrage, erhalte ein Array von Hash-Objekten und zeige sie in der Ansicht an. Auf die eine oder andere Weise schaffen es nur wenige Menschen, Big Data zu „scrollen“, indem sie verwandte Tabellen auf ein Modell projizieren. In der Praxis wird eher eine flache Projektion (Ansicht) verwendet, und sehr ausgereifte Produkte erreichen die Optimierungsphase, wenn komplexere Lösungen wie CQRS (Command and Query Responsibility Segregation) verwendet werden.

Alles zusammenfügen


Also, was wir haben:

  • Wir haben herausgefunden, wie das Modell geladen und gespeichert werden kann. Außerdem haben wir eine grobe Architektur des Web Delivery-Tools des Modells, eines bestimmten Routers, entworfen.
  • Wir kamen zu dem Schluss, dass alle Logik, die nicht Teil des Themenbereichs ist, in Actions (Actions) anstelle von Controllern und Repositories aufgenommen werden kann.
  • Aktionen müssen die Abhängigkeitsinjektion unterstützen
  • anständiges Tool Dependency Injection implementiert;
  • Das notwendige ORM ist implementiert.

Jetzt müssen Sie nur noch denselben "Router" implementieren. Da wir Repositorys und Controller zugunsten von Aktionen entfernt haben, ist es offensichtlich, dass wir für eine Anforderung mehrere Aktionen ausführen müssen. Handlungen sind autonom und wir können nicht ineinander investieren. Aus diesem Grund habe ich als Teil des Dandy-Frameworks einen Router implementiert, mit dem Sie Aktionsketten erstellen können. Konfigurationsbeispiel (auf / pläne achten):

 :receive .-> :before -> common/open_db_session GET -> welcome -> :respond <- show_welcome /auth -> :before -> current_user@users/load_current_user /profile -> GET -> plan@plans/load_plan \ -> :respond <- users/show_user_profile PATCH -> users/update_profile /plans -> GET -> current_plan@plans/load_current_plan \ -> plans@plans/load_plans \ -> :respond <- plans/list :catch -> common/handle_errors 

"GET / auth / plans" zeigt alle verfügbaren Abonnements an und "markiert" das aktuelle Abonnement. Folgendes passiert:

  1. ": before -> common / open_db_session" - Öffnen einer JetSet-Sitzung
  2. / auth ": before -> current_user @ users / load_current_user" - Lädt den aktuellen Benutzer (per Token). Das Ergebnis wird im IoC-Container als current_user (current_user @ Anweisung) protokolliert.
  3. / auth / plans "current_plan @ plans / load_current_plan" - Lädt den aktuellen Plan. Dazu wird der Wert @current_user aus dem Container übernommen. Das Ergebnis wird im IoC-Container als current_plan (current_plan @ Anweisung) aufgezeichnet:

     class LoadCurrentPlan def initialize(current_user, session) @current_user = current_user @session = session end def call sql = <<~SQL SELECT p.* AS ENTITY plan FROM plans p INNER JOIN subscriptions s ON s.user_id = :user_id AND s.current = 't' WHERE p.id = :user_id LIMIT 1 SQL @session.execute(sql, user_id: @current_user.id) do |row| map(Plan, row, 'plan') end end end 

  4. "Plans @ plans / load_plans" - Lädt eine Liste aller verfügbaren Pläne. Das Ergebnis wird im IoC-Container als Pläne (plans @ instruction) registriert.
  5. ": Antwort <- Pläne / Liste" - registrierter ViewBuilder, zum Beispiel JBuilder, zeichnet View 'Pläne / Liste' vom Typ:

     json.plans @plans do |plan| json.id plan.id json.name plan.name json.price plan.price json.active plan.id == @current_plan.id end 


Als @plans und @current_plan werden die in den vorherigen Schritten registrierten Werte aus dem Container abgerufen. Im Action-Konstruktor können Sie im Allgemeinen alles "bestellen", was Sie benötigen, oder vielmehr alles, was im Container registriert ist. Ein aufmerksamer Leser wird höchstwahrscheinlich eine Frage haben, aber gibt es eine Isolierung solcher Variablen im Mehrbenutzermodus? Ja, das tut es. Tatsache ist, dass der Hypo IoC-Container die Lebensdauer von Objekten festlegen und darüber hinaus an die Lebensdauer anderer Objekte binden kann. In Dandy sind Variablen wie @plans, @current_plan, @current_user an das Anforderungsobjekt gebunden und werden sofort nach Abschluss der Anforderung zerstört. Übrigens ist die JetSet-Sitzung auch an die Anforderung gebunden - ein Zurücksetzen ihres Status wird auch durchgeführt, wenn die Dandy-Anforderung abgeschlossen ist. Das heißt Jede Anfrage hat einen eigenen isolierten Kontext. Hypo beherrscht Dandys gesamten Lebenszyklus, egal wie viel Spaß dieses Wortspiel bei der wörtlichen Übersetzung der Namen machte.

Schlussfolgerungen


Im Rahmen der gegebenen Architektur benutze ich das Objektmodell, um den Themenbereich zu beschreiben; Ich verwende geeignete Methoden wie Abhängigkeitsinjektion. Ich kann sogar Vererbung verwenden. Gleichzeitig sind alle diese Aktionen im Wesentlichen gewöhnliche Funktionen, die auf deklarativer Ebene miteinander verkettet werden können. Wir haben das gewünschte Backend in einem funktionalen Stil erhalten, aber mit allen Vorteilen des Objektansatzes, wenn Sie keine Probleme mit Abstraktionen haben und Ihren Code testen. Am Beispiel des DSL-Routers Dandy können wir die erforderlichen Sprachen zur Beschreibung von Routen und mehr erstellen.

Fazit


Als Teil dieses Artikels führte ich eine Art Exkursion zu den grundlegenden Aspekten der Erstellung eines Backends durch, wie ich es sehe. Ich wiederhole, der Artikel ist oberflächlich, er hat nicht viele wichtige Themen angesprochen, wie zum Beispiel die Leistungsoptimierung. Ich habe versucht, mich nur auf die Dinge zu konzentrieren, die für die Community als Denkanstoß wirklich nützlich sein können, und nicht noch einmal von leer zu leer zu schütten, was SOLID, TDD ist, wie das MVC-Schema aussieht und so weiter. Strenge Definitionen dieser und anderer Begriffe, die von neugierigen Lesern verwendet werden, sind in dem riesigen Netzwerk leicht zu finden, ganz zu schweigen von den Kollegen im Geschäft, für die diese Abkürzungen Teil der Alltagssprache sind. Und schließlich, betone ich, versuche ich mich nicht auf die Werkzeuge zu konzentrieren, die ich implementieren musste, um die aufgeworfenen Probleme zu lösen. , . - , .

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


All Articles