Architettura rilassata DAL o BLL anemico?

6

In particolare per il collaudo delle unità, sto cercando di implementare un'applicazione con un'architettura a strati . Sto codificando in C # e utilizzo l'API Web ASP.NET per il livello di servizio. Sto puntando su un'architettura a 3 livelli:

  1. Livello di servizio
  2. Livello di logica aziendale
  3. Livello di accesso ai dati

Il problema

Mi sento a disagio nel dover scegliere tra un DAL di architettura rilassato o classi di BLL anemiche e mi chiedo se sia perché mi sto avvicinando a questo nel modo sbagliato. Quando mi riferisco a "DAL rilassato per architettura", intendo un DAL che può essere chiamato da strati diversi da il BLL direttamente sopra di esso, cioè il livello di servizio.

La mia ragione per non volere un'architettura rilassata DAL: semplifica l'esclusione errata della logica aziendale.

La mia ragione per non volere classi anemiche di BLL: non voglio una pletora di classi che non fanno altro che passare le chiamate tra il livello di servizio e il DAL.

Esempio

Ecco alcuni esempi di codice. Innanzitutto, un metodo del livello di servizio che supporta la registrazione di uno studente in una classe:

public class ClassRegistrationController : ApiController
{
    private readonly BusinessLogic.ClassRegistration _classRegistrationLogic;

    public ClassRegistrationController(
        BusinessLogic.ClassRegistration classRegistrationLogic)
    {
        _classRegistrationLogic = classRegistrationLogic;
    }

    [Route("/api/class/{classId}/register")]
    [HttpPost]
    public IHttpActionResult RegisterForClass(
        int classId,
        [FromBody]ClassRegistrationRequest registrationRequest)
    {
        try
        {
            _classRegistrationLogic.RegisterStudentForClass(
                classId,
                registrationRequest.StudentId);
        }
        catch (ClassFullException)
        {
            return BadRequest("Cannot register for class, it is full");
        }

        return Ok();
    }
}

E la classe della business logic a supporto:

public class ClassRegistration
{
    private DataLayer.IClassDataLayer _classDataLayer;
    private DataLayer.IClassRegistrationDataLayer _classRegistrationDataLayer;

    public ClassRegistration(
        DataLayer.IClassDataLayer classDataLayer,
        DataLayer.IClassRegistrationDataLayer classRegistrationDataLayer)
    {
        _classDataLayer = classDataLayer;
        _classRegistrationDataLayer = classRegistrationDataLayer;
    }

    public void RegisterStudentForClass(int classId, int studentId)
    {
        var classDetails = _classDataLayer.GetClassDetails(classId);

        if (classDetails.ClassIsFull)
        {
            throw new ClassFullException();
        }

        _classRegistrationDataLayer.AddStudentToClass(classId, studentId);
    }
}

Le interfacce del livello dati (l'implementazione non è pertinente alla domanda):

public interface IClassDataLayer
{
    Models.ClassDetails GetClassDetails(int classId);
}

public interface IClassRegistrationDataLayer
{
    int AddStudentToClass(int classId, int studentId);
}

Ora, io penso tutto ciò sembra ragionevole, e ogni classe ha una sola (e non insignificante) responsabilità. Ma ora diciamo che voglio aggiungere un metodo del livello di servizio che espone i dettagli di una classe, come:

[Route("/api/class/{classId}")]
[HttpGet]
public IHttpActionResult GetClassDetails(int classId)
{
    var classDetails = _classDetailsGetter.GetClassDetails(classId);

    if (classDetails == null)
    {
        return NotFound();
    }

    return Ok(classDetails);
}

La domanda è, sarà _classDetailsGetter un'istanza di una classe dal DAL o dalla BLL?

Se proviene dal DAL, allora sto dicendo che è "OK" perché il livello di servizio passi direttamente al DAL, il che apre il rischio, ad esempio, di qualcuno che fa manutenzione sul metodo del livello di servizio RegisterForClass decidendo di andare direttamente a IClassDataLayer anziché BusinessLogic.ClassRegistration .

Ma se proviene dalla BLL, allora quella classe BLL sarebbe responsabile solo della mappatura delle chiamate 1-a-1, il che mi fa anche sbagliare. Come:

public class ClassDetailsLogic
{
    private readonly DataLayer.IClassDataLayer _classDataLayer;

    public ClassDetailsLogic(DataLayer.IClassDataLayer classDataLayer)
    {
        _classDataLayer = classDataLayer;
    }

    public Models.ClassDetails GetClassDetails(int classId)
    {
        return _classDataLayer.GetClassDetails(classId);
    }
}

Sommario

C'è motivo di favorire l'architettura rilassata DAL o la BLL anemica? Le mie preoccupazioni riguardo l'una (o l'altra) sono infondate? O c'è qualcos'altro che mi manca?

    
posta Snixtor 14.04.2016 - 03:56
fonte

2 risposte

1

Gli oggetti Data Transfer (DTO) hanno uno scopo e un solo scopo: recuperare i dati da un database e lavorare con esso in un modulo orientato agli oggetti.

Non esiste un "oggetto BLL anemico". È un oggetto BLL di prima classe o è un DTO. È raramente entrambi.

Ci sono dei motivi per cui hai cose come oggetti ViewModel. Un oggetto Modello di vista contiene dati dal modello, ma è adattato a una vista. Non è un DTO, almeno non nel senso che stai trasferendo dati avanti e indietro da un database. È un oggetto a sé stante, con un proprio scopo specifico.

Quindi, da dove viene il tuo Business Logic Layer?

Lo scopo di un livello di business logic è di convertire le operazioni del dominio aziendale (come Trasferimento denaro o Build Widget) in operazioni CRUD. Tuttavia, in realtà non esegue le operazioni CRUD; questo è ciò che fanno i tuoi DAL e DTO.

Sembra un sacco di oggetti, no? Ma i tuoi DTO in genere verranno generati dal tuo Object-Relational Mapper, a meno che tu non abbia intenzione di creare il database dal dominio dell'oggetto (una tecnica perfettamente valida).

Fai uso di lezioni parziali.

Quindi, come sposare il codice della tua logica aziendale con i tuoi oggetti di business? Un modo per farlo è con classi parziali. C # consente di definire la propria classe insieme al DTO generato dal codice, che aggiungerà la convalida personalizzata o qualsiasi altra logica aziendale che si desidera aggiungere all'oggetto del dominio aziendale. Poiché la tua logica scritta a mano si trova in una classe separata (con lo stesso nome), il tuo codice sarà protetto dalla sovrascrittura della classe generata dal codice, nel caso in cui decidessi di rigenerare i tuoi DTO. Le tue lezioni DAL non saranno più anemiche, perché avrai aggiunto la tua intelligenza a loro tramite classi parziali.

    
risposta data 14.04.2016 - 05:54
fonte
0

Le due operazioni che si stanno confrontando non possono essere confrontate. RegistrationStudentForClass è un comando e GetClassDetails è una query . I comandi e le query sono di natura molto diversa, ecco perché implementare la query mentre il comando sembra un po 'fuori.

I comandi implementano un caso d'uso, hanno validazione, logica, regole, ecc. Il controllore delega tutto questo alla logica aziendale e la logica aziendale utilizzerà un repository o tale per recuperare e quindi salvare i modelli di dati coinvolti (idealmente a livello transazionale ).

Le query, d'altra parte, non hanno logica e non hanno effetti collaterali. Ecco perché non si finisce con nulla nella tua classe logica. Sono, come dice il nome, una query sull'archivio dati. L'unica "logica" che potrebbero avere è assicurarsi che il richiedente abbia accesso ai dati richiesti. Alcune volte è possibile farlo nel controller, alcune volte è necessario parametrizzare la query. Ma c'è non ha senso usare la stessa struttura o le stesse classi dei tuoi comandi e io in realtà ti consiglio di non farlo.

Troverai questo approccio sotto il nome CQRS . Il più delle volte è presentato come uno schema molto complesso non raccomandato per piccoli progetti, ma perché alcune persone pensano che CQRS implichi l'utilizzo di Event Sourcing, separazione degli archivi di dati di lettura e scrittura, coerenza finale, ecc. Ma se si prende solo come la minima definizione del pattern, separando lo stack per le letture e le scritture, ma utilizzando lo stesso archivio dati, diventa molto semplice e ha molto senso.

Quindi, il mio consiglio è:

Esegui tutte le tue scritture / comandi mentre esporti, con le tue classi logiche e repository per caricare e salvare i dati.

Implementa le query completamente separate dai tuoi comandi. Non riutilizzare la logica, i modelli né i repository. Ci sono molti modi per farlo, ma uno semplice: Definisci un'interfaccia di query come IClassroomQueries con i metodi di query che ti servono (non lasciarlo crescere troppo, non vuoi rompere ISP troppo). Ogni metodo di query dovrebbe aspettarsi una query dto oggetto con i parametri di query (classId per esempio) e dovrebbe restituire un risultato dto con esattamente i campi di cui il consumatore ha bisogno (né più né meno). Se hai consumatori diversi con esigenze diverse, definisci un metodo di query diverso o anche un'interfaccia diversa. Ora sentiti libero di implementare ogni query nel modo migliore per ogni scenario. Alcune query potrebbero utilizzare EF mappato direttamente al DTO. Altri potrebbero mappare i DTO a una vista. Altri potrebbero usare una stored procedure. Le query non riguardano la coerenza o il riutilizzo del codice. Riguardano in modo efficiente il ritorno dei dati giusti.

Mi fermerò qui, ma posso seguire ulteriori dettagli di implementazione se sei interessato.

    
risposta data 20.03.2018 - 22:05
fonte

Leggi altre domande sui tag