È una cattiva idea usare getter / setter e / o proprietà? [duplicare]

11

Sono perplesso dai commenti sotto questa risposta: link

Un utente sta discutendo contro l'uso di getter / setter e proprietà. Egli sostiene che la maggior parte delle volte il loro utilizzo è un segno di un cattivo design. I suoi commenti stanno guadagnando voti.

Bene, sono un programmatore alle prime armi, ma per essere onesto, trovo il concetto di proprietà molto attraente. Perché un array non dovrebbe consentire il ridimensionamento scrivendo sulla proprietà size o length ? Perché non dovremmo ottenere tutti gli elementi in un albero binario leggendo la sua proprietà size ? Perché un modello di auto non dovrebbe esporre la sua velocità massima come proprietà? Se stiamo realizzando un gioco di ruolo, perché un mostro non dovrebbe mostrare il suo attacco come getter? E perché non dovrebbe esporre i suoi attuali HP come un getter? Oppure, se realizziamo un software per la progettazione domestica, perché non dovremmo consentire la ricolorazione di un muro scrivendo nel suo color setter?

Sembra tutto così naturale, così ovvio per me, non potrei mai arrivare alla conclusione che usare proprietà o getter / setter potrebbe essere un segnale di avvertimento.

L'utilizzo di proprietà e / o di getter setters di solito non è una buona idea e, in tal caso, perché?

    
posta gaazkam 10.10.2017 - 21:32
fonte

5 risposte

20

La semplice esposizione dei campi, come campi pubblici, proprietà o metodi di accesso, può essere un indicatore della modellazione di oggetti insufficiente. Il risultato è che chiediamo un oggetto per vari dati e prendiamo decisioni su quei dati. Quindi queste decisioni saranno prese al di fuori dell'oggetto. Se questo accade ripetutamente, forse quella decisione dovrebbe essere la responsabilità dell'oggetto, e dovrebbe essere fornita da un metodo di quell'oggetto: dovremmo dire all'oggetto cosa fare, e dovrebbe capire come fallo da solo.

Ovviamente questo non è sempre appropriato. Se esageri, i tuoi oggetti finiscono con una collezione selvaggia di molti piccoli metodi indipendenti che vengono usati una sola volta. Forse, è meglio mantenere il tuo comportamento separato dai tuoi dati e scegliere un approccio più procedurale. A volte viene chiamato "modello di dominio anemico" . I tuoi oggetti quindi hanno un comportamento molto piccolo, ma espongono il loro stato attraverso qualche meccanismo.

Se esponi qualsiasi tipo di proprietà, dovresti essere coerente, sia nel nome che nella tecnica utilizzata. Come farlo dipende principalmente dalla lingua. Esporre tutti come campi pubblici, o tutto tramite le proprietà, o tutti tramite i metodi di accesso. Per esempio. non combinare un campo rectangle.x con un metodo rectange.y() o un metodo user.name() con un user.getEmail() getter.

Un problema con le proprietà e soprattutto con le proprietà o setter scrivibili è che rischi di indebolire l'incapsulamento del tuo oggetto. L'incapsulamento è importante perché puoi ragionare sulla correttezza del tuo oggetto da solo. Questa modularizzazione consente di comprendere più facilmente sistemi complessi. Ma se accediamo ai campi di un oggetto invece di emettere comandi di alto livello, diventiamo sempre più accoppiati a quella classe. Se valori arbitrari possono essere scritti in un campo dall'esterno di un oggetto, diventa molto difficile mantenere uno stato coerente per l'oggetto. È responsabilità dell'oggetto mantenersi coerente e ciò significa convalidare i dati in arrivo. Per esempio. un oggetto array non deve avere la sua lunghezza impostata su un valore negativo. Questo tipo di convalida è impossibile per un campo pubblico e facile da dimenticare per un setter autogenerato. È più ovvio con un'operazione di alto livello come array.resize() .

Termini di ricerca suggeriti per ulteriori informazioni:

risposta data 10.10.2017 - 21:55
fonte
3

Non è un design intrinsecamente sbagliato, ma come qualsiasi funzione potente può essere abusato.

Il punto delle proprietà (metodi impliciti getter e setter) è che forniscono una potente astrazione sintattica che consente al consumatore di utilizzare il codice per trattarli logicamente come campi mantenendo il controllo per l'oggetto che li definisce.

Anche se questo è spesso pensato in termini di incapsulamento, spesso ha più a che fare con uno stato di controllo, e B con comodità sintattica per il chiamante.

Per ottenere un contesto, dobbiamo prima considerare il linguaggio di programmazione stesso. Mentre molte lingue hanno "Proprietà", sia la terminologia che le funzionalità variano notevolmente.

Il linguaggio di programmazione C # ha una nozione molto sofisticata di proprietà sia a livello semantico che sintattico. Consente al getter o setter di essere facoltativo e consente in modo critico di avere diversi livelli di visibilità. Questo fa una grande differenza se si desidera esporre un'API a grana fine.

Considera un assembly che definisce un insieme di strutture dati che sono per natura dei grafici ciclici . Ecco un semplice esempio:

public sealed class Document
{
    public Document(IEnumerable<Word> words)
    {
        this.words = words.ToList();
        foreach (var word in this.words)
        {
            word.Document = this;
        }
    }

    private readonly IReadOnlyList<Word> words;
}

public sealed class Word
{
    public Document Document
    {
        get => this.document;
        internal set
        {
            this.document = value;
        }
    }

    private Document document;
}

Idealmente, Word.Document non sarebbe affatto modificabile, ma non posso esprimerlo nella lingua. Potrei aver creato un IDictionary<Word, Document> che ha mappato Word s a Document s ma alla fine ci sarà la mutabilità da qualche parte

L'intento qui è quello di creare un grafico ciclico tale che qualsiasi funzione a cui è assegnata una parola possa interrogare Word come contenente Document tramite la proprietà Word.Document (getter). Vogliamo anche evitare la mutazione dei consumatori dopo che è stato creato Document . L'unico scopo del setter è stabilire il collegamento. È pensato solo per essere scritto una sola volta. Questo è difficile da fare perché dobbiamo prima creare le istanze di Word . Usando la capacità di C # di attribuire diversi livelli di accessibilità al getter e setter della stessa proprietà, siamo in grado di incapsulare la mutabilità della proprietà Word.Document all'interno del assemblaggio.

Il codice che consuma queste classi da un altro assembly vedrà la proprietà come di sola lettura (poiché ha solo get non set ).

Questo esempio mi porta alla cosa che preferisco delle proprietà. Possono avere solo un getter. Sia che tu voglia semplicemente restituire un valore calcolato o se vuoi esporre solo la capacità di leggere ma non scrivere, l'uso delle proprietà è in realtà intuitivo e semplice sia per l'implementazione che il codice che consuma.

Considera l'esempio banale di

class Rectangle
{
   public Rectangle(double width, double height) => (Width, Height) = (width, height);

   public double Area { get => Width * Height; }

   public double Width { get; private set; }

   public double Height { get; private set; }
}

Ora, come ho detto prima, alcune lingue hanno concetti diversi su cosa sia una proprietà. JavaScript è uno di questi esempi. Le possibilità sono complesse. Ci sono una moltitudine di modi in cui una proprietà può essere definita da un oggetto e i loro sono modi molto diversi in cui la visibilità può essere controllata.

Tuttavia, nel caso banale, come in C #, JavaScript consente di definire le proprietà in modo che espongano solo un getter. Questo è meraviglioso per il controllo della mutazione.

Si consideri:

function createRectangle(width, height) {
   return {
     get width() {
       return width;
     },
     get height() {
       return height;
     }
   };
}

Quindi quando dovrebbero essere evitate le proprietà? In generale, evita i setter che hanno una logica. Anche la semplice convalida è spesso meglio realizzata altrove.

Tornando a C #, è spesso tentato di scrivere codice come

public sealed class Person
{
    public Address Address
    {
        get => this.address;
        set
        {
            if (value is null)
            {
                throw new ArgumentException($"{nameof(Address)} must have a value");
            }
            this.address = value;
        }
    }

    private Address address;
}

public sealed class Address { }

L'eccezione generata è probabilmente una sorpresa per i consumatori. Dopo tutto, Person.Address è dichiarato mutabile e null è un valore perfettamente valido per un valore di tipo Address . A peggiorare le cose, viene alzato un ArgumentException . Il consumatore potrebbe non essere consapevole del fatto che sta chiamando una funzione e quindi il tipo di eccezione aggiunge ancora di più al comportamento sorprendente. Probabilmente, un diverso tipo di eccezione, come InvalidOperationException sarebbe più appropriato. Tuttavia, questo evidenzia dove i setter possono diventare brutti. Il consumatore pensa alle cose in termini diversi. Ci sono momenti in cui questo tipo di design è utile.

Invece, sarebbe meglio rendere Address un argomento richiesto del costruttore, oppure creare un metodo dedicato per impostarlo se dobbiamo esporre l'accesso in scrittura ed esporre una proprietà con un solo getter.

Aggiungerò altro a questa risposta.

    
risposta data 11.10.2017 - 03:38
fonte
2

Aggiungere getter e setter in modo riflessivo (cioè senza pensare, non confonderlo con il farlo in modo riflessivo come nella domanda che si collega) è un segno di problemi. Prima di tutto, se disponi di getter e supervisori per i campi, perché non rende pubblici solo i campi? Ad esempio:

class Foo
{
    private int _i;
    public int getData() { return _i; }
    public void setData(int i) { _i = i; };
}

Quello che hai fatto sopra è solo rendere più difficile l'uso dei dati, e difficile da leggere usando il codice. Piuttosto che limitarti a fare foo.i += 1 , stai costringendo l'utente a fare foo.setData(foo.getData() + 1) , che è semplicemente ottuso. E per quale beneficio? Presumibilmente stai facendo getter e setter per l'encapsultation e / o il controllo dei dati, ma se hai un passthrough public getter e setter non hai comunque bisogno di nessuno di questi. Alcune persone sostengono che potresti voler aggiungere il controllo dei dati in seguito, ma quanto spesso viene fatto in pratica? Se hai getter e setter pubblici stai già lavorando ad un livello di astrazione, semplicemente non lo stai facendo. E nella remota possibilità che lo farai, puoi semplicemente aggiustarlo a quel punto.

I getter e setter pubblici sono un segno della programmazione settoriale del carico, in cui stai solo seguendo le regole del gioco perché ritieni che siano in grado di migliorare il tuo codice, ma non ci hai mai pensato se lo fanno o no. Se la base di codice è piena di getter e setter, considera il motivo per cui semplicemente non stai lavorando con i POD, forse è solo un modo migliore di lavorare con i dati per il tuo dominio?

La critica alla domanda originale non è che i getter o gli incastonatori siano una cattiva idea, è solo che l'aggiunta di getter e setter pubblici per tutti i campi, in linea di principio, in realtà non fa nulla di buono.

    
risposta data 11.10.2017 - 14:21
fonte
0

Per capire il problema con Getter e setter, penso sia importante fare un passo indietro e parlare dell'astrazione e del concetto di un'interfaccia. Non intendo la parola chiave di programmazione interface come in Java, intendo l'interfaccia in senso più generale. Ad esempio, se si dispone di un forno a microonde, ha una sorta di interfaccia utente che consente di controllarlo. Il design dell'interfaccia è uno degli aspetti più fondamentali della progettazione del sistema. Si riferisce direttamente a come si evolverà il tuo sistema.

Iniziamo con un esempio negativo. Diciamo che voglio memorizzare le informazioni sul mio inventario. Così creo una classe: Inventory . Prima di tutto, i miei requisiti dicono che ho bisogno di sapere quanti diversi articoli ci sono o un certo tipo nell'inventario così questa informazione può essere usata in altre parti del programma. OK, diciamo il codice:

public class Inventory {
    public final String sku;
    public int numberOfItems;
}

Ottimo, ora posso creare oggetti di inventario che possono essere condivisi nel programma. Ma presto mi imbatto in un problema. Quando gli articoli sono venduti o aggiunti all'inventario, ho bisogno di aggiornare questi oggetti. Potrebbero esserci molte persone diverse che comprano o vendono queste cose allo stesso tempo. Quando due parti del programma cercano di aggiornare il numero allo stesso tempo, iniziamo a ricevere errori. Ad esempio, diciamo che abbiamo 50 articoli e 100 articoli sono stati portati nel magazzino, ma allo stesso tempo abbiamo venduto 25. Quindi due parti leggono il valore e quindi lo modificano e lo riscrivono. Se il codice di vendita imposta il valore per ultimo, ora abbiamo 25 elementi nell'oggetto, quando in realtà abbiamo 125.

Quindi aggiungiamo un wrapper e sincronizziamo:

public class Inventory {
    public final String sku;
    private int numberOfItems;

    public int getNumberOfItems() {
        synchronized (this) {
            return numberOfItems;
        }
    }

    synchronized public void setNumberOfItems(int number) {
        synchronized (this) {
            this.numberOfItems = number;
        }
    }
}

Grande. Ora due thread non possono leggere e scrivere allo stesso tempo. Ma non abbiamo davvero risolto il problema. Le due parti possono ancora leggere dal getter (uno alla volta) e poi scrivere di nuovo i nuovi valori sbagliati (uno alla volta.) Non siamo mai arrivati da nessuna parte.

Ecco un approccio migliore:

public class Inventory {
    private final String sku;
    private int numberOfItems;

    public int getSku() {
      return sku;
    }

    public int getNumberOfItems() {
        synchronized (this) {
            return numberOfItems;
        }
    }

    public void addItems(int number) {
        synchronized (this) {
            this.numberOfItems += number;
        }
    }
}

Ora abbiamo qualcosa che risolve veramente il problema. Assicura che le modifiche all'inventario siano fatte una alla volta.

Questo è un esempio molto semplice e non è pensato per mostrare come scrivere un buon codice nel 2017. Il punto qui è che per avere un design robusto, è necessario concentrarsi su come i tuoi oggetti / classi definiranno i loro controlli ad altre parti dell'applicazione. La definizione dello stato interno ha ben poco a che fare con questo. Nell'esempio sopra, diciamo che l'azienda cresce e ora è distribuita. L'inventario sarà memorizzato su qualche server remoto da qualche parte. Non ci sarà nemmeno un campo. Per mantenere l'interfaccia getter / setter, dovrai creare altre classi di helper che gestiscano i tuoi bean e trasferiscano i dati avanti e indietro. Questo è il modo in cui funzionano i programmi procedurali (ad esempio COBOL). I dati vengono trasportati e vari programmi che manipolano tali dati. Questi tipi di design sono molto fragili e costosi da modificare perché ci sono molti pezzi di codice disparati che devono essere modificati di concerto. Ogni parte può fare cose che rompono altre parti del sistema.

Si noti che nel design migliorato ci sono ancora getter. I getter sono decisamente meno problematici dei setter. Anche se personalmente non mi piace il prefisso get sulle cose, ci sono momenti in cui è meglio conformarsi alle aspettative. Ma puoi ancora finire nei guai con i Getter. Se utilizzi oggetti come borse di dati per la consegna alle parti del codice con logica, è essenzialmente lo stesso problema.

Spero che questo aiuti. Commenti benvenuto.

    
risposta data 10.10.2017 - 22:31
fonte
-1

Or if we're making a home design software why shouldn't we allow recoloring a wall by writing into its color setter?

Quindi diciamo che abbiamo un programma che ti permette di vedere quali sono le varie combinazioni di colori e finiture. Quindi crei una classe muro e crei un setter per il colore. Quindi, quando l'utente vuole impostare il colore dei muri, devi scorrere tutti i muri e impostare il colore su ciascuno di essi.

In alternativa, potresti far sì che i muri tirino il loro colore da una fonte comune. Quindi aggiorni quella proprietà, e quando i muri devono essere ridisegnati, ognuno di essi tira da quella proprietà.

La differenza sembra minore, ma il loop è una caldaia rumorosa. Inoltre, perché mantenere X copie di un valore quando ne hai uno? Questo tipo di approccio non scala. Questi sono casi semplici ma questo tipo di problemi rende difficile l'impossibilità di utilizzare tecniche di progettazione più avanzate.

Why shouldn't a car model expose its maximum speed as a property?

Potrebbe essere utile, ma prendiamo un esempio più interessante: coefficiente di attrito per le gomme. Un modello semplice avrebbe quel valore in una proprietà. Ma dipende davvero dalla superficie. Diciamo che dobbiamo modellare cosa succede quando l'auto colpisce una grande pozzanghera ma solo su un lato della macchina. Possiamo controllare ripetutamente dove si trova la ruota e quando incontra una superficie diversa, aggiorna il suo coefficiente di attrito. Ma dobbiamo anche considerare la forza normale. Quindi possiamo aggiungere del codice a quel ciclo che lo considera. Inoltre, c'è la velocità dell'aria. E lo stato della sospensione.

Oppure, potresti fare in modo che la ruota calcoli il suo coefficiente di attrito corrente in base alle informazioni che è in grado di recuperare da fonti pertinenti. La logica è la stessa, ma ora è allineata alla ruota. Possiamo quindi aggiungere facilmente diverse versioni della ruota per tenere conto di diversi materiali, per esempio. Se mantieni tutta questa logica in qualche loop esterno, avrai bisogno di un sacco di case o di istruzioni if per gestire tutti i vari scenari.

    
risposta data 10.10.2017 - 23:00
fonte

Leggi altre domande sui tag