Quanto è preciso "La logica aziendale dovrebbe essere in un servizio, non in un modello"?


398

Situazione

All'inizio di questa sera ho dato una risposta a una domanda su StackOverflow.

La domanda:

La modifica di un oggetto esistente dovrebbe essere eseguita a livello di repository o in servizio?

Ad esempio se ho un utente con debito. Voglio cambiare il suo debito. Devo farlo in UserRepository o in servizio, ad esempio BuyingService, ottenendo un oggetto, modificandolo e salvandolo?

La mia risposta:

Dovresti lasciare la responsabilità di mutare un oggetto sullo stesso oggetto e utilizzare il repository per recuperare questo oggetto.

Esempio di situazione:

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
    }
}

Un commento che ho ricevuto:

La logica aziendale dovrebbe davvero essere in un servizio. Non in un modello.

Cosa dice internet?

Quindi, questo mi ha fatto cercare poiché non ho mai usato (consapevolmente) un livello di servizio. Ho iniziato a leggere sul modello del livello di servizio e sul modello unità di lavoro, ma finora non posso dire di essere convinto che debba essere utilizzato un livello di servizio.

Prendiamo ad esempio questo articolo di Martin Fowler sull'anti-schema di un modello di dominio anemico:

Esistono oggetti, molti dei quali prendono il nome dai nomi nello spazio del dominio e questi oggetti sono collegati con le ricche relazioni e la struttura che hanno i veri modelli di dominio. La cattura viene quando si osserva il comportamento e ci si rende conto che non c'è quasi alcun comportamento su questi oggetti, rendendoli poco più che sacchi di getter e setter. In effetti, spesso questi modelli sono dotati di regole di progettazione che dicono che non è necessario inserire alcuna logica di dominio negli oggetti dominio. Esistono invece una serie di oggetti di servizio che acquisiscono tutta la logica del dominio. Questi servizi vivono in cima al modello di dominio e usano il modello di dominio per i dati.

(...) La logica che dovrebbe trovarsi in un oggetto dominio è la logica del dominio - convalide, calcoli, regole aziendali - come preferisci chiamarlo.

Per me, questo sembrava esattamente ciò che la situazione riguardava: ho sostenuto la manipolazione dei dati di un oggetto introducendo metodi all'interno di quella classe che fanno proprio questo. Tuttavia mi rendo conto che questo dovrebbe essere un dato modo e probabilmente ha più a che fare con il modo in cui questi metodi vengono invocati (usando un repository).

Ho anche avuto la sensazione che in quell'articolo (vedi sotto), un livello di servizio sia più considerato come una facciata che delega il lavoro al modello sottostante, piuttosto che un livello ad alta intensità di lavoro.

Livello applicazione [il suo nome per livello di servizio]: definisce i lavori che il software dovrebbe svolgere e indirizza gli oggetti del dominio espressivo a risolvere i problemi. Le attività di cui questo livello è responsabile sono significative per l'azienda o necessarie per l'interazione con i livelli di applicazione di altri sistemi. Questo strato è mantenuto sottile. Non contiene regole o conoscenze aziendali, ma coordina solo attività e delegati lavorano a collaborazioni di oggetti di dominio nel livello successivo in basso. Non ha uno stato che riflette la situazione aziendale, ma può avere uno stato che riflette l'avanzamento di un'attività per l'utente o il programma.

Che è rafforzato qui :

Interfacce di servizio. I servizi espongono un'interfaccia di servizio a cui vengono inviati tutti i messaggi in entrata. Puoi pensare a un'interfaccia di servizio come una facciata che espone la logica di business implementata nell'applicazione (in genere, la logica nel livello aziendale) ai potenziali consumatori.

E qui :

Il livello di servizio dovrebbe essere privo di qualsiasi applicazione o logica aziendale e dovrebbe concentrarsi principalmente su alcune preoccupazioni. Dovrebbe racchiudere le chiamate del livello aziendale, tradurre il dominio in una lingua comune che i clienti possano comprendere e gestire il mezzo di comunicazione tra il server e il client richiedente.

Questo è un grave contrasto con altre risorse che parlano del livello di servizio:

Il livello di servizio deve essere costituito da classi con metodi che sono unità di lavoro con azioni che appartengono alla stessa transazione.

O la seconda risposta a una domanda che ho già collegato:

Ad un certo punto, l'applicazione richiederà qualche logica aziendale. Inoltre, potresti voler convalidare l'input per assicurarti che non ci sia qualcosa di malvagio o non performante richiesto. Questa logica appartiene al tuo livello di servizio.

"Soluzione"?

Seguendo le linee guida in questa risposta , ho trovato il seguente approccio che utilizza un livello di servizio:

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;
    }
}

Conclusione

Tutti insieme non è cambiato molto qui: il codice dal controller è passato al livello di servizio (il che è positivo, quindi c'è un lato positivo di questo approccio). Tuttavia, questo non sembra avere nulla a che fare con la mia risposta originale.

Mi rendo conto che i modelli di progettazione sono linee guida, non regole fissate nella pietra da attuare quando possibile. Tuttavia non ho trovato una spiegazione definitiva del livello di servizio e di come dovrebbe essere considerato.

  • È un mezzo per estrarre semplicemente la logica dal controller e inserirla in un servizio?

  • Dovrebbe formare un contratto tra il controller e il dominio?

  • Dovrebbe esserci un livello tra il dominio e il livello di servizio?

E, ultimo ma non meno importante: seguire il commento originale

La logica aziendale dovrebbe davvero essere in un servizio. Non in un modello.

  • È corretto?

    • Come introdurrei la mia logica aziendale in un servizio anziché nel modello?

6
Considero il livello di servizio come il luogo in cui collocare la parte inevitabile dello script di transazione che agisce su radici aggregate. Se il mio livello di servizio diventa troppo complesso, ciò mi segnala che mi muovo nella direzione del Modello Anemico e il mio modello di dominio necessita di attenzione e revisione. Cerco anche di mettere la logica in SL che non sarà duplicata per sua natura.
Pavel Voronin,

Ho pensato che i servizi facessero parte del livello Model. Sbaglio nel pensarlo?
Florian Margaine,

Uso una regola empirica: non dipendere da ciò che non ti serve; altrimenti potrei essere (di solito negativamente) influenzato da cambiamenti sulla parte che non mi servono. Di conseguenza, finisco con molti ruoli chiaramente definiti. I miei oggetti dati contengono comportamenti purché tutti i client lo utilizzino. Altrimenti sposto il comportamento in classi implementando il ruolo richiesto.
Beluchin,

1
La logica di controllo del flusso dell'applicazione appartiene a un controller. La logica di accesso ai dati appartiene a un repository. La logica di convalida appartiene a un livello di servizio. Un livello di servizio è un livello aggiuntivo in un'applicazione ASP.NET MVC che media la comunicazione tra un controller e un livello di repository. Il livello di servizio contiene una logica di convalida aziendale. repository. asp.net/mvc/overview/older-versions-1/models-data/…
Kbdavis07

3
IMHO, corretto modello di dominio in stile OOP dovrebbe effettivamente evitare "servizi aziendali" e mantenere la logica aziendale nel modello. Ma la pratica dimostra che è così allettante creare un enorme livello aziendale con metodi con nomi distinti e anche mettere una transazione (o unità di lavoro) attorno all'intero metodo. È molto più semplice elaborare più classi di modelli in un metodo di servizio aziendale piuttosto che pianificare le relazioni tra tali classi di modelli, perché in tal caso si avrebbero alcune domande difficili a cui rispondere: dov'è la radice aggregata? Dove devo iniziare / eseguire una transazione del database? Ecc.
JustAMartin,

Risposte:


369

Per definire quali sono le responsabilità di un servizio , è necessario innanzitutto definire che cos'è un servizio .

Il servizio non è un termine canonico o generico del software. In effetti, il suffisso Servicesul nome di una classe è molto simile al Manager molto diffamato : non ti dice quasi nulla di ciò che l'oggetto fa realmente .

In realtà, ciò che un servizio dovrebbe fare è altamente specifico per l'architettura:

  1. In un'architettura a strati tradizionale, il servizio è letteralmente sinonimo di livello di logica aziendale . È il livello tra interfaccia utente e dati. Pertanto, tutte le regole aziendali vanno in servizi. Il livello dati dovrebbe comprendere solo le operazioni CRUD di base e il livello UI dovrebbe occuparsi solo della mappatura dei DTO di presentazione da e verso gli oggetti business.

  2. In un'architettura distribuita in stile RPC (SOAP, UDDI, BPEL, ecc.), Il servizio è la versione logica di un endpoint fisico . È essenzialmente una raccolta di operazioni che il manutentore desidera fornire come API pubblica. Vari best practice guide spiegano che un servizio operazione dovrebbe infatti essere un'operazione a livello di business e non CRUD, e tendo a concordare.

    Tuttavia, poiché l'instradamento di tutto attraverso un vero servizio remoto può compromettere seriamente le prestazioni, di solito è meglio non avere questi servizi che implementano effettivamente la logica aziendale; invece, dovrebbero avvolgere un insieme "interno" di oggetti business. Un singolo servizio può coinvolgere uno o più oggetti business.

  3. In un'architettura MVP / MVC / MVVM / MV *, i servizi non esistono affatto. In caso contrario, il termine viene utilizzato per indicare qualsiasi oggetto generico che può essere iniettato in un controller o in un modello di visualizzazione. La logica aziendale è nel tuo modello . Se si desidera creare "oggetti di servizio" per orchestrare operazioni complesse, questo è visto come un dettaglio di implementazione. Molte persone, purtroppo, implementano MVC in questo modo, ma è considerato un anti-pattern ( Anemic Domain Model ) perché il modello stesso non fa nulla, è solo un mucchio di proprietà per l'interfaccia utente.

    Alcune persone pensano erroneamente che prendere un metodo di controllo a 100 linee e metterlo in un servizio in qualche modo crei un'architettura migliore. Davvero no; non fa altro che aggiungere un altro, probabilmente non necessario livello di indiretta. In pratica , il controller sta ancora facendo il lavoro, lo sta facendo attraverso un oggetto "helper" mal chiamato. Consiglio vivamente la presentazione di Wicked Domain Models di Jimmy Bogard per un chiaro esempio di come trasformare un modello di dominio anemico in uno utile. Implica un attento esame dei modelli che stai esponendo e quali operazioni sono effettivamente valide in un contesto aziendale .

    Ad esempio, se il database contiene ordini e si dispone di una colonna per Importo totale, probabilmente all'applicazione non dovrebbe essere consentito di modificare effettivamente quel campo in un valore arbitrario, poiché (a) è cronologia e (b) dovrebbe essere determinata da ciò che è in ordine, così come forse alcuni altri time-sensitive dati / regole. La creazione di un servizio per la gestione degli ordini non risolve necessariamente questo problema, poiché il codice utente può ancora afferrare l'oggetto Ordine reale e modificarne l'importo. Invece, l'ordine stesso dovrebbe essere responsabile di garantire che possa essere modificato solo in modo sicuro e coerente.

  4. In DDD, i servizi sono pensati specificamente per la situazione in cui si ha un'operazione che non appartiene correttamente a nessuna radice aggregata . Devi stare attento qui, perché spesso la necessità di un servizio può implicare che non hai usato le radici corrette. Ma supponendo di averlo fatto, un servizio viene utilizzato per coordinare le operazioni su più radici o, talvolta, per gestire problemi che non coinvolgono affatto il modello di dominio (come, forse, la scrittura di informazioni in un database BI / OLAP).

    Un aspetto notevole del servizio DDD è che è consentito utilizzare gli script delle transazioni . Quando si lavora su applicazioni di grandi dimensioni, è molto probabile che alla fine si verifichino casi in cui è semplicemente più semplice realizzare qualcosa con una procedura T-SQL o PL / SQL piuttosto che agitarsi con il modello di dominio. Questo va bene e appartiene a un servizio.

    Questa è una deviazione radicale dalla definizione di servizi a architettura stratificata. Un livello di servizio incapsula oggetti di dominio; un servizio DDD incapsula tutto ciò che non è negli oggetti di dominio e non ha senso esserlo.

  5. In un'architettura orientata ai servizi, un servizio è considerato l'autorità tecnica per una capacità aziendale. Ciò significa che è il proprietario esclusivo di un determinato sottoinsieme dei dati aziendali e nient'altro è autorizzato a toccare tali dati, nemmeno a leggerli .

    Per necessità, i servizi sono in realtà una proposta end-to-end in una SOA. Ciò significa che un servizio non è tanto un componente specifico quanto un intero stack e l'intera applicazione (o l'intera azienda) è un insieme di questi servizi che funzionano fianco a fianco senza intersezione, tranne a livello di messaggistica e interfaccia utente. Ogni servizio ha i suoi dati, le sue regole di business e la sua interfaccia utente. Non hanno bisogno di orchestrarsi tra loro perché dovrebbero essere allineati al business - e, come il business stesso, ogni servizio ha il suo insieme di responsabilità e opera più o meno indipendentemente dagli altri.

    Così, dalla definizione SOA, ogni pezzo della logica di business ovunque è contenuto all'interno del servizio, ma poi di nuovo, quindi è l'intero sistema . I servizi in una SOA possono avere componenti e possono avere endpoint , ma è abbastanza pericoloso chiamare un codice qualsiasi servizio perché è in conflitto con ciò che la "S" originale dovrebbe significare.

    Poiché la SOA è in genere piuttosto appassionata di messaggistica, le operazioni che potresti aver impacchettato in un servizio prima sono generalmente incapsulate nei gestori , ma la molteplicità è diversa. Ogni gestore gestisce un tipo di messaggio, un'operazione. È un'interpretazione rigorosa del principio di responsabilità singola , ma garantisce una grande manutenibilità perché ogni possibile operazione rientra nella sua classe. Quindi non hai davvero bisogno di una logica aziendale centralizzata, perché i comandi rappresentano operazioni aziendali piuttosto che tecniche.

Alla fine, in qualsiasi architettura tu scelga, ci sarà qualche componente o strato che ha la maggior parte della logica aziendale. Dopotutto, se la logica aziendale è sparsa ovunque, allora hai solo il codice spaghetti. Ma se si chiama o meno quel componente un servizio e come è progettato in termini di cose come il numero o la dimensione delle operazioni, dipende dagli obiettivi dell'architettura.

Non esiste una risposta giusta o sbagliata, solo ciò che si applica alla tua situazione.


12
Grazie per la risposta molto elaborata, hai chiarito tutto ciò a cui riesco a pensare. Mentre le altre risposte sono buone anche per l'eccellente qualità, credo che questa risposta le superi tutte, quindi accetterò questa. Lo aggiungerò qui per le altre risposte: qualità e informazioni squisite, ma purtroppo potrò solo darti un voto.
Jeroen Vannevel,

2
Non sono d'accordo con il fatto che in un'architettura a strati tradizionale, il servizio è sinonimo del livello di logica aziendale.
CodeART

1
@CodeART: è in un'architettura a 3 livelli. Io ho visto le architetture 4-tier in cui v'è un "livello di applicazione" tra la presentazione e di business strati, che è talvolta chiamato anche uno strato di "servizio", ma onestamente, gli unici posti che abbia mai visto questo implementato con successo sono enormi tentacolare prodotti infinitamente configurabili per la tua intera attività per te come SAP o Oracle, e non pensavo che valesse la pena menzionarli qui. Posso aggiungere un chiarimento, se lo desideri.
Aaronaught,

1
Ma se prendiamo più di 100 controller di linea (ad esempio, quel controller accetta il messaggio - quindi deserializza l'oggetto JSON, effettua la convalida, applica le regole di business, salva su db, restituisce l'oggetto risultato) e sposta una logica in uno dei metodi di servizio chiamati no " che cosa ci aiuta a provare separatamente ogni parte di essa indolore?
Artjom,

2
@Aaronaught Volevo chiarire una cosa se abbiamo oggetti di dominio mappati su db tramite ORM e non ci sono logiche di business in questo modello di dominio anemico o no?
artjom,

40

Per quanto riguarda il tuo titolo , non credo che la domanda abbia senso. Il modello MVC è costituito da dati e logica aziendale. Dire che la logica dovrebbe essere nel Servizio e non nel Modello è come dire "Il passeggero dovrebbe sedere sul sedile, non in macchina".

Inoltre, il termine "Modello" è un termine sovraccarico. Forse non intendevi modello MVC ma intendevi modello nel senso Data Transfer Object (DTO). AKA un'entità. Questo è ciò di cui parla Martin Fowler.

Per come la vedo io, Martin Fowler sta parlando di cose in un mondo ideale. Nel mondo reale di Hibernate e JPA (nella terra di Java) i DTO sono un'astrazione superconducibile. Mi piacerebbe mettere la mia logica aziendale nella mia entità. Renderebbe le cose più pulite. Il problema è che queste entità possono esistere in uno stato gestito / memorizzato nella cache che è molto difficile da capire e impedisce costantemente i tuoi sforzi. Per riassumere la mia opinione: Martin Fowler sta raccomandando nel modo giusto, ma gli ORM ti impediscono di farlo.

Penso che Bob Martin abbia un suggerimento più realistico e lo in questo video non è gratuito . Parla di mantenere i tuoi DTO liberi dalla logica. Conservano semplicemente i dati e li trasferiscono su un altro livello che è molto più orientato agli oggetti e non utilizza direttamente i DTO. Questo evita che l'astrazione che perde ti morda. Lo strato con i DTO e gli stessi DTO non sono OO. Ma una volta usciti da quel livello, diventerai OO come i sostenitori di Martin Fowler.

Il vantaggio di questa separazione è che estrae lo strato di persistenza. È possibile passare da JPA a JDBC (o viceversa) e nessuna logica aziendale dovrebbe cambiare. Dipende solo dai DTO, non importa come vengono popolati quei DTO.

Per modificare leggermente gli argomenti, è necessario considerare il fatto che i database SQL non sono orientati agli oggetti. Ma gli ORM di solito hanno un'entità - che è un oggetto - per tabella. Quindi fin dall'inizio hai già perso una battaglia. Nella mia esperienza, non puoi mai rappresentare l'Entità nel modo esatto che desideri in un modo orientato agli oggetti.

Per quanto riguarda " un servizio", Bob Martin sarebbe contrario ad avere una classe chiamata FooBarService. Questo non è orientato agli oggetti. Cosa fa un servizio? Tutto ciò che riguarda FooBars. Potrebbe anche essere etichettato FooBarUtils. Penso che difenderebbe un livello di servizio (un nome migliore sarebbe il livello di logica aziendale) ma ogni classe in quel livello avrebbe un nome significativo.


2
Concordare con il tuo punto sugli ORM; propagano una bugia che si mappa la tua entità direttamente al db con loro, quando in realtà un'entità può essere memorizzata su più tabelle.
Andy,

@Daniel Kaplan sai qual è il link aggiornato per il video di Bob Martin?
Brian Morearty,

25

Sto lavorando al progetto greenfield in questo momento e abbiamo dovuto prendere alcune decisioni sull'architettura proprio ieri. Stranamente ho dovuto rivisitare alcuni capitoli di "Patterns of Enterprise Application Architecture".

Questo è quello che ci siamo inventati:

  • Livello dati Database di query e aggiornamenti. Lo strato è esposto attraverso repository iniettabili.
  • Livello di dominio. Qui vive la logica aziendale. Questo strato fa uso di repository iniettabili ed è responsabile della maggior parte delle logiche aziendali. Questo è il nucleo dell'applicazione che testeremo a fondo.
  • Livello di servizio. Questo livello comunica con il livello di dominio e soddisfa le richieste del client. Nel nostro caso il livello di servizio è piuttosto semplice: inoltra le richieste al livello di dominio, gestisce la sicurezza e pochi altri problemi trasversali. Questo non è molto diverso da un controller nell'applicazione MVC: i controller sono piccoli e semplici.
  • Livello client. Parla con il livello di servizio tramite SOAP.

Finiamo con il seguente:

Client -> Servizio -> Dominio -> Dati

Siamo in grado di sostituire client, servizio o livello dati con una quantità ragionevole di lavoro. Se la tua logica di dominio viveva nel servizio e hai deciso di voler sostituire o addirittura rimuovere il tuo livello di servizio, allora dovresti spostare tutta la logica aziendale da qualche altra parte. Tale requisito è raro, ma potrebbe accadere.

Detto questo, penso che questo sia abbastanza vicino a ciò che Martin Fowler intendeva dire

Questi servizi vivono in cima al modello di dominio e usano il modello di dominio per i dati.

Lo schema seguente illustra abbastanza bene questo:

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

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


2
Hai deciso per SOAP proprio ieri? È un requisito o non hai avuto un'idea migliore?
JensG,

1
REST non lo taglierà per le nostre esigenze. SOAP o REST, questo non fa alcuna differenza per la risposta. Da quanto ho capito, il servizio è un gateway per la logica del dominio.
CodeART

Assolutamente d'accordo con il Gateway. SOAP è bloatware (standardizzato), quindi ho dovuto chiedere. E sì, nessun impatto sulla domanda / risposta.
JensG,

6
Fatti un favore e uccidi il tuo livello di servizio. La tua interfaccia utente dovrebbe usare direttamente il tuo dominio. L'ho già visto prima e il tuo dominio diventa invariabilmente un mucchio di dtos anemici, non modelli ricchi.
Andy,

Quelle diapositive coprono la spiegazione relativa al livello di servizio e questa grafica sopra abbastanza ordinata: slideshare.net/ShwetaGhate2/…
Marc Juchli

9

Questa è una di quelle cose che dipende davvero dal caso d'uso. Il punto generale di un livello di servizio è consolidare insieme la logica aziendale. Ciò significa che diversi controller possono chiamare lo stesso UserService.MakeHimPay () senza preoccuparsi effettivamente di come viene effettuato il pagamento. Ciò che accade nel servizio può essere semplice come la modifica di una proprietà di un oggetto o può comportare una complessa logica di gestione di altri servizi (ad esempio, chiamare servizi di terze parti, chiamare la logica di convalida o anche semplicemente salvare qualcosa nel database. )

Ciò non significa che devi eliminare TUTTA la logica dagli oggetti dominio. A volte è più sensato che un metodo sull'oggetto dominio esegua alcuni calcoli su se stesso. Nell'esempio finale, il servizio è un livello ridondante sull'oggetto repository / domain. Fornisce un buon buffer contro le modifiche ai requisiti, ma in realtà non è necessario. Se ritieni di aver bisogno di un servizio, prova a fare la semplice logica "modifica proprietà X sull'oggetto Y" anziché l'oggetto dominio. La logica delle classi di dominio tende a cadere nel "calcolare questo valore dai campi" anziché esporre tutti i campi tramite getter / setter.


2
La tua posizione a favore di un livello di servizio con la logica aziendale ha molto senso, ma questo lascia ancora alcune domande. Nel mio post ho citato diverse fonti rispettabili che parlano del livello di servizio come un vuoto di facciata di qualsiasi logica aziendale. Questo è in diretto contrasto con la tua risposta, potresti forse chiarire questa differenza?
Jeroen Vannevel,

5
Trovo che dipenda davvero dal TIPO di logica aziendale e da altri fattori, come il linguaggio utilizzato. Alcune logiche aziendali non si adattano molto bene agli oggetti del dominio. Un esempio è filtrare / ordinare i risultati dopo averli ritirati dal database. È una logica aziendale, ma non ha senso sull'oggetto dominio. Trovo che i servizi siano utilizzati al meglio per la logica semplice o per trasformare i risultati e la logica sul dominio è molto utile quando si tratta di salvare dati o calcolare dati dall'oggetto.
fuochi d'artificio

8

Il modo più semplice per illustrare perché i programmatori evitano di inserire la logica di dominio negli oggetti dominio è che di solito si trovano ad affrontare una situazione di "dove inserisco la logica di convalida?" Prendi questo oggetto dominio per esempio:

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;
        }
    }
}

Quindi abbiamo una logica di validazione di base nel setter (non può essere negativo). Il problema è che non puoi davvero riutilizzare questa logica. Da qualche parte c'è uno schermo o un ViewModel o un controller che deve effettuare la convalida prima che esegua effettivamente la modifica all'oggetto dominio, perché deve informare l'utente prima o quando fanno clic sul pulsante Salva che non possono farlo, e perché . Testare un'eccezione quando chiami il setter è un brutto trucco perché avresti davvero dovuto fare tutta la validazione prima ancora di iniziare la transazione.

Ecco perché le persone spostano la logica di convalida in qualche tipo di servizio, come ad esempio MyEntityValidator. Quindi l'entità e la logica di chiamata possono sia ottenere un riferimento al servizio di convalida sia riutilizzarlo.

Se non lo fai e vuoi ancora riutilizzare la logica di validazione, finisci per metterlo in metodi statici della classe di entità:

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;
    }
}

Ciò renderebbe il tuo modello di dominio meno "anemico" e manterrebbe la logica di convalida accanto alla proprietà, il che è fantastico, ma non credo che a nessuno piacciano molto i metodi statici.


1
Qual è la migliore soluzione per la convalida secondo te?
Flashrunner,

3
@Flashrunner: la mia logica di convalida si trova definitivamente nel livello della logica aziendale, ma in alcuni casi è anche duplicata nei livelli entità e database. Il livello aziendale lo gestisce bene informando l'utente, ecc., Ma gli altri livelli generano solo errori / eccezioni e impediscono al programmatore (me stesso) di commettere errori che danneggiano i dati.
Scott Whitlock,

6

Penso che la risposta sia chiara se leggi l'articolo sul modello di dominio anemico di Martin Fowler .

Rimuovere la logica di business, che è il dominio, dal modello di dominio sta essenzialmente rompendo la progettazione orientata agli oggetti.

Rivediamo il concetto orientato agli oggetti più semplice: un oggetto incapsula dati e operazioni. Ad esempio, la chiusura di un account è un'operazione che un oggetto account deve eseguire su se stesso; pertanto, fare in modo che un livello di servizio esegua tale operazione non è una soluzione orientata agli oggetti. È procedurale, ed è ciò a cui Martin Fowler si riferisce quando parla di un modello di dominio anemico.

Se hai un livello di servizio vicino all'account, piuttosto che avere l'oggetto account chiuso da solo, non hai un oggetto account reale. Il tuo "oggetto" account è semplicemente una struttura di dati. Ciò che si finisce con, come suggerisce Martin Fowler, è un mucchio di borse con getter e setter.


1
Modificato. In realtà ho trovato questa spiegazione abbastanza utile e non credo che meriti un voto negativo.
BadHorsie,

1
C'è un aspetto negativo ampiamente supervisionato di modelli ricchi. E questo è gli sviluppatori che inseriscono qualcosa di leggermente correlato al modello. Lo stato di aperto / chiuso è un attributo dell'account? E il proprietario? E la banca? Dovrebbero essere tutti indicati dall'account? Vorrei chiudere un conto parlando con una banca o tramite il conto direttamente? Con i modelli anemici, tali connessioni non sono parte integrante dei modelli, ma sono piuttosto create quando si lavora con quei modelli in altre classi (chiamarli servizi o gestori).
Hubert Grzeskowiak

4

Come implementeresti la tua logica di business nel livello di servizio? Quando effettui un pagamento da un utente, devi creare un pagamento, non solo detrarre un valore da una proprietà.

Il tuo metodo di pagamento deve creare un record di pagamento, aggiungere al debito di quell'utente e persistere tutto questo nei tuoi repository. Fare questo in un metodo di servizio è incredibilmente semplice e puoi anche concludere l'intera operazione in una transazione. Fare lo stesso in un modello di dominio aggregato è molto più problematico.


2

La versione tl; dr: le
mie esperienze e opinioni dicono che qualsiasi oggetto che abbia una logica aziendale dovrebbe far parte del modello di dominio. È probabile che il modello di dati non abbia alcuna logica. I servizi dovrebbero probabilmente unire le due cose e affrontare i problemi trasversali (database, registrazione, ecc.). Tuttavia, la risposta accettata è la più pratica.

La versione più lunga, che è stata accennata in qualche modo da altri, è che esiste un equivoco sulla parola "modello". Il post passa dal modello di dati al modello di dominio come se fossero gli stessi, il che è un errore molto comune. Potrebbe esserci anche un leggero equivoco sulla parola "servizio".

In termini pratici, non dovresti avere un servizio che apporti modifiche ad alcun oggetto di dominio; la ragione di ciò è che il tuo servizio avrà probabilmente un metodo per ogni proprietà sul tuo oggetto al fine di modificare il valore di quella proprietà. Questo è un problema perché quindi, se si dispone di un'interfaccia per l'oggetto (o anche in caso contrario), il servizio non segue più il principio aperto-chiuso; invece, ogni volta che aggiungi più dati al tuo modello (indipendentemente dal dominio rispetto ai dati), finisci per dover aggiungere più funzioni al tuo servizio. Ci sono alcuni modi per aggirarlo, ma questa è la ragione più comune per cui ho visto fallire le applicazioni "enterprise", specialmente quando quelle organizzazioni pensano che "enterprise" significhi "avere un'interfaccia per ogni oggetto nel sistema". Riesci a immaginare di aggiungere nuovi metodi a un'interfaccia, quindi a due o tre diverse implementazioni (quella in-app, l'implementazione finta e quella di debug, quella in memoria?), solo per una singola proprietà sul tuo modello? Mi sembra un'idea terribile.

C'è un problema più lungo qui che non affronterò, ma l'essenza è questa: la programmazione orientata agli oggetti Hardcore dice che nessuno al di fuori dell'oggetto rilevante dovrebbe essere in grado di cambiare il valore di una proprietà all'interno dell'oggetto, e nemmeno " vedi "il valore della proprietà all'interno dell'oggetto. Questo può essere alleviato rendendo i dati di sola lettura. Puoi ancora riscontrare problemi come quando molte persone usano i dati anche come di sola lettura e devi cambiare il tipo di quei dati. È possibile che tutti i consumatori debbano cambiare per soddisfarlo. Questo è il motivo per cui, quando si fanno in modo che le API vengano utilizzate da chiunque e da tutti, si consiglia di non disporre di proprietà / dati pubblici o addirittura protetti; è la vera ragione per cui OOP è stato inventato, alla fine.

Penso che la maggior parte delle risposte qui, oltre a quella contrassegnata come accettata, stiano tutti oscurando il problema. Quello contrassegnato come accettato è buono, ma ho ancora sentito la necessità di rispondere e concordare sul fatto che il proiettile 4 è la strada da percorrere, in generale.

In DDD, i servizi sono pensati specificamente per la situazione in cui si ha un'operazione che non appartiene correttamente a nessuna radice aggregata. Devi stare attento qui, perché spesso la necessità di un servizio può implicare che non hai usato le radici corrette. Ma supponendo che lo abbia fatto, un servizio viene utilizzato per coordinare le operazioni su più radici o, talvolta, per gestire le preoccupazioni che non coinvolgono affatto il modello di dominio ...


1

La risposta è che dipende dal caso d'uso. Ma nella maggior parte degli scenari generici, aderirei alla logica aziendale che giace nel livello di servizio. L'esempio che hai fornito è davvero semplice. Tuttavia, una volta che inizi a pensare a sistemi o servizi disaccoppiati e ad aggiungere un comportamento transazionale, vuoi davvero che accada come parte del livello di servizio.

Le fonti che hai citato per il livello di servizio privo di qualsiasi logica aziendale introducono un altro livello che è il livello aziendale. In molti scenari il livello di servizio e il livello aziendale sono compressi in uno solo. Dipende davvero da come vuoi progettare il tuo sistema. Puoi svolgere il lavoro in tre livelli e continuare a decorare e aggiungere rumore.

Ciò che puoi idealmente fare sono i servizi modello che comprendono la logica aziendale per lavorare su modelli di dominio per mantenere lo stato . Dovresti provare a disaccoppiare i servizi il più possibile.


0

Nel modello MVC viene definita la logica aziendale. Affermare che dovrebbe essere altrove non è corretto a meno che non stia utilizzando MVC. Vedo i livelli di servizio come simili a un sistema di moduli. Ti consente di raggruppare un set di funzionalità correlate in un bel pacchetto. Gli interni di quel livello di servizio avrebbero un modello che fa lo stesso lavoro del tuo.

Il modello è costituito da dati dell'applicazione, regole aziendali, logica e funzioni. http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller


0

Il concetto di livello di servizio può essere visualizzato dalla prospettiva DDD. Aaronaught lo ha menzionato nella sua risposta, ho appena approfondito un po '.

L'approccio comune è quello di avere un controller specifico per un tipo di client. Diciamo, potrebbe essere un browser web, potrebbe essere un'altra applicazione, potrebbe essere un test funzionale. I formati di richiesta e risposta potrebbero variare. Quindi uso il servizio applicativo come strumento per l'utilizzo di un'architettura esagonale . Inietto lì classi di infrastruttura specifiche per una richiesta concreta. Ad esempio, è così che il mio controller che soddisfa le richieste del browser Web potrebbe apparire come:

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

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

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

Se sto scrivendo un test funzionale, voglio usare un client di pagamento falso e probabilmente non avrei bisogno di una risposta HTML. Quindi il mio controller potrebbe apparire così:

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

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

        return new JsonData($responseData);
    }
}

Quindi il servizio applicativo è un ambiente che ho impostato per l'esecuzione della logica di business. È qui che vengono chiamate le classi del modello - agnosticamente di un'implementazione dell'infrastruttura.

Quindi, rispondendo alle tue domande da questa prospettiva:

È un mezzo per estrarre semplicemente la logica dal controller e inserirla in un servizio?

No.

Dovrebbe formare un contratto tra il controller e il dominio?

Bene, si può chiamare così.

Dovrebbe esserci un livello tra il dominio e il livello di servizio?

No.


Esiste tuttavia un approccio radicalmente diverso che nega totalmente l'uso di qualsiasi tipo di servizio. Ad esempio, David West nel suo libro Object Thinking afferma che qualsiasi oggetto dovrebbe avere tutte le risorse necessarie per fare il suo lavoro. Questo approccio comporta, ad esempio, l' eliminazione di qualsiasi ORM .


-2

Per il record.

prezzo consigliato:

  1. Modello = Dati, qui vanno il setter e i getter.
  2. Logica / Servizi = ecco le decisioni.
  3. Repository / DAO = qui memorizziamo o recuperiamo le informazioni.

In questo caso, è OK per eseguire i passaggi seguenti:

Se il debito non richiederà alcun calcolo:

userObject.Debt = 9999;

Tuttavia, se richiede alcuni calcoli, allora:

userObject.Debt= UserService.CalculateDebt(userObject)

o anche

UserService.UpdateDebt(userObject)

Inoltre, se il calcolo viene eseguito nel livello di persistenza, tale procedura di memorizzazione quindi

UserRepository.UpdateDebt(userObject)

In questo caso, vogliamo recuperare l'utente dal database e aggiornare quindi il debito, dovremmo farlo in diversi passaggi (in effetti, due) e non è necessario avvolgerlo / incapsularlo nella funzione di un servizio.

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

E se è necessario memorizzarlo, possiamo aggiungere un terzo passaggio

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

Informazioni sulla soluzione proposta

a) Non dovremmo avere paura di lasciare allo sviluppatore finale di scrivere un paio invece di incapsularlo in una funzione.

b) E su Interface, alcuni sviluppatori adorano l'interfaccia e stanno bene, ma in molti casi non ne hanno affatto bisogno.

c) L'obiettivo di un servizio è crearne uno senza attributi, principalmente perché possiamo usare le funzioni condivise / statiche. È anche facile test unitario.


come risponde a questa domanda: quanto è precisa "la logica aziendale dovrebbe essere in un servizio, non in un modello"?
moscerino

3
Che tipo di frase è "We shouldn't be afraid to left the end-developer to write a couple of instead of encapsulate it in a function."? Posso solo citare Lewis Black" se non fosse stato per il mio cavallo non avrei trascorso quell'anno al college ".
Malachi,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.