Wie genau ist "Geschäftslogik sollte in einem Service sein, nicht in einem Modell"?

397

Lage

Ich habe heute Abend eine Antwort auf eine Frage zu StackOverflow gegeben.

Die Frage:

Die Bearbeitung eines vorhandenen Objekts sollte in der Repository-Ebene oder im Service erfolgen.

Zum Beispiel, wenn ich einen Benutzer habe, der Schulden hat. Ich möchte seine Schulden ändern. Soll ich es im UserRepository oder im Service zum Beispiel BuyingService tun, indem ich ein Objekt abrufe, es bearbeite und speichere?

Meine Antwort:

Sie sollten die Verantwortung für die Mutation eines Objekts in dasselbe Objekt übernehmen und das Repository zum Abrufen dieses Objekts verwenden.

Beispielsituation:

class User {
    private int debt; // debt in cents
    private string name;

    // getters

    public void makePayment(int cents){
        debt -= cents;
    }
}

class UserRepository {
    public User GetUserByName(string name){
        // Get appropriate user from database
    }
}

Ein Kommentar, den ich erhalten habe:

Geschäftslogik sollte wirklich in einem Service sein. Nicht in einem Modell.

Was sagt das Internet?

Das hat mich auf die Suche gebracht, da ich nie wirklich (bewusst) eine Service-Schicht verwendet habe. Ich habe angefangen, das Service-Layer-Muster und das Muster der Arbeitseinheit zu lesen, kann aber noch nicht sagen, dass ich davon überzeugt bin, dass ein Service-Layer verwendet werden muss.

Nehmen Sie zum Beispiel diesen Artikel von Martin Fowler über das Antimuster eines anämischen Domänenmodells:

Es gibt Objekte, von denen viele nach den Substantiven im Domänenbereich benannt sind, und diese Objekte sind mit den reichen Beziehungen und Strukturen verbunden, über die echte Domänenmodelle verfügen. Der Haken kommt, wenn man sich das Verhalten ansieht und merkt, dass es an diesen Objekten kaum ein Verhalten gibt, so dass sie kaum mehr sind als Säcke voller Getter und Setter. Tatsächlich enthalten diese Modelle häufig Entwurfsregeln, die besagen, dass den Domänenobjekten keine Domänenlogik hinzugefügt werden soll. Stattdessen gibt es eine Reihe von Dienstobjekten, die die gesamte Domänenlogik erfassen. Diese Dienste stehen über dem Domänenmodell und verwenden das Domänenmodell für Daten.

(...) Die Logik, die sich in einem Domänenobjekt befinden sollte, ist Domänenlogik - Validierungen, Berechnungen, Geschäftsregeln - wie auch immer Sie es nennen möchten.

Für mich schien das genau das zu sein, worum es in der Situation ging: Ich befürwortete die Manipulation der Daten eines Objekts, indem ich Methoden innerhalb dieser Klasse einführte, die genau das tun. Mir ist jedoch klar, dass dies so oder so gegeben sein sollte, und es hat wahrscheinlich mehr damit zu tun, wie diese Methoden aufgerufen werden (unter Verwendung eines Repositorys).

Ich hatte auch das Gefühl, dass in diesem Artikel (siehe unten) eine Serviceebene eher als Fassade betrachtet wird , die die Arbeit an das zugrunde liegende Modell delegiert, als als eine tatsächliche arbeitsintensive Ebene.

Application Layer [sein Name für Service Layer]: Definiert die Aufgaben, die die Software ausführen soll, und weist die ausdrucksstarken Domänenobjekte an, Probleme zu lösen. Die Aufgaben, für die diese Schicht zuständig ist, sind für das Unternehmen von Bedeutung oder für die Interaktion mit den Anwendungsschichten anderer Systeme erforderlich. Diese Schicht wird dünn gehalten. Es enthält keine Geschäftsregeln oder Kenntnisse, sondern koordiniert nur Aufgaben und delegiert die Arbeit an Kollaborationen von Domänenobjekten in der nächsten Ebene. Es gibt keinen Status, der die Geschäftssituation widerspiegelt, aber es gibt einen Status, der den Fortschritt einer Aufgabe für den Benutzer oder das Programm widerspiegelt.

Welche verstärkt hier :

Service-Schnittstellen. Dienste stellen eine Dienstschnittstelle bereit, an die alle eingehenden Nachrichten gesendet werden. Sie können sich eine Serviceschnittstelle als Fassade vorstellen, die die in der Anwendung implementierte Geschäftslogik (normalerweise die Logik in der Geschäftsschicht) potenziellen Verbrauchern zugänglich macht.

Und hier :

Die Serviceschicht sollte keine Anwendung oder Geschäftslogik enthalten und sich in erster Linie auf einige wenige Aspekte konzentrieren. Es sollte Business Layer-Aufrufe einschließen, Ihre Domain in eine gemeinsame Sprache übersetzen, die Ihre Kunden verstehen können, und das Kommunikationsmedium zwischen Server und anforderndem Client handhaben.

Dies ist ein schwerwiegender Gegensatz zu anderen Ressourcen , die sich mit der Service-Schicht befassen:

Die Serviceschicht sollte aus Klassen mit Methoden bestehen, die Arbeitseinheiten mit Aktionen sind, die zu derselben Transaktion gehören.

Oder die zweite Antwort auf eine Frage, die ich bereits verlinkt habe:

Irgendwann möchte Ihre Anwendung eine Geschäftslogik. Möglicherweise möchten Sie die Eingabe auch validieren, um sicherzustellen, dass keine bösen oder fehlerhaften Daten angefordert werden. Diese Logik gehört in Ihre Service-Schicht.

"Lösung"?

Nach den Richtlinien in dieser Antwort habe ich den folgenden Ansatz gefunden, bei dem eine Service-Schicht verwendet wird:

class UserController : Controller {
    private UserService _userService;

    public UserController(UserService userService){
        _userService = userService;
    } 

    public ActionResult MakeHimPay(string username, int amount) {
        _userService.MakeHimPay(username, amount);
        return RedirectToAction("ShowUserOverview");
    }

    public ActionResult ShowUserOverview() {
        return View();
    }
}

class UserService {
    private IUserRepository _userRepository;

    public UserService(IUserRepository userRepository) {
        _userRepository = userRepository;
    }

    public void MakeHimPay(username, amount) {
        _userRepository.GetUserByName(username).makePayment(amount);
    }
}

class UserRepository {
    public User GetUserByName(string name){
        // Get appropriate user from database
    }
}

class User {
    private int debt; // debt in cents
    private string name;

    // getters

    public void makePayment(int cents){
        debt -= cents;
    }
}

Fazit

Insgesamt hat sich hier nicht viel geändert: Der Code vom Controller ist in die Service-Schicht übergegangen (was eine gute Sache ist, also gibt es einen Vorteil bei diesem Ansatz). Dies scheint jedoch nichts mit meiner ursprünglichen Antwort zu tun zu haben.

Mir ist klar, dass Entwurfsmuster Richtlinien sind und keine in Stein gemeißelten Regeln, die wann immer möglich umgesetzt werden sollen. Dennoch habe ich keine definitive Erklärung für die Serviceschicht gefunden und wie sie zu betrachten ist.

  • Ist es ein Mittel, einfach die Logik aus dem Controller zu extrahieren und stattdessen in einen Service zu integrieren?

  • Soll es einen Vertrag zwischen dem Controller und der Domain bilden?

  • Sollte es eine Schicht zwischen der Domain und der Service-Schicht geben?

Und zu guter Letzt: Nach dem Originalkommentar

Geschäftslogik sollte wirklich in einem Service sein. Nicht in einem Modell.

  • Ist das richtig?

    • Wie würde ich meine Geschäftslogik in einem Service anstelle des Modells einführen?
Jeroen Vannevel
quelle
6
Ich betrachte die Serviceebene als den Ort, an dem der unvermeidliche Teil des Transaktionsskripts auf aggregierte Wurzeln angewendet wird. Wenn meine Serviceschicht zu komplex wird, signalisiert dies, dass ich mich in Richtung Anämisches Modell bewege und mein Domänenmodell Aufmerksamkeit und Überprüfung benötigt. Ich versuche auch, die Logik in SL zu setzen, die von Natur aus nicht dupliziert wird.
Pavel Voronin
Ich dachte, dass Services Teil der Model-Ebene sind. Irre ich mich, wenn ich das denke?
Florian Margaine
Ich wende eine Faustregel an: Hänge nicht von dem ab, was du nicht brauchst. Andernfalls kann ich (normalerweise negativ) von Änderungen an dem Teil betroffen sein, den ich nicht benötige. Infolgedessen habe ich viele klar definierte Rollen. Meine Datenobjekte enthalten Verhalten, solange alle Clients es verwenden. Ansonsten verschiebe ich das Verhalten in Klassen, die die erforderliche Rolle implementieren.
Beluchin
1
Die Steuerungslogik für den Anwendungsfluss gehört zu einer Steuerung. Datenzugriffslogik gehört in ein Repository. Die Validierungslogik gehört in eine Serviceschicht. Eine Service-Schicht ist eine zusätzliche Schicht in einer ASP.NET MVC-Anwendung, die die Kommunikation zwischen einem Controller und einer Repository-Schicht vermittelt. Die Serviceschicht enthält eine Geschäftsüberprüfungslogik. Repository. asp.net/mvc/overview/older-versions-1/models-data/…
Kbdavis07
3
Meiner Meinung nach sollte ein korrektes Domänenmodell im OOP-Stil tatsächlich "Geschäftsdienste" vermeiden und die Geschäftslogik im Modell beibehalten. Die Praxis zeigt jedoch, dass es so verlockend ist, eine riesige Business-Schicht mit eindeutig benannten Methoden zu erstellen und die gesamte Methode mit einer Transaktion (oder Arbeitseinheit) zu versehen. Es ist viel einfacher, mehrere Modellklassen in einer Business-Service-Methode zu verarbeiten, als die Beziehungen zwischen diesen Modellklassen zu planen, da Sie dann einige schwierige Fragen zu beantworten haben: Wo befindet sich der aggregierte Stamm? Wo soll ich eine Datenbanktransaktion starten / festschreiben? Etc.
JustAMartin

Antworten:

368

Um die Zuständigkeiten eines Dienstes zu definieren , müssen Sie zunächst definieren, was ein Dienst ist.

Service ist kein kanonischer oder allgemeiner Softwarebegriff. Tatsächlich Serviceähnelt das Suffix eines Klassennamens dem vielfach bösartigen Manager : Es sagt fast nichts darüber aus, was das Objekt tatsächlich tut .

In Wirklichkeit sollte ein Service sehr architekturspezifisch sein:

  1. In einer traditionellen Schichtarchitektur ist Service buchstäblich gleichbedeutend mit Business Logic Layer . Es ist die Ebene zwischen Benutzeroberfläche und Daten. Daher werden alle Geschäftsregeln in Dienste einbezogen. Die Datenschicht sollte nur grundlegende CRUD-Operationen verstehen, und die UI-Schicht sollte sich nur mit der Zuordnung von Präsentations-DTOs zu und von den Geschäftsobjekten befassen.

  2. In einer verteilten RPC-Architektur (SOAP, UDDI, BPEL usw.) ist der Dienst die logische Version eines physischen Endpunkts . Es handelt sich im Wesentlichen um eine Sammlung von Vorgängen, die der Betreuer als öffentliche API bereitstellen möchte. In verschiedenen Leitfäden für bewährte Verfahren wird erläutert, dass ein Service- Vorgang tatsächlich ein Vorgang auf Unternehmensebene und nicht CRUD sein sollte, und ich stimme dem eher zu.

    Da jedoch das Routing alles kann ernsthaft Leistung durch einen tatsächlichen Remote - Service verletzt, ist es normalerweise am besten nicht um diese Dienste zu haben tatsächlich die Business - Logik selbst umzusetzen; Stattdessen sollten sie eine "interne" Gruppe von Geschäftsobjekten umschließen. Ein einzelner Service kann ein oder mehrere Geschäftsobjekte umfassen.

  3. In einer MVP / MVC / MVVM / MV * -Architektur sind überhaupt keine Dienste vorhanden. In diesem Fall bezieht sich der Begriff auf ein beliebiges generisches Objekt, das in einen Controller oder ein Ansichtsmodell eingefügt werden kann. Die Geschäftslogik ist in Ihrem Modell . Wenn Sie "Serviceobjekte" erstellen möchten, um komplizierte Vorgänge zu orchestrieren, wird dies als Implementierungsdetail angesehen. Leider implementieren viele Leute MVC wie folgt, aber es wird als Antimuster ( anämisches Domänenmodell ) angesehen, da das Modell selbst nichts tut, sondern nur eine Reihe von Eigenschaften für die Benutzeroberfläche.

    Einige Leute denken fälschlicherweise, dass die Verwendung einer 100-Zeilen-Controller-Methode und das Einfügen all dieser Methoden in einen Dienst auf irgendeine Weise zu einer besseren Architektur führen. Das tut es wirklich nicht. Alles, was es tut, ist, eine weitere, wahrscheinlich unnötige Indirektionsebene hinzuzufügen. In der Praxis erledigt der Controller die Arbeit immer noch, nur über ein schlecht benanntes "Helfer" -Objekt. Ich kann Jimmy Bogards Präsentation " Wicked Domain Models" nur wärmstens empfehlen, um ein anschauliches Beispiel für die Umwandlung eines anämischen Domänenmodells in ein nützliches zu geben. Dabei müssen Sie sorgfältig prüfen, welche Modelle Sie verfügbar machen und welche Vorgänge im Geschäftskontext tatsächlich gültig sind .

    Wenn Ihre Datenbank beispielsweise Aufträge enthält und Sie eine Spalte für den Gesamtbetrag haben, sollte es Ihrer Anwendung wahrscheinlich nicht gestattet sein, dieses Feld tatsächlich in einen beliebigen Wert zu ändern, da (a) es sich um den Verlauf handelt und (b) dies der Fall sein soll bestimmt durch das, was ist in der Reihenfolge sowie vielleicht einige andere zeitkritische Daten / Regeln. Das Erstellen eines Dienstes zum Verwalten von Bestellungen löst dieses Problem nicht unbedingt, da der Benutzercode weiterhin das tatsächliche Bestellobjekt erfassen und den Betrag darauf ändern kann. Stattdessen sollte die Bestellung selbst dafür verantwortlich sein, dass sie nur auf sichere und konsistente Weise geändert werden kann.

  4. In DDD sind Dienste speziell für den Fall gedacht, dass Sie über einen Vorgang verfügen, der nicht ordnungsgemäß zu einem aggregierten Stamm gehört . Hier muss man vorsichtig sein, denn oft kann die Notwendigkeit eines Dienstes bedeuten, dass man nicht die richtigen Wurzeln verwendet hat. Angenommen, Sie verwenden einen Service, um Vorgänge über mehrere Roots hinweg zu koordinieren oder um Probleme zu lösen, die das Domänenmodell überhaupt nicht betreffen (z. B. das Schreiben von Informationen in eine BI / OLAP-Datenbank).

    Ein bemerkenswerter Aspekt des DDD-Dienstes ist, dass er Transaktionsskripten verwenden darf . Wenn Sie an großen Anwendungen arbeiten, werden Sie wahrscheinlich auf Instanzen stoßen, in denen es einfacher ist, mit einer T-SQL- oder PL / SQL-Prozedur etwas zu erreichen, als mit dem Domänenmodell zu tun zu haben. Dies ist in Ordnung und gehört zu einem Dienst.

    Dies ist eine radikale Abkehr von der Definition von Diensten in Schichtenarchitektur. Eine Service-Schicht kapselt Domänenobjekte. Ein DDD-Dienst kapselt alles, was nicht in den Domänenobjekten enthalten ist, und macht keinen Sinn.

  5. In einer serviceorientierten Architektur wird ein Service als technische Autorität für eine Geschäftsfähigkeit betrachtet. Das heißt, es ist der ausschließliche Eigentümer einer bestimmten Teilmenge der Geschäftsdaten und nichts anderes darf diese Daten berühren - nicht einmal, um sie nur zu lesen .

    Services sind notwendigerweise ein End-to-End-Angebot in einer SOA. Das heißt, ein Dienst ist weniger eine bestimmte Komponente als ein ganzer Stapel , und Ihre gesamte Anwendung (oder Ihr ganzes Unternehmen) besteht aus einer Reihe dieser Dienste, die nebeneinander ausgeführt werden und sich nur auf der Messaging- und der UI-Ebene überschneiden. Jeder Service verfügt über eigene Daten, eigene Geschäftsregeln und eine eigene Benutzeroberfläche. Sie müssen nicht miteinander orchestrieren, weil sie auf das Geschäft ausgerichtet sein sollen - und wie das Geschäft selbst hat jeder Dienst seine eigenen Verantwortlichkeiten und arbeitet mehr oder weniger unabhängig von den anderen.

    Nach der SOA-Definition ist also jede Geschäftslogik irgendwo im Service enthalten, aber auch das gesamte System . Dienste in einer SOA können Komponenten und Endpunkte enthalten. Es ist jedoch ziemlich gefährlich, einen Code als Dienst zu bezeichnen, da dies in Konflikt mit der Bedeutung des ursprünglichen "S" steht.

    Da SOA in der Regel sehr auf Messaging ausgerichtet ist, sind die Vorgänge, die Sie möglicherweise zuvor in einem Service gepackt haben, in der Regel in Handlern zusammengefasst , die Vielzahl ist jedoch unterschiedlich. Jeder Handler verarbeitet einen Nachrichtentyp und eine Operation. Es ist eine strikte Interpretation des Single-Responsibility-Prinzips , sorgt jedoch für eine hervorragende Wartbarkeit, da jede mögliche Operation in einer eigenen Klasse liegt. So müssen Sie nicht wirklich brauchen zentralisierte Business - Logik, da Befehle Geschäftsbetrieb eher als technische diejenigen darstellt.

Letztendlich wird es in jeder Architektur, die Sie wählen, eine Komponente oder Schicht geben, die den größten Teil der Geschäftslogik enthält. Wenn die Geschäftslogik überall verstreut ist, haben Sie nur Spaghetti-Code. Ob Sie diese Komponente als Service bezeichnen oder nicht und wie sie in Bezug auf Anzahl oder Größe der Vorgänge ausgelegt ist, hängt von Ihren architektonischen Zielen ab.

Es gibt keine richtigen oder falschen Antworten, sondern nur die für Ihre Situation zutreffenden.

Aaronaught
quelle
12
Vielen Dank für die sehr ausführliche Antwort, Sie haben alles klargestellt, was mir einfällt. Auch wenn die anderen Antworten von guter bis ausgezeichneter Qualität sind, glaube ich, dass diese Antwort sie alle übertrifft, deshalb werde ich diese akzeptieren. Ich werde es hier für die anderen Antworten hinzufügen: exquisite Qualität und Informationen, aber leider kann ich Ihnen nur eine positive Bewertung geben.
Jeroen Vannevel
2
Ich stimme der Tatsache nicht zu, dass in einer traditionellen Schichtarchitektur Service ein Synonym für die Geschäftslogikschicht ist.
CodeART
1
@CodeART: Es handelt sich um eine dreistufige Architektur. Ich habe 4-Tier-Architekturen gesehen, bei denen es eine "Anwendungsschicht" zwischen der Präsentations- und der Geschäftsschicht gibt, die manchmal auch als "Service" -Schicht bezeichnet wird, aber ehrlich gesagt, die einzigen Orte, an denen ich diese Implementierung jemals erfolgreich erlebt habe, sind riesige Ausdehnungen Unendlich konfigurierbare Run-your-Whole-Business-for-You-Produkte von SAP oder Oracle, und ich fand, dass es hier nicht wirklich erwähnenswert ist. Ich kann eine Klarstellung hinzufügen, wenn Sie möchten.
Aaronaught
1
Nehmen wir jedoch einen Controller mit mehr als 100 Zeilen (dieser Controller akzeptiert beispielsweise eine Nachricht - deserialisieren Sie anschließend das JSON-Objekt, führen Sie eine Validierung durch, wenden Sie Geschäftsregeln an, speichern Sie es in der Datenbank und geben Sie das Ergebnisobjekt zurück) und verschieben Sie eine Logik in eine der Dienstmethoden, die als "service method doesn" bezeichnet werden. Hilft uns das nicht, jeden Teil davon schmerzlos einzeln zu testen?
Artjom
2
@Aaronaught Ich wollte eine Sache klären, wenn Domain-Objekte über ORM auf db abgebildet sind und keine Geschäftslogik in ihnen steckt. Ist dies ein anämisches Domain-Modell oder nicht?
Artjom
40

Was Ihren Titel betrifft , halte ich die Frage nicht für sinnvoll. Das MVC-Modell besteht aus Daten und Geschäftslogik. Zu sagen, dass Logik im Service und nicht im Modell enthalten sein sollte, ist wie zu sagen: "Der Passagier sollte auf dem Sitz sitzen, nicht im Auto."

Andererseits ist der Begriff "Modell" ein überladener Begriff. Vielleicht haben Sie nicht MVC-Modell gemeint, sondern Modell im Sinne von Data Transfer Object (DTO). AKA eine Einheit. Davon spricht Martin Fowler.

Aus meiner Sicht spricht Martin Fowler von Dingen in einer idealen Welt. In der realen Welt von Hibernate und JPA (im Java-Land) sind die DTOs eine extrem undichte Abstraktion. Ich würde gerne meine Geschäftslogik in mein Unternehmen integrieren. Es würde die Dinge viel sauberer machen. Das Problem ist, dass diese Entitäten in einem verwalteten / zwischengespeicherten Zustand existieren können, der sehr schwer zu verstehen ist und Ihre Bemühungen ständig verhindert. Zusammenfassend meine Meinung: Martin Fowler empfiehlt den richtigen Weg, aber die ORMs hindern Sie daran.

Ich denke Bob Martin hat einen realistischeren Vorschlag und er gibt es in diesem Video, das nicht kostenlos ist . Er spricht davon, Ihre DTOs frei von Logik zu halten. Sie speichern die Daten einfach und übertragen sie auf eine andere Ebene, die viel objektorientierter ist und die DTOs nicht direkt verwendet. Dies verhindert, dass die undichte Abstraktion Sie beißt. Die Schicht mit den DTOs und die DTOs selbst sind nicht OO. Aber sobald Sie diese Schicht verlassen, werden Sie so OO, wie es Martin Fowler befürwortet.

Der Vorteil dieser Trennung besteht darin, dass die Persistenzschicht abstrahiert wird. Sie könnten von JPA zu JDBC (oder umgekehrt) wechseln, ohne dass sich die Geschäftslogik ändern müsste. Es hängt nur von den DTOs ab, es ist egal, wie diese DTOs bevölkert werden.

Um Themen leicht zu ändern, müssen Sie berücksichtigen, dass SQL-Datenbanken nicht objektorientiert sind. ORMs haben jedoch normalerweise eine Entität - das ist ein Objekt - pro Tabelle. Sie haben also von Anfang an einen Kampf verloren. Nach meiner Erfahrung können Sie die Entität niemals objektorientiert so darstellen, wie Sie es möchten.

Was " einen Dienst" angeht, wäre Bob Martin dagegen, eine Klasse namens zu haben FooBarService. Das ist nicht objektorientiert. Was macht ein Service? Alles, was damit zu tun hatFooBars . Es kann auch beschriftet werden FooBarUtils. Ich denke, er würde eine Service-Schicht befürworten (ein besserer Name wäre die Business-Logik-Schicht), aber jede Klasse in dieser Schicht hätte einen aussagekräftigen Namen.

Daniel Kaplan
quelle
2
Stimmen Sie Ihrem Standpunkt zu ORMs zu. Sie verbreiten eine Lüge, dass Sie Ihre Entität direkt der Datenbank mit ihnen zuordnen. In Wirklichkeit kann eine Entität über mehrere Tabellen hinweg gespeichert sein.
Andy
@Daniel Kaplan wissen Sie, was der aktualisierte Link für das Video von Bob Martin ist?
Brian Morearty
25

Ich arbeite gerade auf der grünen Wiese und wir mussten erst gestern einige architektonische Entscheidungen treffen. Lustigerweise musste ich einige Kapitel von 'Patterns of Enterprise Application Architecture' noch einmal durchgehen.

Das haben wir uns ausgedacht:

  • Datenschicht. Datenbank abfragen und aktualisieren. Die Schicht wird durch injizierbare Depots freigelegt.
  • Domain-Schicht. Hier lebt die Geschäftslogik. Diese Schicht verwendet injizierbare Repositorys und ist für den Großteil der Geschäftslogik verantwortlich. Dies ist der Kern der Anwendung, die wir gründlich testen werden.
  • Service-Schicht. Diese Schicht spricht mit der Domänenschicht und bedient die Client-Anforderungen. In unserem Fall ist die Service-Schicht recht einfach: Sie leitet Anforderungen an die Domain-Schicht weiter, behandelt die Sicherheit und einige andere Querschnittsthemen. Dies unterscheidet sich nicht wesentlich von einem Controller in einer MVC-Anwendung - Controller sind klein und einfach.
  • Client-Schicht. Spricht über SOAP mit der Service-Schicht.

Am Ende haben wir Folgendes:

Client -> Service -> Domain -> Daten

Wir können die Client-, Service- oder Datenschicht durch angemessenen Arbeitsaufwand ersetzen. Wenn Ihre Domänenlogik im Service enthalten war und Sie beschlossen haben, Ihre Service-Schicht zu ersetzen oder sogar zu entfernen, müssen Sie die gesamte Geschäftslogik an einen anderen Ort verschieben. Eine solche Anforderung ist selten, kann aber vorkommen.

Nachdem ich das alles gesagt habe, denke ich, dass dies ziemlich nahe an dem liegt, was Martin Fowler damit gemeint hat

Diese Dienste stehen über dem Domänenmodell und verwenden das Domänenmodell für Daten.

Das folgende Diagramm veranschaulicht dies ziemlich gut:

http://martinfowler.com/eaaCatalog/serviceLayer.html

http://martinfowler.com/eaaCatalog/ServiceLayerSketch.gif

CodeART
quelle
2
Sie haben sich erst gestern für SOAP entschieden? Ist das eine Vorraussetzung oder hattest du einfach keine bessere Idee?
JensG
1
REST wird es nicht für unsere Anforderungen schneiden. SOAP oder REST, das macht keinen Unterschied für die Antwort. Nach meinem Verständnis ist Service ein Tor zur Domänenlogik.
CodeART
Stimmen Sie dem Gateway absolut zu. SOAP ist (standardisierte) Bloatware, also musste ich fragen. Und ja, auch keine Auswirkung auf Frage / Antwort.
JensG
6
Tun Sie sich selbst einen Gefallen und töten Sie Ihre Service-Schicht. Ihre Benutzeroberfläche sollte Ihre Domain direkt verwenden. Ich habe das schon einmal gesehen und Ihre Domain wird immer zu einem Haufen anämischer Dtos, nicht zu reichen Modellen.
Andy
Diese Folien decken die Erklärung zur Serviceebene und diese Grafik oben ziemlich ordentlich ab: slideshare.net/ShwetaGhate2/…
Marc Juchli
9

Dies ist eines der Dinge, die wirklich vom Anwendungsfall abhängen. Der Hauptzweck einer Serviceschicht ist die Konsolidierung der Geschäftslogik. Dies bedeutet, dass mehrere Controller denselben UserService.MakeHimPay () aufrufen können, ohne sich wirklich darum zu kümmern, wie die Zahlung ausgeführt wird. Was in dem Dienst vor sich geht, kann so einfach wie das Ändern einer Objekteigenschaft sein, oder es kann komplexe Logik ausführen, die sich mit anderen Diensten befasst (z. B. Aufrufen von Diensten von Drittanbietern, Aufrufen von Validierungslogik oder einfach nur Speichern von etwas in der Datenbank). )

Dies bedeutet nicht, dass Sie ALLE Logik von den Domänenobjekten entfernen müssen. Manchmal ist es einfach sinnvoller, wenn eine Methode für das Domänenobjekt einige Berechnungen für sich selbst ausführt. In Ihrem letzten Beispiel ist der Service eine redundante Schicht über dem Repository / Domain-Objekt. Es bietet einen schönen Puffer gegen Anforderungsänderungen, ist aber wirklich nicht notwendig. Wenn Sie glauben, einen Dienst zu benötigen, versuchen Sie, die einfache Logik "Eigenschaft X für Objekt Y ändern" anstelle des Domänenobjekts auszuführen. Die Logik für die Domänenklassen fällt in der Regel eher in den Bereich "Diesen Wert aus Feldern berechnen", als dass alle Felder über Getter / Setter verfügbar gemacht werden.

firelore
quelle
2
Ihre Haltung zugunsten einer Serviceschicht mit Geschäftslogik ist sehr sinnvoll, lässt aber noch einige Fragen offen. In meinem Beitrag habe ich mehrere seriöse Quellen zitiert, die von der Serviceebene als einer Fassade ohne Geschäftslogik sprechen. Dies steht in direktem Gegensatz zu Ihrer Antwort. Können Sie diesen Unterschied vielleicht klarstellen?
Jeroen Vannevel
5
Ich finde, dass es wirklich von der Art der Geschäftslogik und anderen Faktoren abhängt, wie zum Beispiel der verwendeten Sprache. Einige Geschäftslogiken passen nicht sehr gut zu den Domänenobjekten. Ein Beispiel ist das Filtern / Sortieren von Ergebnissen, nachdem diese aus der Datenbank zurückgezogen wurden. Es ist eine Geschäftslogik, aber für das Domänenobjekt macht es keinen Sinn. Ich finde, dass Dienste am besten für einfache Logik oder die Transformation der Ergebnisse und Logik in der Domäne verwendet werden, wenn es um das Speichern von Daten oder das Berechnen von Daten aus dem Objekt geht.
Firelore
8

Der einfachste Weg zu veranschaulichen, warum Programmierer sich davor scheuen, Domänenlogik in die Domänenobjekte einzufügen, besteht darin, dass sie normalerweise mit der Situation konfrontiert werden, dass "Wo lege ich die Überprüfungslogik ab?" Nehmen Sie zum Beispiel dieses Domain-Objekt:

public class MyEntity
{
    private int someProperty = 0;

    public int SomeProperty
    {
        get { return this.someProperty; }
        set
        {
            if(value < 0) throw new ArgumentOutOfRangeException("value");
            this.someProperty = value;
        }
    }
}

Wir haben also eine grundlegende Validierungslogik im Setter (kann nicht negativ sein). Das Problem ist, dass Sie diese Logik nicht wirklich wiederverwenden können. Irgendwo gibt es einen Bildschirm oder ein ViewModel oder einen Controller, der eine Validierung durchführen muss, bevor er die Änderung an dem Domänenobjekt festschreibt, da er den Benutzer informieren muss, bevor oder wenn er auf die Schaltfläche Speichern klickt, dass dies nicht möglich ist. und warum . Das Testen auf eine Ausnahme, wenn Sie den Setter aufrufen, ist ein hässlicher Hack, da Sie eigentlich die gesamte Validierung hätten durchführen müssen, bevor Sie die Transaktion überhaupt gestartet haben.

Aus diesem Grund verschieben die Benutzer die Validierungslogik auf eine Art von Dienst, z MyEntityValidator. Dann können die Entität und die aufrufende Logik einen Verweis auf den Validierungsdienst erhalten und ihn wiederverwenden.

Wenn Sie dies nicht tun und die Validierungslogik dennoch wiederverwenden möchten, werden Sie sie in statische Methoden der Entitätsklasse einfügen:

public class MyEntity
{
    private int someProperty = 0;

    public int SomeProperty
    {
        get { return this.someProperty; }
        set
        {
            string message;
            if(!TryValidateSomeProperty(value, out message)) 
            {
                throw new ArgumentOutOfRangeException("value", message);
            }
            this.someProperty = value;
        }
    }

    public static bool TryValidateSomeProperty(int value, out string message)
    {
        if(value < 0)
        {
            message = "Some Property cannot be negative.";
            return false;
        }
        message = string.Empty;
        return true;
    }
}

Dies würde Ihr Domain-Modell weniger "anämisch" machen und die Überprüfungslogik neben der Eigenschaft beibehalten, was großartig ist, aber ich denke, niemand mag die statischen Methoden wirklich.

Scott Whitlock
quelle
1
Was ist Ihrer Meinung nach die beste Lösung für die Validierung?
Flashrunner
3
@Flashrunner - meine Validierungslogik befindet sich definitiv in der Geschäftslogikebene, ist aber in einigen Fällen auch in der Entitäts- und Datenbankebene dupliziert. Die Geschäftsschicht handhabt es gut, indem sie den Benutzer usw. informiert, aber die anderen Schichten werfen nur Fehler / Ausnahmen und halten den Programmierer (mich) davon ab, Fehler zu machen, die Daten beschädigen.
Scott Whitlock
6

Ich denke, die Antwort ist klar, wenn Sie Martin Fowlers Artikel über das anämische Domänenmodell lesen .

Das Entfernen der Geschäftslogik, bei der es sich um die Domäne handelt, aus dem Domänenmodell bricht im Wesentlichen das objektorientierte Design.

Lassen Sie uns das grundlegendste objektorientierte Konzept betrachten: Ein Objekt kapselt Daten und Operationen. Das Schließen eines Kontos ist beispielsweise eine Operation, die ein Kontoobjekt für sich selbst ausführen sollte. Daher ist es keine objektorientierte Lösung, wenn eine Serviceschicht diese Operation ausführt. Es ist prozedural und das ist es, worauf sich Martin Fowler bezieht, wenn er über ein anämisches Domänenmodell spricht.

Wenn Sie das Konto von einer Serviceebene schließen lassen, anstatt das Kontoobjekt selbst schließen zu lassen, haben Sie kein echtes Kontoobjekt. Ihr Konto "Objekt" ist lediglich eine Datenstruktur. Was Sie am Ende haben, wie Martin Fowler vorschlägt, ist ein Bündel Taschen mit Gettern und Setzern.

Carlos A Merighe - Utah
quelle
1
Bearbeitet Ich fand das eigentlich eine recht hilfreiche Erklärung und denke nicht, dass es Abwertungen verdient.
BadHorsie
1
Es gibt einen großen Nachteil der reichen Modelle. Und das sind die Entwickler, die alles einbeziehen, was etwas mit dem Modell zu tun hat. Ist der Status von Öffnen / Schließen ein Attribut des Kontos? Was ist mit dem Besitzer? Und die Bank? Sollten sie alle vom Konto referenziert werden? Würde ich ein Konto auflösen, indem ich mit einer Bank spreche oder direkt über das Konto? Bei anämischen Modellen sind diese Verbindungen kein fester Bestandteil der Modelle, sondern werden eher erstellt, wenn mit diesen Modellen in anderen Klassen gearbeitet wird (nennen Sie sie Dienste oder Manager).
Hubert Grzeskowiak
4

Wie würden Sie Ihre Geschäftslogik in der Serviceschicht implementieren? Wenn Sie eine Zahlung von einem Benutzer ausführen, erstellen Sie eine Zahlung und ziehen nicht nur einen Wert von einer Immobilie ab.

Ihre Zahlungsmethode "Zahlung ausführen" muss einen Zahlungsdatensatz erstellen, die Schulden dieses Benutzers erhöhen und all dies in Ihren Repositorys beibehalten. Dies in einer Servicemethode zu tun, ist unglaublich einfach, und Sie können den gesamten Vorgang auch in eine Transaktion einschließen. Dasselbe in einem aggregierten Domänenmodell zu tun, ist viel problematischer.

Herr Cochese
quelle
2

Die tl; dr-Version:
Meine Erfahrungen und Meinungen besagen, dass Objekte mit Geschäftslogik Teil des Domänenmodells sein sollten. Das Datenmodell sollte wahrscheinlich überhaupt keine Logik haben. Die Dienste sollten wahrscheinlich beides miteinander verbinden und sich mit übergreifenden Belangen (Datenbanken, Protokollierung usw.) befassen. Die akzeptierte Antwort ist jedoch die praktischste.

Die längere Version, auf die andere anspielen, ist, dass das Wort "Modell" nicht eindeutig ist. Der Beitrag wechselt zwischen Datenmodell und Domänenmodell, als ob sie gleich wären, was ein sehr häufiger Fehler ist. Es kann auch eine leichte Mehrdeutigkeit in Bezug auf das Wort "Service" geben.

In der Praxis sollten Sie keinen Dienst haben, der Änderungen an Domänenobjekten vornimmt. Der Grund dafür ist, dass Ihr Dienst wahrscheinlich über eine Methode für jede Eigenschaft Ihres Objekts verfügt, um den Wert dieser Eigenschaft zu ändern. Dies ist ein Problem, da dann, wenn Sie eine Schnittstelle für Ihr Objekt haben (oder auch nicht), der Dienst nicht mehr dem Open-Closed-Prinzip folgt. Immer wenn Sie Ihrem Modell mehr Daten hinzufügen (unabhängig von Domain und Daten), müssen Sie Ihrem Service mehr Funktionen hinzufügen. Es gibt bestimmte Möglichkeiten, aber dies ist der häufigste Grund, warum "Unternehmensanwendungen" fehlschlagen, insbesondere wenn diese Organisationen der Meinung sind, dass "Unternehmen" "eine Schnittstelle für jedes Objekt im System" bedeutet. Können Sie sich vorstellen, einer Schnittstelle neue Methoden hinzuzufügen? dann auf zwei oder drei verschiedene Implementierungen (die In-App-Implementierung, die Mock-Implementierung und die Debug-Implementierung, die In-Memory-Implementierung?), nur für eine einzelne Eigenschaft Ihres Modells? Klingt für mich nach einer schrecklichen Idee.

Es gibt hier ein längeres Problem, auf das ich nicht näher eingehen werde, aber das Wesentliche ist folgendes: Die objektorientierte Hardcore-Programmierung besagt, dass niemand außerhalb des relevanten Objekts den Wert einer Eigenschaft innerhalb des Objekts ändern darf, auch nicht " siehe "den Wert der Eigenschaft innerhalb des Objekts. Dies kann vermieden werden, indem die Daten schreibgeschützt werden. Sie können immer noch auf Probleme stoßen, wenn viele Benutzer die Daten nur zum Lesen verwenden und Sie den Datentyp ändern müssen. Es ist möglich, dass sich alle Verbraucher ändern müssen, um dem Rechnung zu tragen. Aus diesem Grund wird empfohlen, dass Sie keine öffentlichen oder sogar geschützten Eigenschaften / Daten haben, wenn Sie APIs erstellen, die von jedermann und jedem verwendet werden sollen. es ist genau der Grund, warum OOP letztendlich erfunden wurde.

Ich denke, die Mehrheit der Antworten hier, abgesehen von der als akzeptiert gekennzeichneten, trübt das Problem. Der als akzeptiert markierte ist gut, aber ich hatte immer noch das Bedürfnis zu antworten und zuzustimmen, dass Kugel 4 der richtige Weg ist.

In DDD sind Dienste speziell für den Fall gedacht, dass Sie über einen Vorgang verfügen, der nicht ordnungsgemäß zu einem aggregierten Stamm gehört. Sie müssen hier vorsichtig sein, da die Notwendigkeit eines Dienstes oft impliziert, dass Sie nicht die richtigen Wurzeln verwendet haben. Angenommen, Sie verwenden einen Service, um Vorgänge über mehrere Roots hinweg zu koordinieren oder um Probleme zu lösen, die das Domänenmodell überhaupt nicht betreffen.

WolfgangSenff
quelle
1

Die Antwort ist, dass es vom Anwendungsfall abhängt. In den meisten allgemeinen Szenarien würde ich mich jedoch an die Geschäftslogik halten, die in der Serviceschicht liegt. Das Beispiel, das Sie angegeben haben, ist wirklich einfach. Wenn Sie jedoch erst einmal über entkoppelte Systeme oder Dienste nachdenken und darüber hinaus Transaktionsverhalten hinzufügen, möchten Sie, dass dies als Teil der Service-Schicht geschieht.

Die Quellen, die Sie für die Serviceschicht ohne Geschäftslogik angegeben haben, führen eine weitere Schicht ein, die die Geschäftsschicht ist. In vielen Szenarien werden die Service-Schicht und die Business-Schicht zu einer komprimiert. Es hängt wirklich davon ab, wie Sie Ihr System entwerfen möchten. Sie können die Arbeit in drei Schichten erledigen und das Dekorieren fortsetzen und Geräusche hinzufügen.

Im Idealfall können Sie Dienste modellieren, die Geschäftslogik umfassen, um an Domänenmodellen zu arbeiten und den Status beizubehalten . Sie sollten versuchen, die Dienste so weit wie möglich zu entkoppeln.

sonnig
quelle
0

In MVC wird Modell als Geschäftslogik definiert. Die Behauptung, dass es sich um einen anderen Ort handeln sollte, ist falsch, es sei denn, er verwendet MVC nicht. Ich betrachte Serviceschichten als ähnlich wie ein Modulsystem. Damit können Sie eine Reihe verwandter Funktionen in einem netten Paket zusammenfassen. Die Interna dieser Service-Schicht würden ein Modell haben, das die gleiche Arbeit leistet wie Sie.

Das Modell besteht aus Anwendungsdaten, Geschäftsregeln, Logik und Funktionen. http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller

Steinmetalle
quelle
0

Das Konzept der Service-Schicht kann aus DDD-Sicht betrachtet werden. Aaronaught erwähnte es in seiner Antwort, ich gehe nur ein bisschen darauf ein.

Ein gängiger Ansatz besteht darin, einen Controller zu haben, der für einen bestimmten Client-Typ spezifisch ist. Angenommen, es könnte ein Webbrowser sein, es könnte eine andere Anwendung sein, es könnte ein Funktionstest sein. Die Anforderungs- und Antwortformate können variieren. Daher verwende ich den Anwendungsdienst als Tool für die Verwendung einer hexagonalen Architektur . Ich injiziere dort Infrastrukturklassen, die spezifisch für eine konkrete Anfrage sind. So könnte beispielsweise mein Controller aussehen, der Webbrowser-Anfragen bearbeitet:

class WebBroserController
{
    public function purchaseOrder()
    {
        $data = $_POST;

        $responseData =
            new PurchaseOrderApplicationService(
                new PayPalClient(),
                new OrderRepository()
            )
        ;

        $response = new HtmlView($responseData);
    }
}

Wenn ich einen Funktionstest schreibe, möchte ich einen gefälschten Zahlungs-Client verwenden und benötige wahrscheinlich keine HTML-Antwort. So könnte mein Controller aussehen:

class FunctionalTestController
{
    public function purchaseOrder()
    {
        $data = $_POST;

        $responseData =
            new PurchaseOrderApplicationService(
                new FakePayPalClient(),
                new OrderRepository(),
                $data
            )
        ;

        return new JsonData($responseData);
    }
}

Der Anwendungsservice ist also eine Umgebung, die ich für die Ausführung von Geschäftslogik eingerichtet habe. Hier werden die Modellklassen aufgerufen - unabhängig von einer Infrastrukturimplementierung.

Beantworten Sie also Ihre Fragen aus dieser Perspektive:

Ist es ein Mittel, einfach die Logik aus dem Controller zu extrahieren und stattdessen in einen Service zu integrieren?

Nein.

Soll es einen Vertrag zwischen dem Controller und der Domain bilden?

Nun, man kann es so nennen.

Sollte es eine Schicht zwischen der Domain und der Service-Schicht geben?

Nee.


Es gibt jedoch einen radikal anderen Ansatz, der die Nutzung jeglicher Art von Diensten vollständig verweigert. Zum Beispiel behauptet David West in seinem Buch Object Thinking , dass jedes Objekt alle notwendigen Ressourcen haben sollte, um seine Arbeit zu erledigen. Dieser Ansatz führt beispielsweise zum Verwerfen von ORM .

Zapadlo
quelle
-2

Für die Aufzeichnung.

SRP:

  1. Modell = Daten, hier geht der Setter und Getter.
  2. Logik / Dienste = hier sind die Entscheidungen.
  3. Repository / DAO = hier werden die Informationen permanent gespeichert oder abgerufen.

In diesem Fall können Sie die folgenden Schritte ausführen:

Wenn die Schulden keine Berechnung erfordern:

userObject.Debt = 9999;

Wenn jedoch eine Berechnung erforderlich ist, gilt Folgendes:

userObject.Debt= UserService.CalculateDebt(userObject)

oder auch

UserService.UpdateDebt(userObject)

Aber auch, wenn die Berechnung in der Persistenzschicht erfolgt, ist eine solche Speicherprozedur dann

UserRepository.UpdateDebt(userObject)

In diesem Fall möchten wir den Benutzer aus der Datenbank abrufen und die Schulden dann aktualisieren. Wir sollten dies in mehreren Schritten tun (in der Tat in zwei Schritten) und es ist nicht erforderlich, sie in die Funktion eines Dienstes einzuschließen / einzuschließen.

User userObject=UserRepository.GetUserByName(somename);
UserService.UpdateDebt(userObject)

Und wenn es erforderlich ist, es dann zu speichern, können wir einen dritten Schritt hinzufügen

User userObject=UserRepository.GetUserByName(somename);
UserService.UpdateDebt(userObject)
UserRepository.Save(userobject);

Über die vorgeschlagene Lösung

a) Wir sollten keine Angst haben, dem Endentwickler zu überlassen, ein paar zu schreiben, anstatt es in eine Funktion zu kapseln.

b) Und was Interface angeht, einige Entwickler lieben Interface und es geht ihnen gut, aber in einigen Fällen werden sie überhaupt nicht benötigt.

c) Das Ziel eines Dienstes ist es, einen Dienst ohne Attribute zu erstellen, hauptsächlich weil wir Shared / Static-Funktionen verwenden können. Es ist auch einfach zu Unit-Test.

Magallane
quelle
Wie beantwortet dies die gestellte Frage: Wie genau ist "Geschäftslogik sollte in einem Service sein, nicht in einem Modell"?
gnat
3
Was für ein Satz ist "We shouldn't be afraid to left the end-developer to write a couple of instead of encapsulate it in a function."? Ich kann Lewis Black nur zitieren" ohne mein Pferd hätte ich dieses Jahr nicht in der Schule verbracht. "
Malachi