Un oggetto dovrebbe essere in grado di rappresentare se stesso attraverso i componenti?

4

Sto lavorando a un gioco in rete. Ho provato a progettare i vari componenti (client, server, engine) solo su cose all'interno del loro dominio. Ad esempio, il server dovrebbe occuparsi solo della gestione dei pacchetti e delle connessioni, non delle entità all'interno del motore di gioco o della gestione degli eventi hardware sul lato client. Non sono stato in grado di trovare un buon modo per unire insieme questi domini in modo pulito, perché alla fine, un'entità che si muove nel mondo del gioco alla fine deve essere rappresentata all'utente in qualche modo. Questa è una domanda correlata, ma forse non è il nucleo di questa.

Con questo in mente, ho osservato alcune delle mie classi e mi sono chiesto se fossero progettate in modo pulito.

Un esempio che ho è una classe Party , che rappresenta un gruppo di Pokemon (animali con cui combattere). Il mio progetto iniziale assomigliava a

// Max number of Pokemon is 6.
public interface Party {

    Set<Pokemon> getPokemon();

    void add(Pokemon pokemon);

    void remove(Pokemon pokemon);

    boolean contains(Pokemon pokemon);

    boolean contains(Species species);

    int getNumberOfPokemon();
}

La maggior parte dei metodi nell'interfaccia espongono lo stato di Party . Ci si aspettava che il codice client verificasse se il numero di Pokemon all'interno del party fosse inferiore a 6, e in tal caso, era permesso a add per il party; oltre a verificare che ci fossero almeno 2 nella parte prima che venisse chiamato remove . L'ho semplificato un po 'nel tentativo di esporre meno dello stato dell'oggetto, e mi è rimasto con

public interface Party {

    boolean hasRoom();

    void add(Pokemon pokemon);

    void remove(Pokemon pokemon);
}

Un Party viene utilizzato in molti posti tra i domini del codice. Ad esempio, c'è una vista sul lato client che mostra all'utente ciò che è nella loro Party . È anche memorizzato in un repository.

La classe% co_de dovrebbe sapere come inviarsi al client e al repository?

public interface Party {

    // ...
    void saveTo(PartyRepository repo);

    void showUITo(Client client);
}

Certo, farlo in questo modo mantiene lo stato della festa bloccato all'interno dell'oggetto, ma poi corri il rischio di avere molti oggetti quasi-dio, che sanno tutto di come vengono utilizzati.

Un'alternativa che avevo pensato era quella di dividere Party in due tipi distinti, uno specificamente progettato per rappresentare lo stato e uno per rappresentare il comportamento. Ma vedere come Party è solo una collezione di Pokemon, sarebbe davvero qualcosa di simile a

public interface PartyDTO {

    Set<Pokemon> getMembers();
}

public interface PartyBehavior {

    void add(Pokemon pokemon);
}

Che comunque non sembra giusto perché allora quale è il Party ? Continuerò a manipolare lo stato della classe Party attraverso la classe PartyDTO , quindi non mi sembra di guadagnare davvero qualcosa.

È qualcosa che dovrebbe essere fatto? Quali alternative ci sono?

    
posta Zymus 09.04.2016 - 07:28
fonte

4 risposte

1

Ciò che stai affrontando qui è la tensione tra due questioni: il buon incapsulamento e il principio di responsabilità unica. Se Party deve esporre il suo stato interiore a tutti, è scarsamente incapsulato con tutti gli effetti usuali che ha (in questo caso, che include la violazione di Tell, Do not Ask ). Ma se mantiene il suo stato incapsulato, solo allora ha l'accesso necessario per salvare quello stato.

Ma questo non è sempre irrisolvibile. Ad esempio, per una festa che puoi aggiungere fino a un limite, puoi provare un progetto come questo:

public interface Party {
    Iterable<Pokemon> getMembers();
    Iterable<EmptySlot> getSlots(); // This could also be Optional<EmptySlot> getSlot();
}

public interface EmptySlot {
    void Fill(Pokemon member);
}

Ora, quando vuoi salvare il tuo gruppo, puoi getMembers e passarlo al repository. Quando si desidera aggiungere un membro al gruppo, si getSlots , quindi selezionarne uno e riempirlo. Non c'è modo di aggirare questo per creare una parte non valida, nessuna necessità di gestire le eccezioni, restituire codici di stato / indicatori di successo o metodi di convalida speciali.

    
risposta data 15.04.2016 - 18:30
fonte
4

Prima di tutto, dominio in design basato sul dominio ( DDD più avanti ) non significa che dividi la tua applicazione in livelli, che sono solo responsabili di operazioni specifiche. Il dominio in DDD è il livello che contiene le regole aziendali e che è completamente ignaro di qualsiasi altra cosa nell'app. Che si tratti di persistenza o presentazione, il dominio non dovrebbe preoccuparsi di come viene salvato o visualizzato.

Questo contraddice direttamente il suggerimento che hai fatto, che il Party dovrebbe sapere come salvare e mostrare se stesso, e tu contro a destra subito dopo (non avere oggetti di dio è sicuramente un buon approccio).

Immagina di avere una ciotola che rappresenta un livello di persistenza (un database se lo desideri), che ha mele (oggetti di dominio). Aggiungere una mela alla ciotola equivale a salvarlo in un database e rimuovere un oggetto dalla ciotola equivale a cancellarlo dal database.

Che cosa ha più senso?

  1. Quando vuoi aggiungere una mela alla ciotola, porti l'intera ciotola contenente milioni di mele alla mela e poi aggiungi la mela alla ciotola.
  2. Porta la mela nella ciotola e aggiungila ad essa.

Un sacco di volte, l'approccio alla programmazione è diverso da quello che vedresti nel mondo reale, qui, in realtà non lo è. Un repository funziona avendo un elemento passato e il repository sa che farlo.

Inoltre, se hai aggiunto la logica persistente direttamente alla classe di dominio, ora potresti essere limitato a un solo modo di salvare l'oggetto (distruggendo l'oggetto in tipi di dati scalari e passandolo nel repository) o delegato l'oggetto completo, this , al repository, nel qual caso non avrebbe senso passare il repository in primo luogo, perché si potrebbe anche fare qualcosa come repository.Save(PartyObject) .

Most of the methods within the interface expose the Party's state. The client code was expected to check if the number of Pokemon within the party is less than 6, and if so, it was allowed to add to the party; as well as checking that there were at least 2 in the party before remove was called.

Il design iniziale presentava dei difetti, la seconda versione promossa dell'interfaccia Party è migliore, purché sia effettivamente necessario estrarre il valore hasRoom dall'oggetto (ad esempio per il livello di presentazione).

Se non lo fai, allora anche quel metodo è necessario e l'interfaccia potrebbe essere semplice come questa:

public interface Party 
{
    // throws NoMoreRoomException when the party is full
    // throws PokemonAlreadyExistsException
    void add(Pokemon pokemon);

    // throws PokemonNotFoundException
    // throws MinimumNumberOfPokemonsReachedException when you try to remove
    // a pokemon from the party which already contains the minimum required
    // number of pokemons
    void remove(Pokemon pokemon);
}

Un'implementazione di questa interfaccia indicherà quindi le regole. Potresti avere un TwoToSixParty e SevenToTventyOneParty , entrambi implementando l'interfaccia Party e generando eccezioni quando il settimo o il 22esimo Pokemon dovevano essere aggiunti alla raccolta o quando si cercava di rimuovere una Pokemon dal parte già contenente solo due o sette Pokemon, rispettivamente.

One alternative I had thought of was to split the Party into two separate types, one specifically designed to represent the state, and one to represent behavior.

Questo è un disegno perfettamente valido. Il PartyDTO sarà un oggetto stateless, con getter e setter per tutte le sue proprietà e sarà l'oggetto che fornisce il livello intermedio tra il tuo dominio e un metodo permanente concreto (sia esso il salvataggio di un modello di dominio - nel qual caso un modello di dominio è decostruito per formare il DTO, o usando il DTO per costruire l'oggetto dominio).

Il DTO non ha bisogno di regole, perché il dominio stesso assicura che tutte le combinazioni di dati che sono passati al DTO siano in uno stato valido.

Tutto sommato, alla fine dovrai comunque accedere ai valori dei modelli di dominio, devi stamparli per l'utente e per questo avrai bisogno di getter. Ma i getter dovrebbero essere usati solo per leggere i valori dagli oggetti del dominio, non si dovrebbe mai leggere un valore dell'oggetto, valutarne il valore e modificare l'oggetto in base al valore appena valutato. Questa è la logica della classe da cui hai ricavato le informazioni e dovrebbe esserci al suo interno.

Anche allora ricorda, non tutte le pratiche "raccomandate" sono effettivamente valide per tutti i progetti. Se stavo costruendo una piccolissima applicazione che dovevo implementare in modo rapido, in un giorno o due, probabilmente non mi dispiacerebbe il schema di registrazione attivo , anche se di solito sono strongmente contrario.

Non c'è un approccio buono per ogni cosa nella programmazione, quasi tutto ha i suoi pro e contro e devi decidere se vuoi passare il tempo a creare 8 nuove interfacce solo così puoi astrarre la persistenza di un oggetto, o se hai solo bisogno per farlo.

    
risposta data 09.04.2016 - 11:40
fonte
2

"Should the Party class know how to send itself to the client, and repository?"

È difficile rispondere alla tua domanda perché stai scrivendo la tua applicazione in uno stile procedurale piuttosto che orientato agli oggetti, nonostante i tag di progettazione orientati agli oggetti e basati sul dominio. In un programma orientato agli oggetti il tuo oggetto Party vincolerebbe il numero di Pokemon a 6, non il codice cliente. Il tuo cliente non chiederà il numero in Party prima che il cliente decida di aggiungerne uno.

Tuttavia, lasciatemi rispondere assumendo che tu fossi un approccio orientato agli oggetti: -

"Should the Party class know how to send itself to the client, and repository?"

No. Un oggetto Party dovrebbe solo conoscere e fare cose legate all'essere una parte di Pokemon. Non dovrebbe sapere nulla di persistenza, interazione umana o interazione con altri sistemi. Un PartyPepitory, d'altro canto, sa come rappresentare una parte in termini di interazione con un archivio persistente. Ad esempio, il tuo PartyPository potrebbe sapere come salvare il tuo Party.

Se vuoi salvare una festa, invia un messaggio (chiama un metodo) a PartyRepository con la Parte da salvare.

    
risposta data 14.04.2016 - 07:47
fonte
0

Should the Party class know how to send itself to the client, and repository?

Suggerirei che non dovrebbe sapere di nessuno di questi.

Il tuo oggetto Party rappresenta qualcosa nel tuo modello di dominio, quindi non dovrebbe davvero essere necessario conoscere la rete.

Allo stesso modo se il tuo repository è utilizzato per memorizzare Party oggetti allora Party non dovrebbe sapere nulla del suo proprietario / custode, tuttavia il tuo Repository potrebbe ragionevolmente avere un metodo AddParty .

One alternative I had thought of was to split the Party into two separate types, one specifically designed to represent the state, and one to represent behavior.

That still doesn't feel right though because then which one is the Party? I'd still be manipulating the state of the PartyDTO class through the PartyBehavior class, so I don't feel like I'm really gaining anything.

Invece di pensare in termini di "suddivisione del comportamento e dello stato", prendi in considerazione la scomposizione della serializzazione di rete da oggetti di dominio .

I DTO sono usati per comunicare i dati attraverso un confine, che è un problema che devi risolvere per la comunicazione client / server; il tuo oggetto dominio Party non dovrebbe aver bisogno di sapere nulla sulla rete o sulla serializzazione.

Un DTO potrebbe essere utile, ma solo per la serializzazione dei dati sul client e non come un oggetto "stato" lato server. La creazione di un nuovo PartyDTO non implicherebbe la modifica di nulla sul tuo oggetto dominio Party o sulla separazione di stato e comportamento. Quota sotto dalla pagina di wikipedia su DTO:

Data transfer object (DTO), formerly known as value objects or VO, is a design pattern used to transfer data between software application subsystems. DTOs are often used in conjunction with data access objects to retrieve data from a database.

The difference between data transfer objects and business objects or data access objects is that a DTO does not have any behaviour except for storage and retrieval of its own data (accessors and mutators).

-- https://en.wikipedia.org/wiki/Data_transfer_object

Nel tuo scenario, il tuo server potrebbe creare un'istanza di PartyDTO basata su Party ogni volta che è necessario pubblicare un aggiornamento sul client, ma l'oggetto DTO sarebbe di breve durata per la rete di invio / pubblicazione operazione.

Il vantaggio di scrivere un DTO è la separazione tra l'oggetto dominio Party sul server e gli aggiornamenti di rete ai client. La classe Party non si preoccuperebbe della comunicazione di rete e il tuo cliente non avrebbe bisogno di alcuna conoscenza della vera classe Party , che sarebbe nascosta nel server.

Il codice cliente sarebbe libero di definire il proprio codice specifico del client per gestire showOnUI in base al contenuto di PartyDTO in una classe solo client.

Allo stesso modo, le tue classi client / server di rete potrebbero aver bisogno di send / receive o publish / subscribe metodi per PartyDTO - questo potrebbe essere completamente disaccoppiato da Party tuttavia.

es. nella tua classe ServerApp , o qualunque altra classe incolli il tuo codice di rete nel tuo modello di dominio:

public class ServerApp {
    public void SendUpdates() {
        foreach (Party p : partyRepository.getParties()) {
            PartyDTO dto = mapper.mapTo<PartyDTO>(p);
            networkServer.sendUpdate(dto);
        }
    }
}

Lo svantaggio è la necessità di scrivere codice di mappatura degli oggetti per generare il DTO da Party , che è noioso e molto "hot plate" se lo scrivi interamente a mano, ma puoi togliere la maggior parte di quel dolore con una libreria come ModelMapper .

    
risposta data 09.04.2016 - 13:03
fonte

Leggi altre domande sui tag