Classificare con membri mutabili durante la creazione ma immutabili in seguito


22

Ho un algoritmo che crea una raccolta di oggetti. Questi oggetti sono mutabili durante la creazione, poiché iniziano con pochissimo, ma poi vengono popolati con dati in luoghi diversi all'interno dell'algoritmo.

Una volta completato l'algoritmo, gli oggetti non devono mai essere modificati, tuttavia vengono consumati da altre parti del software.

In questi scenari, è buona norma disporre di due versioni della classe, come descritto di seguito?

  • Il mutevole viene quindi creato dall'algoritmo
  • Al completamento dell'algoritmo, i dati vengono copiati in oggetti immutabili che vengono restituiti.

3
Potresti modificare la tua domanda per chiarire qual è il tuo problema / domanda?
Simon Bergot,

Potresti anche essere interessato a questa domanda: Come congelare un ghiacciolo in .NET (rendere immutabile una classe)
R0MANARMY

Risposte:


46

Puoi forse usare il modello del costruttore . Utilizza un oggetto 'builder' separato con lo scopo di raccogliere i dati necessari e quando tutti i dati vengono raccolti crea l'oggetto reale. L'oggetto creato può essere immutabile.


La tua soluzione è stata quella giusta per le mie esigenze (non tutte sono state dichiarate). All'indagine gli oggetti restituiti non hanno tutte le stesse caratteristiche. La classe builder ha molti campi nullable e quando i dati sono stati creati, sceglie quale delle 3 diverse classi generare: queste sono correlate tra loro per eredità.
Paul Richards,

2
@Paul In tal caso, se questa risposta ha risolto il tuo problema, dovresti contrassegnarlo come accettato.
Riking

24

Un modo semplice per raggiungere questo obiettivo sarebbe avere un'interfaccia che consenta di leggere le proprietà e chiamare solo i metodi di sola lettura e una classe che implementa quell'interfaccia che consente anche di scrivere quella classe.

Il tuo metodo che lo crea, si occupa del primo e quindi restituisce il secondo fornendo solo un'interfaccia di sola lettura con cui interagire. Ciò non richiederebbe alcuna copia e ti consentirà di mettere a punto facilmente i comportamenti che desideri siano disponibili per il chiamante rispetto al creatore.

Prendi questo esempio:

public interface IPerson 
{
    public String FirstName 
    {
        get;
    }

    public String LastName 
    {
        get;
    }
} 

public class PersonImpl : IPerson 
{
    private String firstName, lastName;

    public String FirstName 
    {
        get { return firstName; }
        set { firstName = value; }
    }

    public String LastName 
    {
        get { return lastName; }
        set { lastName = value; }
    }
}

class Factory 
{
    public IPerson MakePerson() 
    {
        PersonImpl person = new PersonImpl();
        person.FirstName = 'Joe';
        person.LastName = 'Schmoe';
        return person;
    }
}

L'unico svantaggio di questo approccio è che si potrebbe semplicemente lanciarlo nella classe di implementazione. Se fosse una questione di sicurezza, l'utilizzo di questo approccio non è sufficiente. Per ovviare a questo problema, è possibile creare una classe di facciata per avvolgere la classe mutabile, che presenta semplicemente un'interfaccia con cui il chiamante lavora e che non può avere accesso all'oggetto interno.

In questo modo, nemmeno il casting ti aiuterà. Entrambi possono derivare dalla stessa interfaccia di sola lettura, ma il cast dell'oggetto restituito ti darà solo la classe Facade, che è immutabile in quanto non cambia lo stato sottostante della classe mutabile avvolta.

Vale la pena ricordare che questo non segue la tendenza tipica in cui un oggetto immutabile viene costruito una volta per tutte attraverso il suo costruttore. Comprensibilmente, potresti dover gestire molti parametri, ma dovresti chiederti se tutti questi parametri devono essere definiti in anticipo o se alcuni possono essere introdotti in un secondo momento. In tal caso, dovrebbe essere utilizzato un semplice costruttore con solo i parametri richiesti. In altre parole, non utilizzare questo modello se sta nascondendo un altro problema nel programma.


1
La sicurezza non è migliore restituendo un oggetto "sola lettura", poiché il codice che ottiene l'oggetto può comunque modificare l'oggetto usando reflection. Anche una stringa può essere modificata (non copiata, modificata sul posto) usando la riflessione.
Mentre era il

Il problema di sicurezza può essere risolto ordinatamente con una classe privata
Esben Skov Pedersen,

@Esben: devi ancora fare i conti con MS07-052: l'esecuzione del codice porta all'esecuzione del codice . Il tuo codice è in esecuzione nello stesso contesto di sicurezza del loro codice, quindi possono semplicemente collegare un debugger e fare tutto ciò che desiderano.
Kevin,

Kevin1 puoi dirlo di tutte le incapsulazioni. Non sto cercando di proteggere dalla riflessione.
Esben Skov Pedersen,

1
Il problema con l'uso della parola "sicurezza" è che immediatamente qualcuno presume che quella che io dico sia l'opzione più sicura equivale alla massima sicurezza e alle migliori pratiche. Nemmeno io l'ho mai detto. Se stai consegnando la libreria affinché qualcuno possa usarla, a meno che non sia offuscata (e talvolta anche quando offuscata), puoi dimenticare di garantire la sicurezza. Tuttavia, penso che possiamo essere tutti d'accordo sul fatto che se stai manomettendo un oggetto di facciata restituito usando la riflessione per recuperare l'oggetto interno contenuto all'interno, non stai esattamente usando la libreria come dovrebbe essere usata.
Neil,

8

Puoi usare il modello Builder come dice @JacquesB, o in alternativa pensare perché in realtà questi tuoi oggetti devono essere mutabili durante la creazione?

In altre parole, perché il processo di creazione di questi deve essere diffuso nel tempo, anziché passare tutti i valori richiesti nel costruttore e creare istanze in una volta sola?

Perché il Builder potrebbe essere una buona soluzione per il problema sbagliato.

Se il problema è che si finisce con un costruttore lungo 10 parametri e si desidera mitigarlo costruendo l'oggetto a poco a poco, potrebbe indicare che il design è incasinato e questi 10 valori dovrebbero essere " bagged "/ raggruppati in alcuni oggetti ... o l'oggetto principale diviso in alcuni più piccoli ...

In tal caso, attenersi completamente all'immutabilità, solo migliorare il design.


Sarebbe difficile crearlo tutto in una volta poiché il codice esegue più query di database per ottenere i dati; confronta i dati in diverse tabelle e in diversi database.
Paul Richards,
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.