Design di incapsulamento relativo

2

Diciamo che sto facendo un'applicazione 2D con il seguente design:

C'è l'oggetto Livello che gestisce il mondo, e ci sono oggetti del mondo che sono entità all'interno dell'oggetto Livello.

Un oggetto del mondo ha una posizione e velocità, oltre a dimensioni e una trama. Tuttavia, un oggetto mondo espone solo% proprietà% di%. Le proprietà get sono private (o protette) e sono disponibili solo per le classi ereditate.

Ma ovviamente Level è responsabile di questi oggetti del mondo e deve in qualche modo essere in grado di manipolare almeno alcuni dei suoi setter privati. Ma al momento, Level non ha accesso, il che significa che gli oggetti del mondo devono cambiare i suoi setter privati in pubblico (violando l'incapsulamento).

Come affrontare questo problema? Dovrei semplicemente rendere tutto pubblico?

Attualmente quello che sto facendo è avere una classe interna all'interno dell'oggetto del gioco che fa funzionare set . Quindi, quando Level ha bisogno di aggiornare la posizione di un oggetto, va qualcosa del tipo:

void ChangeObject(GameObject targetObject, int newX, int newY){
 // targetObject.SetX and targetObject.SetY cannot be set directly
 var setter = new GameObject.Setter(targetObject);
 setter.SetX(newX); 
 setter.SetY(newY); 
}

Questo codice sembra eccessivo, ma non sembra giusto avere tutto pubblico in modo che tutto possa cambiare la posizione di un oggetto, per esempio.

    
posta taher1992 18.04.2014 - 19:15
fonte

5 risposte

2

Che ne dici di utilizzare le interfacce?

public interface IImmutableWorld
{
    // only get methods
}

public interface IMutableWorld : IImmutableWorld
{
    // add set methods
}

public class World : IMutableWorld
{
    // implementation for both get and set methods.
}

Non hai bisogno di parlare direttamente con World classe , parli con le interfacce .

Ad esempio, Level parlerà con IMutableWorld e può usare i suoi metodi impostati. Altre classi parleranno con IImmutableWorld , quindi non hanno accesso ai metodi di impostazione.

Se sei ancora preoccupato che qualcuno possa creare un'istanza Mondo e manipolarlo direttamente, puoi utilizzare Fabbrica .

public class WorldFactory
{
    IImmutableWorld createImmutableWorld()
    {
        // return World instance
    }

    IMutableWorld createMutableWorld()
    {
        // return World instance
    }
}

Crea Mondo come classe interna di WorldFacotry , ora, le altre classi non conoscono l'esistenza di Mondo . Li costringe a gestire le interfacce.

    
risposta data 17.10.2014 - 18:56
fonte
0

L'idea è di limitare quali oggetti possono modificare un altro oggetto. Ci sono due modi per raggiungere questo obiettivo.

  1. Dimenticalo e lascia che qualcuno modifichi gli oggetti. Basta non chiamare le funzioni mutanti dal livello esterno e andare avanti.

  2. Usa una funzione linguistica per raggiungere questo obiettivo. Ad esempio, in C ++ è possibile utilizzare la parola chiave friend per consentire a Livello più accesso. In Java, è possibile utilizzare la visibilità privata del pacchetto e inserire quegli oggetti (e solo quegli oggetti) che devono accedere allo stesso pacchetto.

Personalmente penso che un design chiaro e pulito sia davvero bello da avere. Tuttavia, il mondo reale lo colpisce a volte e rende necessario battere quel piolo quadrato nel buco rotondo. Mentre può essere una buona pratica OO per limitare chi può mutare o usare un oggetto, forse l'opzione 1 è il modo facile e veloce. Forse l'opzione 2 è migliore.

Se questo codice non sarà usato da altri (ad esempio plugin), allora forse l'opzione 1 è più veloce e più semplice. Se sarà usato ed esteso dal codice degli altri, allora l'opzione due sarebbe meglio.

    
risposta data 18.04.2014 - 20:08
fonte
0

Una soluzione semplice sarebbe renderli immutabili e produrre nuovi oggetti del mondo per il prossimo stato mondiale. Ciò fornisce al Level il controllo completo sullo stato del mondo senza consentire agli oggetti di interferire direttamente tra loro.

Un altro vantaggio di questo approccio è che funziona ancora anche se gli oggetti del tuo mondo smettono di essere puri e hanno una IA interna. Gli oggetti possono "aggiornare" il loro stato restituendo una copia di se stesso con la posizione / velocità modificata, e il mondo è ancora libero di prendere quella copia e "alterarla" ulteriormente per risolvere collisioni o applicare forze globali (es. Gravità, vento). Poiché gli oggetti sono immutabili, puoi anche dividere il lavoro di calcolo delle nuove posizioni su più thread.

Infine, se hai mai bisogno di implementare una sorta di meccanismo "salva stato" o "riavvolgi tempo" lo ottieni gratuitamente, perché non stai distruggendo i vecchi stati. Dovrai farlo se vuoi avere un comportamento deterministico e un'animazione fluida.

Il comportamento deterministico richiede che tu aggiorni il mondo in incrementi di tempo fissi (al contrario di quanto tempo è passato dall'ultimo aggiornamento.) Per mantenere il gameplay sincronizzato con il tempo reale devi tenere traccia del anche in eccesso (es. aggiorni in intervalli di 0.05s, ma è stato 0.07 secondi - hai un eccesso di 0.02 che non puoi gettare via.) Alla fine quel tempo in eccesso si accumula abbastanza per produrre un secondo aggiornamento mondiale nella stessa iterazione di il ciclo di gioco, che causa l'animazione periodica della balbuzie. La soluzione è mantenere lo stato del mondo precedente e interpolare le posizioni tra lo stato precedente e quello attuale per appianare nuovamente l'animazione. Vedi Fix Your Timestep! per ulteriori informazioni.

    
risposta data 18.04.2014 - 19:59
fonte
0

This code feels like overkill, but it doesn't feel right to have everything public so that anything can change an objects location for example.

L'incapsulamento è parte del problema, ma direi che dovresti progettare oggetti con servizi ai clienti (responsabilità) che sono più astratti dei semplici metodi (imposta e ottiene).

setLocation(x,y) è un esempio di un servizio . Rivela quel servizio solo ai clienti che dovrebbero essere in grado di farlo. Se x e y sono cose che potresti cambiare (e quindi vuoi nascondere), astratele in un oggetto Location , ad esempio setLocation(new Location(x,y)) . L'implementazione di Location potrebbe effettivamente essere un angolo e una distanza dall'origine. Potresti avere due modi per accedere alle posizioni, theta + distanza o x, y (è solo un esempio).

Un altro motivo per pensare ai servizi è che puoi evitare che gli oggetti si trovino in uno stato incoerente tra i setter. Ad esempio, se un client fa set(x) , non riesce a chiamare set(Y) , la posizione dell'oggetto potrebbe essere incoerente. L'inserimento dell'operazione in un servizio ( setLocation ) consente di applicare regole di coerenza sullo stato dell'oggetto.

Infine, uno scopo importante di nascondere le cose dall'esterno con l'incapsulamento è che ti sarà permesso cambiarle in un secondo momento. Se sei sicuro al 100%, nulla cambierà, renderà tutto pubblico. Questa è generalmente una cattiva idea, perché in realtà le cose fanno cambiano. Qualsiasi cosa renderai pubblica sarà probabilmente un mal di testa più tardi se cambierà (le tue classi client potrebbero interrompersi).

TL; DR I progettisti devono progettare due aspetti di ogni classe: la parte pubblica e la parte privata . Rendere la parte pubblica come servizi utili ai clienti (classi che useranno la classe in fase di progettazione).

Ulteriori informazioni in Hiding the implementation chapter di Pensare in Java di Bruce Eckel.

    
risposta data 19.04.2014 - 20:11
fonte
0

Questo ha una risposta chiara: Interfacce (o varianti di essi in lingue che non li supportano).

Gli oggetti possono implementare varie interfacce, che consentono al livello di manipolarli. Ad esempio, potrebbe esserci un'interfaccia IMovable , che consente all'oggetto di cambiare la sua posizione. Quindi, se hai utilizzato is e as operatori, puoi selezionare gli oggetti mobili e operare su di essi. Questo ha molti vantaggi. Il primo è che non è necessario per gli oggetti stessi implementare tali interfacce. Potresti avere wrapper attorno a quegli oggetti, che implementano interfacce di livello, ottenendo un disaccoppiamento più elevato. Si potrebbe anche usare l'implementazione esplicita dell'interfaccia di C # per "nascondere" tali interfacce, quindi i loro metodi non vengono chiamati "accidentalmente".

    
risposta data 18.08.2014 - 08:34
fonte