Strategia per evitare copie difensive mantenendo il corretto incapsulamento

5

Voglio mantenere una dipendenza disaccoppiata, ma allo stesso tempo, una volta passata alla funzione di costruzione, voglio consentire le modifiche solo attraverso Whatever (nell'esempio seguente) perché la modifica della dipendenza dall'esterno invaliderebbe la stato dell'oggetto Whatever che dipende da esso. Quindi tengo una copia, invece:

class Whatever{
    private Dependency d;
    public constructor(Dependency d){
        this.d = d.clone();
    }
    // ...
}

Tuttavia la dipendenza è enorme quindi ho deciso di evitare la copia; Ho rimosso clone() e ho spiegato chiaramente che una volta che la dipendenza viene passata a questa classe, non deve essere modificata dall'esterno.

La domanda è: esiste un modo per evitare la copia e allo stesso tempo mantenere l'incapsulamento corretto? E con l'incapsulamento corretto intendo immediatamente l'errore sui tentativi di accesso dall'esterno o evitando completamente la possibilità di accesso dall'esterno.

La mia ipotesi è che non posso. Non usando l'interpretazione OOP di Java / C # / etc.

Quindi chiedo anche, ci sono lingue che coprono un caso del genere? Come funzionano?

Un'opzione che potrei avere è la seguente, assumendo una lingua che fa conteggio dei riferimenti :

class Whatever{
    private Dependency d;
    public constructor(Dependency d){
        this.d = d.isTheOnlyReference() ? d : d.clone();
    }
    // ...
}

new Whatever(new Dependency()); // no clone performed

Dependency d = new Dependency();
new Whatever(d); // clone performed, as Dependency has >= 1 references 
    
posta Wes 10.01.2017 - 04:24
fonte

4 risposte

8

La condivisione dello stato mutabile non è un male che puoi correggere con la documentazione. Perché? Perché i programmatori sono fantastici, ignorando la documentazione.

Ciò che puoi fare è una copia difensiva, che hai rifiutato per motivi di prestazioni, mantenerla immutabile, che affermi che non puoi, invalidare Whatever quando cambia la dipendenza o non dirlo a nessuno di questa dipendenza quindi nient'altro può cambiarlo.

Conosco due modi per invalidare Whatever :

Rendi Whatever un osservatore. Quando la dipendenza cambia, chiama Whatever.invalidate() in modo che sappia che non può fidarsi del fatto che la sua dipendenza sia la stessa.

Memorizza un contatore di stati. La dipendenza incrementa il suo contatore di stati ogni volta che cambia. Quando Whatever accetta la dipendenza, copia il contatore. Quando Whatever usa la dipendenza controlla che il contatore non sia cambiato. Una volta che ha Whatever sa di non fidarsi più della sua dipendenza.

Questo ti consente di ignorare se la dipendenza è stata modificata.

Se non condividi questa dipendenza, Whatever può controllarne le modifiche. Nient'altro può cambiare la dipendenza perché nient'altro lo sa.

Il conteggio dei riferimenti non aiuta qui. Quello che penso tu stia cercando di fare non è creare cloni extra quando hai solo memorizzato un riferimento ad esso in un unico posto.

Questa è una strategia fattibile. Uno che è più facile da estrarre se le cose che accettano il riferimento non sono responsabili della decisione di clonare.

Diciamo che ho una classe Injector . Injector contiene un riferimento a un'istanza Dependency . Potrebbe essere Injector costruito in primo luogo. Injector non muterà mai Dependency quindi non conta fino alla condivisione.

Injector può passare questa istanza una sola volta senza clonare nulla, Whatever o, eh, qualunque cosa. Dopodiché deve clonare. Ora le cose che accettano il riferimento Dependency non devono preoccuparsi della clonazione e si clona solo quando è necessario.

    
risposta data 10.01.2017 - 07:50
fonte
1

Se Qualunque ha una dipendenza, di solito non si ha la possibilità di consentire modifiche alla dipendenza o meno. Non è possibile richiedere modifiche per passare a Qualunque, in primo luogo perché ciò accoppiare il codice cambiando la dipendenza con Qualunque cosa sia orribile, in secondo luogo perché WhateverElse potrebbe anche avere la stessa dipendenza, e quindi si è bloccati.

Se copi la dipendenza nel costruttore, non puoi reagire alle modifiche, quindi metà del valore è esaurita. Ciò significherebbe di nuovo che quando la dipendenza cambia, qualcuno deve distruggere tutto e crearne uno nuovo. Questo può essere fatto. Spesso è meglio fare Qualunque sia un osservatore e scrivere codice per modificarlo in modo appropriato quando la dipendenza cambia.

    
risposta data 10.01.2017 - 11:21
fonte
1

I want to keep a dependency decoupled, but at the same time, once it's passed to the constructor, I want to allow changes only through Whatever...

Se vuoi che Whatever sia libero di cambiare la dipendenza, ma non cambiare qualcos'altro, allora chiaramente che la dipendenza non è in alcun modo disaccoppiata. Vuoi Whatever per totalizzarlo completamente una volta ricevuto. È così lontano dal disaccoppiamento che puoi ottenere.

So I keep a copy instead ... However the dependency is huge so I've decided to avoid the copy...

Quindi la tua dipendenza deve essere di proprietà di Whatever , ma è enorme, quindi non può essere copiata? Perché la dipendenza è enorme? Whatever usa davvero tutto questo? Se è così, presumibilmente Whatever è troppo grande?

Question is: is there a way to avoid the copy and at the same time maintaining proper encapsulation?

Non puoi incapsulare qualcosa che ti è passato dalla cosa che ti è passata. Quindi se il codice chiamante dà la stessa enorme dipendenza ad altre parti del sistema, non hai incapsulamento.

I can't change Dependency. Also it must be mutable...

Quindi in sintesi:

  1. questa enorme% mutabile% co_de è una classe che non puoi cambiare,
  2. Dependency deve essere in grado di modificare Whatever ,
  3. Ma Dependency è strettamente accoppiato a Whatever e quindi non deve avere altro cambiamento Dependency

Chiedere modi per evitare che Dependency venga modificato al di fuori di Dependency è chiedere un cerotto "cerotto" per mettere su un arto reciso. Il tuo design è seriamente imperfetto: Whatever non dovrebbe essere enorme; Dependency non dovrebbe dipendere da un oggetto mutabile che non deve essere modificato ecc.

Tuttavia, dal suono di ciò, sei bloccato con questo disegno imperfetto. Certo, ci sono modi in cui puoi aggiungere ancora più complessità al sistema per cercare di ridurre le possibilità che altre parti del sistema modificino Whatever , ma non sarà infallibile. Tutto sarà a rischio di fallimento attraverso un cambiamento nel futuro.

Quindi la migliore soluzione a breve termine è davvero quella di documentare questo problema e riconoscere che hai un grosso debito tecnico che dovrebbe essere ripagato il prima possibile ristrutturando il sistema.

    
risposta data 10.01.2017 - 12:25
fonte
1

La soluzione più semplice qui per me sarà un wrapper immutable o copy-on-write su Dependency se puoi almeno permetterti di lavorare con un'istanza avvolta di tutto il sistema.

Entrambi utilizzerebbero qualcosa come il conteggio dei riferimenti o il GC per consentire copie condivise non modificate della risorsa da condividere.

Se usi COW, ad esempio, ogni volta che Whatever memorizza una copia di Dependency , verrà semplicemente memorizzata una copia poco costosa (solo il costo di un puntatore, ad es.). Tuttavia, se qualcosa di esterno tenta di scrivere sul wrapper delle dipendenze, verrà generata una nuova versione dei dati senza influire sulla copia di Whatever's . Una versione immutabile segue la stessa idea.

La versione COW è simile a questa:

void Something::create():
    self.ref_count = new Counter(1)
    self.data = new Data

void Something::copy(other):
    // Just shallow copy the data and increment ref count.
    self.ref_count = ++other.ref_count
    self.data = other.data

void Something::destroy():
    // Only destroy the data we're sharing if the ref count 
    // goes to zero.
    if --self.ref_count == 0:
        destroy self.data
        destroy self.ref_count
    self.ref_count = null
    self.data = null

void Something::write(...):
    // Make a new deep copy of the data with a unique ref
    // count and modify that. Then make this 'Something'
    // refer to the new data and new reference count.
    new_data = new Data(self.data)
    // change new_data
    self.destroy()
    self.data = new_data
    self.ref_count = new Counter(1)

Mentre la versione immutabile sarebbe così:

Something Something::write(...) const:
    // Make a new version of 'Something' with a deep
    // copy of the data and unique ref count. Modify the 
    // deep copy and return the new something.
    new_something = Something::create()
    new_something.data = new Data(self.data)
    // change new_something.data
    return new_something

L'idea generale è di rendere la copia di sporco a basso costo semplicemente rendendola più semplice copiare i dati in cambio di un po 'più di overhead ogni volta che si desidera modificare i dati, a quel punto viene creata una nuova versione dei dati quando vengono richieste modifiche.

Se la logica write è troppo granulare per garantire la creazione di una nuova copia profonda dei dati (e anche potenzialmente di blocco per la sicurezza dei thread) per ogni piccola modifica che è possibile apportare, quindi un progetto immutabile con un "transitorio" "o" builder "è spesso la scelta migliore. Quel "transitorio" o "costruttore" può aggregare più modifiche richieste alla struttura e quindi eseguire il commit di tali modifiche per ottenere una nuova copia immutabile. In genere la maggior parte delle lingue che ho visto hanno i meccanismi appropriati per generalizzare questo tipo di wrapper immutabile in un modo che ti consente di rendere qualsiasi tipo di dati mutabile che desideri immutabile con una versione generalizzata di questo wrapper.

    
risposta data 16.12.2017 - 22:15
fonte