Zero oggetti comportamentali in OOP - il mio dilemma progettuale


94

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-pattern.

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 nascoste tra classi e progetti vagamente accoppiati, più le mie classi diventano un mix di dati puri senza classi di comportamento e tutto il comportamento senza classi di dati.

In genere progetto classi in modo da ridurre al minimo la consapevolezza dell'esistenza di altre classi e minimizzare la conoscenza delle interfacce di altre classi. In particolare, lo impongo dall'alto in basso, le classi di livello inferiore non conoscono le classi di livello superiore. Per esempio:

Supponiamo di avere un'API di gioco di carte generale. Hai una lezione Card. Ora questa Cardclasse deve determinare la visibilità per i giocatori.

Un modo è quello di avere boolean isVisible(Player p)in Cardclasse.

Un altro è quello di avere boolean isVisible(Card c)in Playerclasse.

Non mi piace il primo approccio in particolare poiché garantisce la conoscenza di una Playerclasse di livello superiore a una Cardclasse di livello inferiore .

Invece ho optato per la terza opzione in cui abbiamo una Viewportclasse che, dato un Playere un elenco di carte determina quali carte sono visibili.

Tuttavia questo approccio ruba entrambi Carde Playerclassi di una possibile funzione membro. Una volta che lo fai per cose diverse dalla visibilità delle carte, ti rimangono Carde le Playerclassi che contengono dati puramente poiché tutte le funzionalità sono implementate in altre classi, che sono per lo più classi senza dati, solo metodi, come Viewportsopra.

Ciò è chiaramente contrario all'idea principale di OOP.

Qual è il modo corretto? Come dovrei svolgere il compito di ridurre al minimo le interdipendenze tra le classi e minimizzare le conoscenze e l'accoppiamento presunti, ma senza concludere con uno strano design 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 prospettiva sul design di classe che evita l'intero problema?

PS Ecco un altro esempio:

Supponiamo che tu abbia una classe DocumentIdimmutabile, che abbia un solo BigDecimal idmembro e un getter per questo membro. Ora è necessario disporre di un metodo da qualche parte, che ha DocumentIdrestituito Documentquesto ID da un database.

Fai:

  • Aggiungi il Document getDocument(SqlSession)metodo alla DocumentIdclasse, introducendo improvvisamente la conoscenza della 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 il metodo Document getDocument(DocumentId id), lasciando la DocumentIdclasse come morta, nessun comportamento, classe simile a una struttura.

21
Alcune delle tue premesse qui sono completamente sbagliate, il che renderà molto difficile rispondere alla domanda sottostante. Mantieni le tue domande il più concise e privo di opinioni possibile e otterrai risposte migliori.
pdr,

31
"Questo è chiaramente contro l'idea principale di OOP" - no, non lo è, ma è un errore comune.
Doc Brown,

5
Immagino che il problema risieda nel fatto che ci sono state diverse scuole per "Orientamento agli oggetti" in passato - il modo in cui era originariamente inteso da persone come Alan Kay (vedi geekswithblogs.net/theArchitectsNapkin/archive/2013/09/08/ ... ), e il modo in cui è stato insegnato nel contesto di OOA / OOD da quelle persone di Rational ( en.wikipedia.org/wiki/Object-oriented_analysis_and_design ).
Doc Brown,

21
Questa è un'ottima domanda e ben pubblicata - contrariamente ad alcuni altri commenti, direi. Mostra chiaramente quanto la maggior parte dei consigli ingenui o incompleti su come strutturare il programma siano - e quanto sia difficile farlo, e quanto sia irraggiungibile un corretto design in molte situazioni, non importa quanto uno si sforzi di farlo nel modo giusto. E sebbene una risposta ovvia alla domanda specifica siano i multi-metodi, il problema alla base della progettazione persiste.
Thiago Silva,

5
Chi dice che le classi senza comportamento siano un anti-modello?
James Anderson,

Risposte:


42

Quello che descrivi è noto come un modello di dominio anemico . Come con molti principi di progettazione OOP (come la Legge di Demetra ecc.), Non vale la pena chinarsi all'indietro solo per soddisfare una regola.

Non c'è niente di sbagliato nell'avere borse di valori, purché non ingombrino l'intero paesaggio e non si affidino ad altri oggetti per fare le pulizie che potrebbero fare da soli .

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

Ma è davvero un lavoro di Cardsapere a quale Playerè visibile?

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

Sì, puoi provare a inventare una sorta di regola per quello che hai fatto - come Playeressere più alto livello di un Card(?) - ma non è così semplice da indovinare e dovrò cercare in più di un posto per trovare il metodo.

Nel tempo può portare a un marcio compromesso progettuale dell'implementazione isVisibleTosu entrambi Card e sulla Playerclasse, che credo sia un no-no. Perchè così? Perché immagino già il giorno vergognoso in cui player1.isVisibleTo(card1)restituirà un valore diverso da quello card1.isVisibleTo(player1).che penso - è soggettivo - questo dovrebbe essere reso impossibile dal design .

La visibilità reciproca di carte e giocatori dovrebbe essere meglio governata da una sorta di oggetto contestuale, sia esso Viewport, Dealo Game.

Non è uguale ad avere funzioni globali. Dopotutto, potrebbero esserci molti giochi simultanei. Nota che la stessa carta può essere utilizzata contemporaneamente su molti tavoli. Creeremo molte Cardistanze per ogni asso di spade?

Potrei ancora implementare isVisibleTosu Card, ma passare un oggetto contesto ad esso e rendere Carddelegato la query. Programma per interfacciarsi per evitare un elevato accoppiamento.

Come per il tuo secondo esempio - se l'ID documento è costituito solo da a BigDecimal, perché creare una classe wrapper per esso?

Direi che tutto ciò che serve è un DocumentRepository.getDocument(BigDecimal documentID);

A proposito, mentre è assente da Java, ci sono structs in C #.

Vedere

per riferimento. È un linguaggio fortemente orientato agli oggetti, ma nessuno ne ricava molto.


1
Solo una nota sulle strutture in C #: non sono strutture tipiche, come le conoscete in C. In realtà, supportano anche OOP con eredità, incapsulamento e polimorfismo. Oltre ad alcune peculiarità, la differenza principale è come il runtime gestisce le istanze quando vengono passate ad altri oggetti: le strutture sono tipi di valore e le classi sono tipi di riferimento!
Aschratt,

3
@Aschratt: le strutture non supportano l'ereditarietà. Le strutture possono implementare interfacce, ma le strutture che implementano interfacce si comportano diversamente dagli oggetti di classe che funzionano allo stesso modo. Mentre è possibile far sì che le strutture si comportino in qualche modo come oggetti, il miglior caso d'uso per le strutture in cui si desidera qualcosa che si comporti come una struttura a C e le cose che si incapsulano sono primitivi o tipi di classe immutabili.
supercat

1
+1 per il motivo "non è uguale ad avere funzioni globali". Questo non è stato molto affrontato da altri. (Sebbene in presenza di più deck, una funzione globale restituirà comunque valori diversi per istanze distinte della stessa scheda).
alexis,

@supercat Questo è degno di una domanda separata o di una sessione di chat, ma al momento non sono interessato neanche a :-( Dici (in C #) "le strutture che implementano le interfacce si comportano diversamente dagli oggetti di classe che fanno allo stesso modo". Sono d'accordo che ci sono altre differenze comportamentali da considerare, ma AFAIK nel seguente codice Interface iObj = (Interface)obj;, il comportamento di iObjnon è influenzato dallo structo dallo classstato obj(tranne che sarà una copia in scatola in quel compito se è un struct).
Mark Hurd

150

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.

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

Supponiamo di avere un'API di gioco di carte generale. Hai una carta di classe. Ora questa classe di carte deve determinare la visibilità per i giocatori.

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

Un modo è di avere Boolean isVisible (Player p) nella classe Card. Un altro è avere IsVisible (Carta c) booleana nella classe Giocatore.

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

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

Non ho mai giocato a carte con un "viewport" prima d'ora, quindi non ho idea di cosa dovrebbe incapsulare questa classe. Io ho giocato a carte con un paio di mazzi di carte, alcuni giocatori, un tavolo e una copia di Hoyle. Quale di queste cose rappresenta Viewport?

Tuttavia, questo approccio priva entrambe le classi Card e Player di una possibile funzione membro.

Buono!

Una volta che lo fai per altre cose oltre alla visibilità delle carte, ti rimangono le classi Card e Player che contengono dati puramente poiché tutte le funzionalità sono implementate in altre classi, che sono per lo più classi senza dati, solo metodi, come il Viewport sopra. Ciò è chiaramente contrario all'idea principale di OOP.

No; l'idea di base di OOP è che gli oggetti incapsulano le loro preoccupazioni . Nel tuo sistema una carta non è molto preoccupata. Né è un giocatore. Questo perché stai modellando accuratamente il mondo . Nel mondo reale, le proprietà sono le carte che sono rilevanti per un gioco sono estremamente semplici. Potremmo sostituire le immagini sulle carte con i numeri da 1 a 52 senza cambiare molto il gioco. Potremmo sostituire le quattro persone con manichini etichettati 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'intelligenza artificiale non determina se può vedere una carta. Le regole lo determinano .

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 cuori, e poi succede qualcosa, e quindi il giocatore due gioca uno dei sette cuori, il giocatore tre può determinare che erano gli stessi sette 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 regole è ciò che coordina tutto ciò. Le regole producono una sequenza di azioni visibili e informano i giocatori:

  • Giocatore uno, il dieci di cuori ti è stato consegnato 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 the fromp.
  • Giocatore uno, che è un'azione illegale perché un trip triplo produce una mossa indifendibile.
  • Giocatore uno, cosa vuoi fare?
  • Il giocatore uno dice: scarta la regina di picche.
  • Giocatore due, giocatore uno ha scartato la regina di picche.

E così via.

Separare i meccanismi dalle politiche . Le politiche del gioco dovrebbero essere incapsulate in un oggetto politico , non nelle carte . Le carte sono solo un meccanismo.


41
@gnat: Un'opinione contrastante di Alan Kay è "In realtà ho inventato il termine" orientato agli oggetti ", e posso dire che non avevo in mente C ++." Ci sono linguaggi OO senza classi; Mi viene in mente JavaScript.
Eric Lippert,

19
@gnat: Concordo sul fatto che JS così com'è oggi non è un grande esempio di un linguaggio OOP, ma mostra che si potrebbe facilmente costruire un linguaggio OO senza lezioni. Concordo sul fatto che l'unità fondamentale di OO-ness in Eiffel e C ++ sia la classe; dove non sono d'accordo è l'idea che le classi siano la condizione sine qua non di OO. Il presupposto essenziale di OO sono gli oggetti che incapsulano il comportamento e comunicano tra loro tramite un'interfaccia pubblica ben definita.
Eric Lippert,

16
Sono d'accordo w / @EricLippert, le classi non sono fondamentali per OO, né l'eredità, qualunque cosa il mainstream possa dire. L'incapsulamento di dati, comportamento e responsabilità è comunque realizzato. Ci sono linguaggi basati su prototipi oltre Javascript che sono OO ma senza classe. È particolarmente un errore concentrarsi sull'eredità su questi concetti. Detto questo, le classi sono mezzi molto utili per organizzare l'incapsulamento del comportamento. Il fatto di poter trattare le classi come oggetti (e nei linguaggi prototipo, viceversa) rende la linea sfocata.
Schwern,

6
Consideralo in questo modo: quale comportamento mostra una carta reale, nel mondo reale, da sola? Penso che la risposta sia "nessuna". Altre cose agiscono sulla carta. La carta stessa, nel mondo reale, è letteralmente solo informazione (4 di fiori), senza alcun comportamento intrinseco di alcun tipo. Il modo in cui tali informazioni (alias "una carta") vengono utilizzate è al 100% fino a qualcosa / qualcun altro, ovvero le "regole" e i "giocatori". Le stesse carte possono essere usate per un'infinita (beh, forse non del tutto) varietà di giochi diversi, da un numero qualsiasi di giocatori diversi. Una carta è solo una carta e tutto ciò che ha sono proprietà.
Craig,

5
@Montagist: Vorrei chiarire un po 'le cose allora. Considera C. Penso che saresti d'accordo sul fatto che C non abbia classi. Tuttavia, potresti dire che le strutture sono "classi", potresti creare campi di tipo puntatore a funzione, potresti costruire vtables, potresti creare metodi chiamati "costruttori" che impostano le vtables in modo che alcune strutture "ereditate" l'una dall'altra, e così via. È possibile emulare l' eredità di classe in C. E puoi emularla in JS. Ma farlo significa costruire qualcosa in cima al linguaggio che non è già lì.
Eric Lippert,

29

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

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

Sebbene OOP abbia alcuni chiari vantaggi (in particolare rispetto alla semplice programmazione procedurale), è ingenuo lottare per OOP "puro". Alcuni problemi non si associano bene a un approccio orientato agli oggetti e vengono risolti più facilmente da altri paradigmi. Quando si incontra 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 isVisiblerelazione appartiene ad entrambe le classi, a nessuno dei due, o effettivamente: al contesto . I record di comportamento sono tipici di un approccio di programmazione funzionale o procedurale, che sembra essere la soluzione migliore per il tuo problema. Non c'è niente di sbagliato in

    static boolean isVisible(Card c, Player p);
    

    e non c'è nulla di sbagliato nel Cardnon avere metodi oltre ranke suitaccessori.


11
@UMad sì, questo è esattamente il mio punto, e non c'è nulla di sbagliato in questo. Utilizzare il paradigma <del> lingua </del> corretto per il lavoro da svolgere. (A proposito, la maggior parte dei linguaggi oltre a Smalltalk non sono orientati agli oggetti puri. Ad esempio Java, C # e C ++ supportano la programmazione imperativa, strutturata, procedurale, modulare, funzionale e orientata agli oggetti. Tutti quei paradigmi non OO sono disponibili per un motivo : in modo da poterli usare)
amon

1
C'è un modo OO ragionevole per fare Fibonacci, chiamare il fibonaccimetodo su un'istanza di integer. Vorrei sottolineare il tuo punto che OO riguarda l'incapsulamento, anche in luoghi apparentemente piccoli. Lascia che il numero intero capisca come fare il lavoro. Successivamente è possibile migliorare l'implementazione, aggiungere la memorizzazione nella cache per migliorare le prestazioni. A differenza delle funzioni, i metodi seguono i dati in modo che tutti i chiamanti possano beneficiare di una migliore implementazione. Forse gli interi di precisione arbitraria vengono aggiunti in seguito, possono essere trattati in modo trasparente come interi normali e possono avere il proprio fibonaccimetodo ottimizzato per le prestazioni .
Schwern,

2
@Schwern se qualcosa Fibonacciè una sottoclasse della classe astratta Sequence, la sequenza viene utilizzata da qualsiasi set di numeri ed è responsabile della memorizzazione di semi, stato, memorizzazione nella cache e un iteratore.
George Reith,

2
Non mi aspettavo che il "puro OOP Fibonacci" fosse così efficace nel cecchino . Si prega di interrompere qualsiasi discussione circolare al riguardo in questi commenti, sebbene avesse un certo valore di intrattenimento. Ora facciamo tutti qualcosa di costruttivo per un cambiamento!
amon

3
sarebbe sciocco rendere fibonacci un metodo di numeri interi, solo per poter dire che è OOP. È una funzione e dovrebbe essere trattata come una funzione.
user253751

19

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-pattern. (...) Questo è chiaramente contro l'idea principale di OOP.

Questa è una domanda difficile perché si basa su alcune premesse difettose:

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

Non tratterò molto il numero 1-3, perché ognuno potrebbe generare la propria risposta, e questo invita molte discussioni basate sull'opinione. Ma trovo che l'idea di "OOP riguardi l'accoppiamento di dati e comportamenti" sia particolarmente preoccupante. Non solo porta al n. 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 è possibile utilizzare quel tipo. Essere in grado di recuperare il ith elemento è 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 non più di quanto deve essere "crea un nuovo array contenente solo gli elementi pari".

OOP riguarda l'utilizzo di oggetti. Gli oggetti sono solo un modo per raggiungere l'astrazione . L'astrazione è un mezzo per evitare inutili accoppiamenti nel codice, non un fine tutto per sé. Se la tua nozione di carta è definita esclusivamente dal valore della sua suite e grado, va bene implementarla come semplice tupla o record. Non ci sono dettagli non essenziali su cui qualsiasi altra parte del codice possa formare una dipendenza. A volte semplicemente non hai nulla da nascondere.

Non faresti isVisibleun metodo del Cardtipo perché essere visibile non è probabilmente essenziale per la tua idea di una carta (a meno che tu non abbia carte molto speciali che possono diventare traslucide o opache ...). Dovrebbe essere un metodo del Playertipo? Beh, probabilmente non è nemmeno una qualità determinante per i giocatori. Dovrebbe essere una parte di qualche Viewporttipo? Ancora una volta ciò dipende da cosa definisci una finestra e se l'idea di controllare la visibilità delle carte è parte integrante della definizione di una finestra.

È molto possibile che isVisibledovrebbe essere solo una funzione gratuita.


1
+1 per buonsenso invece di ronzii senza cervello.
destra del

Dalle righe che ho letto, il saggio che hai collegato sembra quello di una lettura solida che non ho da un po 'di tempo.
Arthur Havlicek,

@ArthurHavlicek È più difficile da seguire se non capisci le lingue usate nell'esempio di codice, ma l'ho trovato abbastanza illuminante.
Doval,

9

Ovviamente secondo i principi di OOP, gli oggetti che sono solo dati (come le strutture C) sono considerati un anti-pattern.

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

Mentre il tuo livello dati potrebbe eseguire lo spooling di una Playerclasse completa quando legge dalla Playerstabella, potrebbe invece essere solo una libreria di dati generale che restituisce un POD con i campi dalla tabella, che passa a un'altra area del programma che converte un giocatore POD per la tua Playerclasse concreta .

L'uso di oggetti dati, sia tipizzati che non tipizzati, potrebbe non avere senso nel tuo programma, ma ciò non li rende un anti-pattern. Se hanno senso, usali e, se non lo fanno, non farlo.


5
Non essere in disaccordo con qualsiasi cosa tu abbia detto, ma questo non risponde affatto alla domanda. Detto questo, biasimo la domanda più che la risposta.
pdr

2
Esattamente, le carte e i documenti sono solo contenitori di informazioni anche nel mondo reale e qualsiasi "modello" che non può gestirle deve essere ignorato.
JeffO,

1
Plain-Old-Data objects are a perfectly valid pattern Non ho detto che non lo erano, sto dicendo che è sbagliato quando popolano l'intera metà inferiore dell'applicazione.
RokL

8

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

Tuttavia, se non ho il concetto di viewport nel mio gioco, ed è qualcosa di cui penso di aver bisogno perché altrimenti il ​​codice "sembra sbagliato". Penso due volte all'aggiunta del mio modello di dominio.

La parola modello significa che stai costruendo una rappresentazione di qualcosa. Metto in guardia dal mettere in una classe che rappresenti qualcosa di astratto oltre la cosa che stai rappresentando.

Modificherò per aggiungere che è possibile che tu abbia bisogno del concetto di Viewport in un'altra parte del tuo codice, se devi interfacciarti con un display. Ma in termini di DDD questo sarebbe un problema di infrastruttura ed esisterebbe al di fuori del modello di dominio.


La risposta di Lippert sopra è un esempio migliore di questo concetto.
RibaldEddie,

5

Di solito non faccio l'autopromozione, ma il fatto è che ho scritto molto sui problemi di progettazione di OOP sul mio blog . Per riassumere diverse pagine: non dovresti iniziare a progettare con le classi. A partire da interfacce o API e da qui il codice di forma hanno maggiori possibilità di fornire astrazioni significative, adeguare le specifiche ed evitare gonfiori di classi concrete con codice non riutilizzabile.

Come questo si applica al Card- Playerproblema: la creazione di ViewPortun'astrazione ha senso se si pensa Carde Playersi tratta di due librerie indipendenti (che implicherebbe a Playervolte vengono utilizzate senza Card). Tuttavia, sono propenso a pensare a delle Playerprese Cardse dovrebbe fornire loro un Collection<Card> getVisibleCards ()accessore. Entrambe queste soluzioni ( ViewPorte le mie) sono meglio che fornire isVisiblecome metodo Cardo Player, in termini di creazione di relazioni di codice comprensibili.

Una soluzione fuori dalla classe è molto, molto meglio per DocumentId. C'è poca motivazione per far dipendere (sostanzialmente, un numero intero) da una libreria di database complessa.


Mi piace il tuo blog
RokL

3

Non sono sicuro che alla domanda in corso venga data risposta al livello giusto. Ho esortato i saggi nel forum a pensare attivamente al nocciolo della domanda qui.

U Mad sta sollevando una situazione in cui crede che la programmazione secondo la sua comprensione di OOP avrebbe generalmente come risultato che molti nodi foglia sono titolari di dati mentre la sua API di livello superiore comprende la maggior parte del comportamento.

Penso che l'argomento sia diventato leggermente tangente nella definizione di isVisible su Card vs Player; era un semplice esempio illustrato, sebbene ingenuo.

Tuttavia, avevo spinto l'esperienza qui per esaminare il problema. Penso che ci sia una buona domanda per cui U Mad ha spinto. Capisco che spingeresti le regole e la logica in questione a un oggetto a sé stante; ma a quanto ho capito la domanda è

  1. Va bene avere semplici costrutti di dati (classi / strutture; non mi interessa ciò che 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 possiamo incorporare queste parti dei contatori di dati in classi API superiori (incluso il comportamento)

La mia opinione:

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


3
Il problema è che la domanda (e la tua risposta) riguarda come fare le cose "in generale". Il fatto è che non facciamo mai le cose "in generale". Facciamo sempre cose specifiche. È necessario esaminare le nostre cose specifiche e misurarle rispetto ai nostri requisiti al fine di determinare se le nostre cose specifiche sono le cose giuste per la situazione.
John Saunders,

@JohnSaunders Percepisco la tua saggezza qui e sono d'accordo in una certa misura, ma è necessario anche un semplice approccio concettuale prima di affrontare un problema. Dopotutto, la domanda qui non è così aperta come sembra. Penso che sia una valida domanda OOD che qualsiasi designer OO deve affrontare negli usi iniziali di OOP. Qual è la tua opinione? Se una concrezione aiuta, potremmo discutere di costruire un esempio di tua scelta.
Harsha,

Sono fuori dalla scuola da oltre 35 anni. Nel mondo reale, trovo molto poco valore negli "approcci concettuali". Trovo che l'esperienza sia un insegnante migliore di Meyers in questo caso.
John Saunders,

Non capisco davvero la classe per i dati contro la classe per la distinzione del comportamento. Se astratti correttamente i tuoi oggetti, non c'è distinzione. Immagina un Pointcon la getX()funzione. Potresti immaginare che sta ottenendo uno dei suoi attributi, ma potrebbe anche leggerlo dal disco o da Internet. Ottenere e impostare è un comportamento, e avere lezioni che fanno proprio questo va benissimo. I database ottengono e impostano i dati solo prima
Arthur Havlicek,

@ArthurHavlicek: Sapere cosa non farà una lezione è spesso utile quanto sapere cosa farà. Avere qualcosa nel suo contratto che si comporterà come nient'altro che un detentore di dati immutabili condivisibili, o come nient'altro che un detentore di dati mutabili non condivisibili, è utile.
supercat

2

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

Suggerirei che probabilmente sarebbe utile avere un CardEntityoggetto che incapsuli quegli aspetti della carta in componenti separati. Un componente riguarderebbe i segni sulla carta (es. "Diamond King" o "Lava Blast; i giocatori hanno la possibilità di schivare AC-3, oppure subiscono danni 2D6"). Uno potrebbe riguardare un aspetto unico dello stato come la posizione (ad es. È nel mazzo, o nella mano di Joe, o sul tavolo di fronte a Larry). Un terzo potrebbe riguardare può vederlo (forse nessuno, forse un giocatore o forse molti giocatori). Per garantire che tutto sia sincronizzato, i luoghi in cui una carta potrebbe essere non verrebbero incapsulati come campi semplici, ma piuttosto CardSpaceoggetti; per spostare una carta in uno spazio, si darebbe un riferimento al proprioCardSpaceoggetto; si rimuoverà quindi dal vecchio spazio e si inserirà nel nuovo spazio).

Incapsulare esplicitamente "chissà di X" separatamente da "ciò che X sa" dovrebbe aiutare a evitare molta confusione. A volte è necessario prestare attenzione per evitare perdite di memoria, specialmente con molte associazioni (ad es. Se nuove carte possono nascere e vecchie carte scompaiono, è necessario assicurarsi che le carte che dovrebbero essere abbandonate non vengano lasciate perennemente attaccate a oggetti di lunga durata ) ma se l'esistenza di riferimenti a un oggetto costituirà una parte rilevante del suo stato, è del tutto appropriato che l'oggetto stesso incapsuli esplicitamente tali informazioni (anche se delega a un'altra classe il lavoro di gestirlo effettivamente).


0

Tuttavia, questo approccio priva entrambe le classi Card e Player di una possibile funzione membro.

E come va male / sconsigliato?

Per usare un'analogia simile all'esempio delle tue carte, considera a Car, a Drivere devi determinare se Driverpuò guidare il Car.

OK, quindi hai deciso che non vuoi che tu Carsappia se Driverha la chiave della macchina giusta o no, e per qualche ragione sconosciuta hai anche deciso che non vuoi che tu Driversappia della Carclasse (non hai capito bene anche questo nella tua domanda originale). Quindi, hai una classe intermedia, qualcosa sulla falsariga di una Utilsclasse, che contiene il metodo con regole di business al fine di restituire un booleanvalore per la domanda sopra.

Penso che vada bene. La classe intermediaria potrebbe aver bisogno solo di verificare le chiavi dell'auto adesso, ma può essere riformattata per considerare se il conducente ha una patente di guida valida, sotto l'effetto dell'alcool o, in un futuro distopico, verificare la biometria del DNA. Per incapsulamento, non ci sono davvero grossi problemi nel far coesistere queste tre classi.

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.