Zero oggetti comportamentali in OOP - il mio dilemma di design

90

L'idea alla base di OOP è che i dati e il comportamento (su quei dati) sono inseparabili e sono accoppiati all'idea di un oggetto di una classe. L'oggetto ha dati e metodi che funzionano con quello (e altri dati). Ovviamente secondo i principi di OOP, gli oggetti che sono solo dati (come le strutture C) sono considerati un anti-modello.

Fin qui tutto bene.

Il problema è che ho notato che il mio codice sembra andare sempre più nella direzione di questo anti-pattern ultimamente. Mi sembra che più cerco di ottenere informazioni che nascondono tra classi e disegni liberamente accoppiati, più le mie classi diventano un mix di dati puri, nessuna classe di comportamento e tutti i comportamenti non classi di dati.

Generalmente disegno le classi in un modo che minimizza la loro consapevolezza dell'esistenza di altre classi e riduce al minimo la loro conoscenza delle interfacce di altre classi. In particolare, lo impongo in modo top-down, le classi di livello inferiore non conoscono le classi di livello superiore. Per esempio:.

Supponiamo che tu abbia un'API generale di giochi di carte. Hai una classe Card . Ora questa classe Card deve determinare la visibilità dei giocatori.

Un modo è avere boolean isVisible(Player p) su Card class.

Un altro è avere boolean isVisible(Card c) su Player class.

Non mi piace il primo approccio in particolare in quanto garantisce la conoscenza della classe di livello superiore Player a una classe di Card di livello inferiore.

Ho optato invece per la terza opzione in cui abbiamo una classe Viewport che, dato un Player e un elenco di carte determina quali carte sono visibili.

Tuttavia questo approccio ruba entrambe le classi Card e Player di una possibile funzione membro. Una volta fatto questo per altre cose oltre alla visibilità delle carte, rimani con Card e Player classi che contengono puramente dati poiché tutte le funzionalità sono implementate in altre classi, che sono per lo più classi senza dati, solo metodi, come il Viewport sopra.

Questo è chiaramente contro l'idea principale di OOP.

Qual è il modo corretto? Come dovrei andare sul compito di ridurre al minimo le interdipendenze di classe e ridurre al minimo la conoscenza e l'accoppiamento presunte, ma senza finire con il design strano in cui tutte le classi di basso livello contengono solo dati e le classi di alto livello contengono tutti i metodi? Qualcuno ha una terza soluzione o una prospettiva sulla progettazione della classe che evita l'intero problema?

P.S. Ecco un altro esempio:

Supponiamo di avere classe DocumentId che è immutabile, ha solo un membro BigDecimal id e un getter per questo membro. Ora devi avere un metodo da qualche parte, che ha dato un DocumentId restituisce Document per questo ID da un database.

Fai:

  • Aggiungi il metodo Document getDocument(SqlSession) alla classe DocumentId , introducendo improvvisamente conoscenza sulla tua persistenza ( "we're using a database and this query is used to retrieve document by id" ), l'API utilizzata per accedere a DB e simili. Anche questa classe ora richiede il file JAR di persistenza solo per la compilazione.
  • aggiungi un'altra classe con metodo Document getDocument(DocumentId id) , lasciando DocumentId class come dead, no behavior, struct-like class.
posta U Mad 02.04.2014 - 14:23
fonte

10 risposte

42

Ciò che descrivi è noto come modello di dominio anemico . Come con molti principi di progettazione OOP (come Law of Demeter ecc.), Non vale la pena piegarsi all'indietro solo per soddisfare una regola.

Niente di sbagliato nell'avere sacchi di valori, purché non ingombrino l'intero paesaggio e non facciano affidamento su altri oggetti per fare le pulizie che potrebbe fare da soli .

Sarebbe certamente un odore di codice se avessi una classe separata solo per modificare le proprietà di Card - se ci si potrebbe ragionevolmente aspettare che si prenda cura di loro da solo.

Ma è davvero un lavoro di un Card sapere quale% diPlayer è visibile a?

E perché implementare Card.isVisibleTo(Player p) , ma non Player.isVisibleTo(Card c) ? O vice versa?

Sì, puoi provare a creare una sorta di regola per quello che hai fatto, ad esempio Player è più alto di Card (?), ma non è così semplice da indovinare e io Dovremo cercare in più di un posto per trovare il metodo.

Nel corso del tempo può portare a un compromesso sul design scadente implementando isVisibleTo su entrambi Card e Player class, che credo sia un no-no. Perchè così? Perché immagino già il giorno vergognoso quando player1.isVisibleTo(card1) restituirà un valore diverso da card1.isVisibleTo(player1). Penso che sia soggettivo: ciò dovrebbe essere reso impossibile di design .

La visibilità reciproca delle carte e dei giocatori dovrebbe essere governata da una sorta di oggetto di contesto - sia esso Viewport , Deal o Game .

Non è uguale ad avere funzioni globali. Dopotutto, ci possono essere molti giochi concorrenti. Si noti che la stessa carta può essere utilizzata contemporaneamente su molti tavoli. Creeremo molte istanze di Card per ogni asso di spade?

Potrei ancora implementare isVisibleTo su Card , ma passare un oggetto contesto ad esso e rendere Card delegare la query. Programma per interfacciare per evitare un accoppiamento elevato.

Come per il tuo secondo esempio - se l'ID del documento è composto solo da un BigDecimal , perché creare una classe wrapper per questo?

Direi che tutto ciò di cui hai bisogno è un DocumentRepository.getDocument(BigDecimal documentID);

A proposito, mentre assenti da Java, ci sono struct s in C #.

Vedi

per riferimento. È un linguaggio altamente orientato agli oggetti, ma nessuno ne fa un grosso problema.

    
risposta data 02.04.2014 - 15:52
fonte
145

The basic idea behind OOP is that data and behavior (upon that data) are inseparable and they are coupled by the idea of an object of a class.

Stai facendo l'errore comune di presumere che le classi siano un concetto fondamentale in OOP. Le classi sono solo un modo particolarmente popolare per ottenere l'incapsulamento. Ma possiamo permettere che scivolino.

Suppose you have a general card game API. You have a class Card. Now this Card class needs to determine visibility to players.

BUONI PARADISCHI NO. Quando giochi a Bridge fai chiedi ai sette di cuori quando è il momento di cambiare la mano del morto da un segreto noto solo al manichino per essere conosciuto da tutti? Ovviamente no. Non è affatto una preoccupazione per la carta.

One way is to have boolean isVisible(Player p) on Card class. Another is to have boolean isVisible(Card c) on Player class.

Entrambi sono orridi; non fare nessuno di quelli. Né il giocatore né la carta sono responsabili dell'attuazione delle regole di Bridge!

Instead I opted for the third option where we have a Viewport class which, given a Player and a list of cards determines which cards are visible.

Non ho mai giocato a carte con una "vista" prima, quindi non ho idea di cosa questa classe debba incapsulare. Ho ho giocato carte con un paio di mazzi di carte, alcuni giocatori, un tavolo e una copia di Hoyle. Quale di queste cose rappresenta Viewport?

However this approach robs both Card and Player classes of a possible member function.

Bene!

Once you do this for other stuff than visibility of cards, you are left with Card and Player classes which contain purely data as all functionality is implemented in other classes, which are mostly classes with no data, just methods, like the Viewport above. This is clearly against the principal idea of OOP.

No; l'idea di base di OOP è che gli oggetti incapsulino le loro preoccupazioni . Nel tuo sistema una carta non è molto preoccupata. Neanche un giocatore. Questo perché stai modellando accuratamente il mondo . Nel mondo reale, le proprietà sono carte che sono rilevanti per un gioco sono estremamente semplici. Potremmo sostituire le immagini sulle carte con i numeri da 1 a 52 senza modificare molto il gioco del gioco. Potremmo sostituire le quattro persone con manichini etichettati come Nord, Sud, Est e Ovest senza cambiare molto il gioco. I giocatori e le carte sono le cose più semplici nel mondo dei giochi di carte. Le regole sono ciò che è complicato, quindi la classe che rappresenta le regole è dove dovrebbe essere la complicazione.

Ora, se uno dei tuoi giocatori è un'intelligenza artificiale, il suo stato interno potrebbe essere estremamente complicato. Ma quell'IA non determina se può vedere una carta. Le regole determinano che .

Ecco come progetterei il tuo sistema.

Prima di tutto, le carte sono sorprendentemente complicate se ci sono giochi con più di un mazzo. Devi considerare la domanda: i giocatori possono distinguere tra due carte dello stesso valore? Se il giocatore uno gioca uno dei sette di cuori, e poi succede qualcosa, e poi il giocatore due gioca uno dei sette di cuori, il giocatore tre può determinare che erano gli stessi sette di cuori? Consideralo attentamente. Ma a parte questa preoccupazione, le carte dovrebbero essere molto semplici; sono solo dati.

Quindi, qual è la natura di un giocatore? Un giocatore consuma una sequenza di azioni visibili e produce un'azione .

L'oggetto rules è ciò che coordina tutto questo. Le regole producono una sequenza di azioni visibili e informano i giocatori:

  • Giocatore uno, i dieci di cuori ti sono stati dati dal giocatore tre.
  • Giocatore due, una carta è stata consegnata al giocatore uno dal giocatore tre.

E poi chiede al giocatore un'azione.

  • Giocatore uno, cosa vuoi fare?
  • Il giocatore uno dice: treble theop.
  • Giocatore uno, si tratta di un'azione illegale perché un triplo di p produce un gambale indifendibile.
  • Giocatore uno, cosa vuoi fare?
  • Il giocatore uno dice: scarta la regina di picche.
  • Giocatore due, il giocatore uno ha scartato la regina di picche.

E così via.

Separa i tuoi meccanismi dalle tue norme . Le norme del gioco devono essere incapsulate in un oggetto della politica , non nelle carte . Le carte sono solo un meccanismo.

    
risposta data 02.04.2014 - 19:40
fonte
28

Hai ragione nel dire che l'accoppiamento di dati e comportamenti è l'idea centrale di OOP, ma c'è dell'altro. Ad esempio, incapsulamento : la programmazione OOP / modulare ci consente di separare un'interfaccia pubblica dai dettagli dell'implementazione. In OOP ciò significa che i dati non dovrebbero mai essere accessibili al pubblico e dovrebbero essere utilizzati solo tramite gli accessor. Con questa definizione, un oggetto senza alcun metodo è davvero inutile.

Una classe che non offre metodi al di fuori degli accessor è essenzialmente una struttura complicata. Ma questo non è male, perché OOP ti dà la flessibilità di modificare i dettagli interni, che una struttura non ha. Ad esempio, invece di memorizzare un valore in un campo membro, potrebbe essere ricalcolato ogni volta. Oppure viene modificato un algoritmo di backup e con esso lo stato di cui tenere traccia.

Sebbene l'OOP abbia alcuni chiari vantaggi (specialmente per quanto riguarda la programmazione procedurale semplice), è ingenuo cercare il "puro" OOP. Alcuni problemi non si adattano bene ad un approccio orientato agli oggetti e sono risolti più facilmente con altri paradigmi. Quando incontri un problema del genere, non insistere su un approccio inferiore.

  • Considera di calcolare la sequenza di Fibonacci in modo orientato agli oggetti . Non riesco a pensare a un modo sano per farlo; la semplice programmazione strutturata offre la migliore soluzione a questo problema.

  • La tua relazione isVisible appartiene a entrambe le classi, oa nessuna delle due, o in realtà: al contesto . I record comportamentali sono tipici di un approccio di programmazione funzionale o procedurale, che sembra essere il più adatto al tuo problema. Non c'è niente di sbagliato in

    static boolean isVisible(Card c, Player p);
    

    e non c'è nulla di sbagliato in Card senza metodi oltre rank e suit accessors.

risposta data 02.04.2014 - 14:59
fonte
19

The basic idea behind OOP is that data and behavior (upon that data) are inseparable and they are coupled by the idea of an object of a class. Object have data and methods that work with that (and other data). Obviously by the principles of OOP, objects that are just data (like C structs) are considered an anti-pattern. (...) This is clearly against the principal idea of OOP.

Questa è una domanda difficile perché si basa su alcuni presupposti difettosi:

  1. L'idea che OOP sia l'unico modo valido per scrivere codice.
  2. L'idea che l'OOP sia un concetto ben definito. È diventata una parola d'ordine che è difficile trovare due persone che possano essere d'accordo su cosa sia l'OOP.
  3. L'idea che OOP riguardi il raggruppamento di dati e comportamenti.
  4. L'idea che tutto sia / dovrebbe essere un'astrazione.

Non toccherò molto il n. 1-3, perché ognuno potrebbe generare la propria risposta e invita molte discussioni basate sull'opinione pubblica. Ma trovo che l'idea di "OOP riguarda l'accoppiamento di dati e comportamenti" sia particolarmente preoccupante. Non solo porta a # 4, ma porta anche all'idea che tutto dovrebbe essere un metodo.

C'è una differenza tra le operazioni che definiscono un tipo e i modi in cui puoi usare quel tipo. Essere in grado di recuperare l'elemento i th è essenziale per il concetto di un array, ma l'ordinamento è solo una delle molte cose che posso scegliere di fare con uno. L'ordinamento non deve essere un metodo più di quanto non debba essere "creare un nuovo array contenente solo gli elementi pari".

OOP riguarda l'uso degli oggetti. Gli oggetti sono solo un modo per ottenere l'astrazione . L'astrazione è un mezzo per evitare l'accoppiamento non necessario nel codice, non un fine a se stesso. Se la tua nozione di una carta è definita esclusivamente dal valore della sua suite e del suo rango, va bene implementarla come semplice tupla o registrazione. Non ci sono dettagli non essenziali su cui qualsiasi altra parte del codice possa formare dipendenza. A volte non hai niente da nascondere.

Non crei isVisible un metodo del tipo Card perché essere visibile non è probabilmente essenziale per la tua nozione di una carta (a meno che tu non abbia carte molto speciali che possono diventare traslucide o opache ...). Dovrebbe essere un metodo del tipo Player ? Beh, probabilmente non è nemmeno una definizione dei giocatori. Dovrebbe essere parte di un certo tipo Viewport ? Ancora una volta ciò dipende da cosa si definisce una finestra e se la nozione di controllare la visibilità delle carte è parte integrante della definizione di una vista.

È molto possibile che isVisible sia solo una funzione gratuita.

    
risposta data 02.04.2014 - 15:56
fonte
9

Obviously by the principles of OOP, objects that are just data (like C structs) are considered an anti-pattern.

No, non lo sono. Gli oggetti Plain-Old-Data sono un modello perfettamente valido e li aspetterei in qualsiasi programma che tratti i dati che devono essere persistenti o comunicati tra aree distinte del programma.

Mentre il tuo potrebbe livello di dati spoolare una classe Player completa quando viene letto dalla tabella Players , potrebbe essere semplicemente una libreria di dati generica che restituisce un POD con campi dalla tabella, che passa ad un'altra area del tuo programma che converte un POD giocatore nella tua classe Player concreta.

L'uso di oggetti dati, sia tipizzati che non tipizzati, potrebbe non avere senso nel programma, ma questo non li rende anti-pattern. Se hanno senso, usali e, in caso contrario, non farlo.

    
risposta data 02.04.2014 - 14:37
fonte
8

Personalmente ritengo che Domain Driven Design contribuisca a chiarire questo problema. La domanda che chiedo è: come descrivo il gioco di carte agli esseri umani? In altre parole, cosa sto modellando? Se la cosa che sto modellando include effettivamente la parola "viewport" e un concetto che corrisponde al suo comportamento, allora creerei l'oggetto viewport e fargli fare ciò che dovrebbe logicamente.

Tuttavia, se non ho il concetto di viewport sul mio gioco, ed è qualcosa che penso di cui ho bisogno perché altrimenti il codice "si sente sbagliato". Ci penso due volte prima di aggiungerlo al mio modello di dominio.

Il modello di parole significa che stai costruendo una rappresentazione di qualcosa. Evito di inserire in una classe che rappresenta qualcosa di astratto oltre la cosa che stai rappresentando.

Verrà modificato per aggiungere che è possibile che tu possa aver bisogno del concetto di un Viewport in un'altra parte del tuo codice, se hai bisogno di interfacciarti con un display. Ma in termini DDD questo sarebbe un problema di infrastruttura e esisterebbe al di fuori del modello di dominio.

    
risposta data 02.04.2014 - 18:46
fonte
5

Di solito non faccio auto-promozione, ma il fatto è che ho scritto molto sui problemi di progettazione di OOP su il mio blog . Per riassumere diverse pagine: non si dovrebbe iniziare la progettazione con le classi. A partire da interfacce o API e il codice di forma da lì hanno maggiori possibilità di fornire astrazioni significative, specifiche di adattamento ed evitare gonfiore di classi concrete con codice non riutilizzabile.

Come si applica a Card - Player problema: la creazione di un'astrazione ViewPort ha senso se si pensa a Card e Player come a due librerie indipendenti (il che implicherebbe Player a volte è usato senza Card ). Tuttavia, sono propenso a pensare che Player detenga Cards e debba fornire ad essi un Collection<Card> getVisibleCards () accessor. Entrambe queste soluzioni ( ViewPort e mia) sono migliori rispetto a fornire isVisible come metodo di Card o Player , in termini di creazione di relazioni di codice comprensibili.

Una soluzione esterna alla classe è molto, molto meglio per DocumentId . C'è poca motivazione a fare (in pratica, un numero intero) dipende da una libreria di database complessa.

    
risposta data 02.04.2014 - 21:07
fonte
3

Non sono sicuro che la domanda in questione venga esaudita al giusto livello. Ho esortato i saggi del forum a pensare attivamente al nucleo della domanda qui.

U Mad sta sollevando una situazione in cui crede che la programmazione secondo la sua comprensione di OOP comporterebbe in genere un sacco di nodi foglia che sono titolari di dati mentre la sua API di livello superiore comprende la maggior parte del comportamento.

Penso che l'argomento sia andato leggermente in tangenza sul fatto che sia visibile Visible su Card vs Player; era un semplice esempio illustrato, anche se ingenuo.

Avevo spinto l'esperto qui a guardare il problema a portata di mano però. Penso che ci sia una buona domanda per cui U Mad ha insistito. Capisco che avresti spinto le regole e la logica in questione a un suo proprio oggetto; ma come capisco la domanda è

  1. Va bene avere semplici costrutti di data holder (classi / structs, non mi interessa cosa sono modellati come per questa domanda) che non offrono davvero molte funzionalità?
  2. Se sì, qual è il modo migliore o preferito per modellarli?
  3. In caso negativo, come incorporare queste parti del contatore dei dati in classi API più elevate (incluso il comportamento)

La mia vista:

Penso che tu stia facendo una domanda di granularità difficile da ottenere nella programmazione orientata agli oggetti. Nella mia piccola esperienza, non includerei un'entità nel mio modello che non includa alcun comportamento da solo. Se devo, probabilmente ho usato un costrutto una struttura progettata per contenere un'astrazione simile a una classe che ha l'idea di incapsulare dati e comportamenti.

    
risposta data 03.04.2014 - 06:35
fonte
2

Una fonte comune di confusione in OOP deriva dal fatto che molti oggetti racchiudono due aspetti dello stato: le cose che conoscono e le cose che le conoscono. Le discussioni sullo stato degli oggetti spesso ignorano quest'ultimo aspetto, poiché in strutture in cui i riferimenti agli oggetti sono promiscui non esiste un modo generale per determinare quali cose potrebbero sapere su qualsiasi oggetto il cui riferimento sia mai stato esposto al mondo esterno.

Suggerirei che sarebbe probabilmente utile avere un oggetto CardEntity che incapsuli quegli aspetti della carta in componenti separati. Un componente si riferirebbe ai segni sulla carta (ad es. "Diamond King" o "Lava Blast; i giocatori hanno AC-3 possibilità di schivare, altrimenti subiscono 2D6 danni"). Uno potrebbe riguardare un aspetto unico di stato come la posizione (ad esempio è nel mazzo o nella mano di Joe o sul tavolo di fronte a Larry). Un terzo potrebbe riguardare per vederlo (forse nessuno, forse un giocatore, o forse molti giocatori). Per garantire che tutto sia tenuto sincronizzato, i posti in cui una carta potrebbe essere non sarebbero incapsulati come semplici campi, ma piuttosto CardSpace oggetti; per spostare una carta in uno spazio, si darebbe un riferimento all'oggetto CardSpace corretto; si rimuoverebbe quindi dal vecchio spazio e si inserirà nel nuovo spazio).

L'incapsulamento esplicito di "chi conosce X" separatamente da "ciò che X sa" dovrebbe aiutare a evitare un sacco di confusione. Talvolta è necessario prestare attenzione per evitare perdite di memoria, specialmente con molte associazioni (ad esempio se nuove carte possono nascere e le vecchie carte scompaiono, bisogna assicurarsi che le carte che dovrebbero essere abbandonate non rimangano perennemente attaccate a oggetti longevi ) ma se l'esistenza di riferimenti a un oggetto formerà una parte rilevante del suo stato, è del tutto corretto che l'oggetto stesso incorpori esplicitamente tali informazioni (anche se delega a qualche altra classe il lavoro di gestirlo effettivamente).

    
risposta data 02.04.2014 - 19:16
fonte
0

However this approach robs both Card and Player classes of a possible member function.

E come va male / a male?

Per usare un'analogia simile al tuo esempio di carte, considera Car , Driver e devi determinare se Driver può guidare Car .

OK, quindi hai deciso che non vuoi che il tuo Car sappia se Driver ha la chiave giusta o meno, e per qualche motivo sconosciuto hai anche deciso di non volere il tuo Driver per conoscere la classe Car (non l'hai pienamente spiegato nella tua domanda originale). Quindi, hai una classe intermedia, qualcosa sulla falsariga di una classe Utils , che contiene il metodo con regole aziendali per restituire un valore boolean per la domanda di cui sopra.

Penso che vada bene. La classe intermediario potrebbe solo aver bisogno di verificare le chiavi dell'auto ora, ma può essere sottoposta a refactoring per valutare se il conducente ha una patente di guida valida, sotto l'influenza dell'alcool o in un futuro distopico, controllare la biometria del DNA. Con l'incapsulamento, non ci sono davvero grossi problemi nel far coesistere queste tre classi.

    
risposta data 03.04.2014 - 05:31
fonte

Leggi altre domande sui tag