Sto infrangendo la pratica OOP con questa architettura?


23

Ho un'applicazione web. Non credo che la tecnologia sia importante. La struttura è un'applicazione di livello N, mostrata nell'immagine a sinistra. Ci sono 3 livelli.

UI (modello MVC), Business Logic Layer (BLL) e Data Access Layer (DAL)

Il problema che ho è che il mio BLL è enorme in quanto ha la logica e i percorsi attraverso la chiamata degli eventi dell'applicazione.

Un flusso tipico attraverso l'applicazione potrebbe essere:

L'evento generato nell'interfaccia utente, attraversa un metodo nel BLL, esegue la logica (possibilmente in più parti del BLL), infine al DAL, di nuovo al BLL (dove probabilmente più logica) e quindi restituisce un valore all'interfaccia utente.

Il BLL in questo esempio è molto impegnato e sto pensando a come dividerlo. Ho anche la logica e gli oggetti combinati che non mi piacciono.

inserisci qui la descrizione dell'immagine

La versione a destra è il mio sforzo.

La logica è ancora il modo in cui l'applicazione scorre tra UI e DAL, ma probabilmente non ci sono proprietà ... Solo metodi (la maggior parte delle classi in questo livello potrebbe essere statica in quanto non memorizzano alcuno stato). Il livello Poco è dove esistono le classi che hanno proprietà (come una classe Person dove ci sarebbero nome, età, altezza ecc.). Questi non avrebbero nulla a che fare con il flusso dell'applicazione, immagazzinano solo lo stato.

Il flusso potrebbe essere:

Anche attivato dall'interfaccia utente e passa alcuni dati al controller di livello dell'interfaccia utente (MVC). Questo traduce i dati grezzi e li converte nel modello poco. Il modello poco viene quindi passato al livello Logic (che era il BLL) e infine al livello query dei comandi, potenzialmente manipolato lungo il percorso. Il livello di query Command converte il POCO in un oggetto database (che sono quasi la stessa cosa, ma uno è progettato per la persistenza, l'altro per il front-end). L'elemento viene archiviato e un oggetto database viene restituito al livello Query comandi. Viene quindi convertito in un POCO, dove ritorna al livello Logic, potenzialmente elaborato ulteriormente e, infine, torna all'interfaccia utente

La logica e le interfacce condivise sono dove possiamo avere dati persistenti, come MaxNumberOf_X e TotalAllowed_X e tutte le interfacce.

Sia la logica / interfacce condivise che DAL sono la "base" dell'architettura. Questi non sanno nulla del mondo esterno.

Tutto sa poco oltre alle logiche / interfacce condivise e DAL.

Il flusso è ancora molto simile al primo esempio, ma ha reso ogni livello più responsabile di 1 cosa (sia esso stato, flusso o qualsiasi altra cosa) ... ma sto rompendo OOP con questo approccio?

Un esempio per demo di Logic e Poco potrebbe essere:

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method1(PocoB pocoB) 
    { 
        return cmdQuery.Save(pocoB); 
    }

    /*This has no state objects, only ways to communicate with other 
    layers such as the cmdQuery. Everything else is just function 
    calls to allow flow via the program */
    public PocoA Method2(PocoB pocoB) 
    {         
        pocoB.UpdateState("world"); 
        return Method1(pocoB);
    }

}

public struct PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     public int DataC {get;set;}

    /*This simply returns something that is part of this class. 
     Everything is self-contained to this class. It doesn't call 
     trying to directly communicate with databases etc*/
     public int GetValue()
     {

         return DataB * DataC; 
     }

     /*This simply sets something that is part of this class. 
     Everything is self-contained to this class. 
     It doesn't call trying to directly communicate with databases etc*/
     public void UpdateState(string input)
     {        
         DataA += input;  
     }
}

Non vedo nulla di fondamentalmente sbagliato nella tua architettura come l'hai attualmente descritta.
Robert Harvey,

19
Nell'esempio di codice non sono disponibili dettagli funzionali sufficienti per fornire ulteriori approfondimenti. Gli esempi di Foobar raramente forniscono un'illustrazione sufficiente.
Robert Harvey,


4
Possiamo trovare un titolo migliore per questa domanda in modo che possa essere trovato online più facilmente?
Soner Gönül,

1
Solo per essere pedanti: un livello e un livello non sono la stessa cosa. Un "livello" parla della distribuzione, un "livello" sulla logica. Il livello dati verrà distribuito su entrambi i livelli lato server e database. Il tuo livello di interfaccia utente verrà distribuito su entrambi i livelli web-client e server-side-code. L'architettura che mostri è un'architettura a 3 strati. I livelli sono "Client Web", "Codice lato server" e "Database".
Laurent LA RIZZA,

Risposte:


54

Sì, molto probabilmente stai rompendo i concetti OOP fondamentali . Tuttavia, non sentirti male, la gente lo fa sempre, non significa che la tua architettura sia "sbagliata". Direi che è probabilmente meno gestibile di un corretto design OO, ma questa è piuttosto soggettiva e non è comunque la tua domanda. ( Ecco un mio articolo che critica l'architettura a più livelli in generale).

Ragionamento : il concetto più elementare di OOP è che dati e logica formano una singola unità (un oggetto). Anche se questa è un'affermazione molto semplicistica e meccanica, anche così, non è davvero seguita nel tuo design (se ti capisco correttamente). Stai separando abbastanza chiaramente la maggior parte dei dati dalla maggior parte della logica. Avere metodi apolidi (simili a statici), ad esempio, è chiamato "procedure" e sono generalmente antitetici a OOP.

Ovviamente ci sono sempre delle eccezioni, ma questo progetto viola di regola queste cose.

Ancora una volta, vorrei sottolineare "viola OOP"! = "Sbagliato", quindi questo non è necessariamente un giudizio di valore. Tutto dipende dai vincoli dell'architettura, dai casi d'uso di manutenibilità, dai requisiti, ecc.


9
Abbi un voto, questa è una buona risposta, se stessi scrivendo il mio, lo copierei e incollerei, ma aggiungo anche che, se ti accorgi che non stai scrivendo il codice OOP, forse dovresti considerare un linguaggio non OOP in quanto viene fornito con un sacco di spese extra di cui puoi fare a meno se non lo stai utilizzando
TheCatWhisperer

2
@TheCatWhisperer: le architetture aziendali moderne non eliminano del tutto OOP, solo selettivamente (ad es. Per DTO).
Robert Harvey,

@RobertHarvey D'accordo, intendevo dire che se non usi OOP quasi ovunque nel tuo progetto
TheCatWhisperer,

@TheCatWhisperer molti dei vantaggi in una oop come c # non sono necessariamente nella parte oop della lingua ma nel supporto disponibile come librerie, visual studio, gestione della memoria ecc.

@Orangesandlemons Sono sicuro che ci sono molte altre lingue ben supportate là fuori ...
TheCatWhisperer

31

Uno dei principi fondamentali della programmazione funzionale sono le funzioni pure.

Uno dei principi fondamentali della programmazione orientata agli oggetti è mettere insieme le funzioni con i dati su cui agiscono.

Entrambi questi principi fondamentali scompaiono quando l'applicazione deve comunicare con il mondo esterno. In effetti, puoi essere fedele a questi ideali solo in uno spazio appositamente preparato nel tuo sistema. Non tutte le righe del codice devono soddisfare questi ideali. Ma se nessuna riga del tuo codice soddisfa questi ideali, non puoi davvero affermare di utilizzare OOP o FP.

Quindi è OK avere solo "oggetti" di dati su cui lanciarsi perché sono necessari per attraversare un confine che semplicemente non è possibile rifattorizzare per spostare il codice interessato. Sappi solo che non è OOP. Questa è la realtà. OOP è quando, una volta dentro quel confine, raccogli tutta la logica che agisce su quei dati in un unico posto.

Non che tu debba farlo neanche. OOP non è tutto per tutti. È quello che è. Non pretendere che qualcosa segua OOP quando non lo fa o confonderai le persone che cercano di mantenere il tuo codice.

I tuoi POCO sembrano avere una logica di business perfetta, quindi non mi preoccuperei troppo di essere anemico. Quello che mi preoccupa è che sembrano tutti molto mutevoli. Ricorda che getter e setter non forniscono un vero incapsulamento. Se il tuo POCO è diretto a quel servizio allora va bene. Basta capire che questo non ti dà tutti i vantaggi di un vero oggetto OOP incapsulato. Alcuni lo chiamano un Data Transfer Object o DTO.

Un trucco che ho usato con successo è creare oggetti OOP che mangiano DTO. Uso il DTO come oggetto parametro . Il mio costruttore legge lo stato da esso (leggi come copia difensiva ) e lo butta da parte. Ora ho una versione completamente incapsulata e immutabile del DTO. Tutti i metodi relativi a questi dati possono essere spostati qui a condizione che si trovino su questo lato di tale limite.

Non fornisco getter o setter. Seguo dire, non chiedere . Chiami i miei metodi e vanno a fare ciò che deve fare. Probabilmente non ti dicono nemmeno cosa hanno fatto. Lo fanno e basta.

Ora alla fine qualcosa, da qualche parte si imbatterà in un altro confine e tutto questo cadrà di nuovo a pezzi. Va bene. Spin up un altro DTO e lanciarlo sul muro.

Questa è l'essenza dell'architettura delle porte e degli adattatori. Ne ho letto da una prospettiva funzionale . Forse ti interesserà anche te.


5
" Getter e setter non forniscono un vero incapsulamento " - sì!
Boris the Spider,

3
@BoristheSpider - Getter e setter forniscono assolutamente l'incapsulamento, semplicemente non si adattano alla tua definizione ristretta di incapsulamento.
Assapora Ždralo il

4
@ DavorŽdralo: occasionalmente sono utili come soluzione alternativa, ma per loro stessa natura, getter e setter rompono l'incapsulamento. Fornire un modo per ottenere e impostare alcune variabili interne è l'opposto dell'essere responsabile per il proprio stato e per agire su di esso.
cHao,

5
@cHao - non capisci cos'è un getter. Non significa un metodo che restituisce un valore di una proprietà dell'oggetto. È un'implementazione comune, ma può restituire un valore da un database, richiederlo su http, calcolarlo al volo, qualunque cosa. Come ho detto, getter e setter rompono l'incapsulamento solo quando le persone usano le proprie definizioni ristrette (e errate).
Assapora Ždralo il

4
@cHao - incapsulamento significa che stai nascondendo l'implementazione. Questo è ciò che viene incapsulato. Se hai un geter "getSurfaceArea ()" su una classe Square, non sai se l'area della superficie è un campo, se è calcolata al volo (altezza di ritorno * larghezza) o un terzo metodo, quindi puoi cambiare l'implementazione interna ogni volta che vuoi, perché è incapsulato.
Assapora Ždralo il

1

Se leggo correttamente la tua spiegazione, i tuoi oggetti sembrano un po 'così: (difficile senza contesto)

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method(PocoB pocoB) { ... }
}

public class PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     ... etc
}

In quanto le tue classi Poco contengono solo dati e le tue classi Logic contengono i metodi che agiscono su quei dati; sì, hai infranto i principi di "Classic OOP"

Ancora una volta, è difficile dirlo dalla tua descrizione generalizzata, ma rischierei che ciò che hai scritto possa essere classificato come Anemic Domain Model.

Non penso che questo sia un approccio particolarmente negativo, né, se consideri il tuo Poco come delle strutture, rompe OOP in modo più specifico nel senso più specifico. In quanto i tuoi oggetti ora sono le LogicClasses. In effetti, se rendi immutabile il tuo Pocos, il design potrebbe essere considerato abbastanza funzionale.

Tuttavia, quando fai riferimento a Shared Logic, Pocos che sono quasi ma non uguali e statica, comincio a preoccuparmi dei dettagli del tuo design.


Ho aggiunto al mio post, essenzialmente copiando il tuo esempio. Spiacenti, non era chiaro per cominciare
MyDaftQuestions,

1
ciò che intendo è che se ci dicessi che cosa fa l'applicazione sarebbe più facile scrivere esempi. Invece di LogicClass potresti avere PaymentProvider o qualsiasi altra cosa
Ewan

1

Un potenziale problema che ho visto nel tuo progetto (ed è molto comune) - alcuni dei peggiori codici "OO" che abbia mai incontrato sono stati causati da un'architettura che separava gli oggetti "Dati" dagli oggetti "Codice". Questa è roba da incubo! Il problema è che ovunque nel tuo codice aziendale quando vuoi accedere ai tuoi oggetti dati TI TENETE semplicemente a codificarlo proprio lì in linea (Non è necessario, è possibile creare una classe di utilità o un'altra funzione per gestirlo, ma questo è ciò che Ho visto accadere ripetutamente nel tempo).

Il codice di accesso / aggiornamento non viene generalmente raccolto, quindi si finisce con funzionalità duplicate ovunque.

D'altro canto, tali oggetti dati sono utili, ad esempio come persistenza del database. Ho provato tre soluzioni:

Copiare valori dentro e fuori da oggetti "reali" e buttare via il tuo oggetto dati è noioso (ma può essere una soluzione valida se vuoi andare in quel modo).

L'aggiunta di metodi di wrangling dei dati agli oggetti dati può funzionare, ma può creare un grosso oggetto dati disordinato che sta facendo più di una cosa. Può anche rendere l'incapsulamento più difficile poiché molti meccanismi di persistenza vogliono accessi pubblici ... Non l'ho adorato quando l'ho fatto ma è una soluzione valida

La soluzione che ha funzionato meglio per me è il concetto di una classe "Wrapper" che incapsula la classe "Dati" e contiene tutte le funzionalità di wrangling dei dati - quindi non espongo affatto la classe di dati (Neanche setter e getter a meno che non siano assolutamente necessari). Ciò elimina la tentazione di manipolare direttamente l'oggetto e ti costringe invece ad aggiungere funzionalità condivise al wrapper.

L'altro vantaggio è che puoi assicurarti che la tua classe di dati sia sempre in uno stato valido. Ecco un breve esempio di psuedocode:

// Data Class
Class User {
    String name;
    Date birthday;
}

Class UserHolder {
    final private User myUser // Cannot be null or invalid

    // Quickly wrap an object after getting it from the DB
    public UserHolder(User me)
    {
        if(me == null ||me.name == null || me.age < 0)
            throw Exception
        myUser=me
    }

    // Create a new instance in code
    public UserHolder(String name, Date birthday) {
        User me=new User()
        me.name=name
        me.birthday=birthday        
        this(me)
    }
    // Methods access attributes, they try not to return them directly.
    public boolean canDrink(State state) {
        return myUser.birthday.year < Date.yearsAgo(state.drinkingAge) 
    }
}

Nota che non hai il controllo dell'età distribuito su tutto il codice in diverse aree e anche che non sei tentato di usarlo perché non riesci nemmeno a capire quale sia il compleanno (a meno che non ti serva per qualcos'altro, in quale caso puoi aggiungerlo).

Tendo a non estendere semplicemente l'oggetto dati perché perdi questo incapsulamento e la garanzia di sicurezza - a quel punto potresti anche aggiungere i metodi alla classe di dati.

In questo modo la tua logica aziendale non ha un sacco di junk / iteratori di accesso ai dati distribuiti su di essa, diventa molto più leggibile e meno ridondante. Consiglio anche di prendere l'abitudine di avvolgere sempre le raccolte per lo stesso motivo: mantenere loop / ricerche di costrutti fuori dalla logica aziendale e assicurarsi che siano sempre in buono stato.


1

Non cambiare mai il codice perché pensi o qualcuno ti dice che non è questo o no. Cambia il tuo codice se ti dà problemi e hai trovato un modo per evitare questi problemi senza crearne altri.

Quindi a parte il fatto che non ti piacciono le cose, vuoi investire molto tempo per fare un cambiamento. Scrivi i problemi che hai in questo momento. Annota come il tuo nuovo design potrebbe risolvere i problemi. Scopri il valore del miglioramento e il costo per apportare le modifiche. Quindi - e questo è molto importante - assicurati di avere il tempo di completare quei cambiamenti, o finirai per metà in questo stato, per metà in quello stato, e questa è la peggiore situazione possibile. (Una volta ho lavorato a un progetto con 13 diversi tipi di stringhe e tre sforzi identificabili a metà per standardizzare un tipo)


0

La categoria "OOP" è molto più ampia e astratta di quello che stai descrivendo. Non importa di tutto questo. Si preoccupa di chiare responsabilità, coesione, accoppiamento. Quindi, a livello che stai chiedendo, non ha molto senso chiedere "Pratica OOPS".

Detto questo, al tuo esempio:

Mi sembra che ci sia un malinteso sul significato di MVC. Stai chiamando la tua UI "MVC", separatamente dalla tua logica aziendale e dal controllo "backend". Ma per me, MVC include l'intera applicazione Web:

  • Modello: contiene i dati aziendali + la logica
    • Livello dati come dettaglio di implementazione del modello
  • Visualizza: codice UI, modelli HTML, CSS ecc.
    • Include aspetti sul lato client come JavaScript o le librerie per applicazioni Web "a una pagina" ecc.
  • Controllo: la colla lato server tra tutte le altre parti
  • (Ci sono estensioni come ViewModel, Batch ecc. In cui non entrerò, qui)

Ci sono alcune ipotesi di base estremamente importanti qui:

  • Una classe / oggetti del Modello non ha mai alcuna conoscenza di nessuna delle altre parti (Visualizza, Controllo, ...). Non li chiama mai, non presume che siano chiamati da loro, non ottiene attributi / parametri di sesssion o qualsiasi altra cosa lungo questa linea. È completamente solo. Nelle lingue che lo supportano (ad es. Ruby), puoi attivare una riga di comando manuale, creare un'istanza di classi Model, lavorare con esse sul contenuto del tuo cuore e fare tutto ciò che fanno senza alcuna istanza di Control o View o qualsiasi altra categoria. Non ha alcuna conoscenza di sessioni, utenti, ecc., Soprattutto.
  • Nulla tocca il livello dati se non attraverso un modello.
  • La vista ha solo un leggero tocco sul modello (visualizzazione di elementi, ecc.) E nient'altro. (Si noti che una buona estensione è "ViewModel" che sono classi speciali che eseguono un'elaborazione più sostanziale per il rendering dei dati in modo complicato, che non si adatterebbe bene né al Modello né alla Vista - questo è un buon candidato per rimuovere / evitare il gonfiamento nel modello puro).
  • Il controllo è il più leggero possibile, ma è responsabile della raccolta di tutti gli altri giocatori e del trasferimento di elementi tra loro (ad esempio, estrarre le voci degli utenti da un modulo e inoltrarlo al modello, inoltrare le eccezioni dalla logica aziendale a un utile messaggi di errore per l'utente, ecc.). Per le API Web / HTTP / REST ecc., Tutte le autorizzazioni, la sicurezza, la gestione delle sessioni, la gestione degli utenti ecc. Avvengono qui (e solo qui).

È importante sottolineare che l'interfaccia utente fa parte di MVC. Non viceversa (come nel diagramma). Se lo accetti, i modelli grassi sono in realtà piuttosto buoni, a condizione che in realtà non contengano roba che non dovrebbero.

Si noti che "modelli fat" indica che tutta la logica aziendale è nella categoria Modello (pacchetto, modulo, qualunque sia il nome nella lingua scelta). Le singole classi dovrebbero ovviamente essere strutturate in modo OOP in modo ottimale secondo le linee guida di codifica fornite dall'utente (ovvero alcune righe di codice massime per classe o per metodo, ecc.).

Si noti inoltre che l'implementazione del livello dati ha conseguenze molto importanti; soprattutto se il livello del modello è in grado di funzionare senza un livello dati (ad esempio, per test unitari o per DB in memoria a buon mercato sul laptop dello sviluppatore anziché costosi DB Oracle o qualsiasi altra cosa possiate avere). Ma questo è davvero un dettaglio di implementazione a livello di architettura che stiamo esaminando in questo momento. Ovviamente qui vuoi ancora avere una separazione, cioè non vorrei vedere un codice che ha una logica di dominio pura direttamente interlacciata con l'accesso ai dati, accoppiandolo intensamente insieme. Un argomento per un'altra domanda.

Per tornare alla tua domanda: mi sembra che ci sia una grande sovrapposizione tra la tua nuova architettura e lo schema MVC che ho descritto, quindi non stai sbagliando completamente, ma sembra che tu stia reinventando alcune cose, o usandolo perché il tuo attuale ambiente di programmazione / librerie suggeriscono tale. Difficile dirlo per me. Quindi non posso darti una risposta esatta sul fatto che ciò che intendi sia particolarmente buono o cattivo. Puoi scoprire controllando se ogni singola "cosa" ha esattamente una classe responsabile per essa; se tutto è altamente coesivo e poco accoppiato. Questo ti dà una buona indicazione ed è, secondo me, sufficiente per un buon design OOP (o un buon benchmark dello stesso, se vuoi).

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.