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.