Come discutere contro questa mentalità "completamente pubblica" del design delle classi di oggetti business


9

Stiamo effettuando molti test unitari e refactoring dei nostri oggetti di business e sembra che io abbia opinioni molto diverse sul design di classe rispetto ad altri colleghi.

Un esempio di classe di cui non sono fan:

public class Foo
{
    private string field1;
    private string field2;
    private string field3;
    private string field4;
    private string field5;

    public Foo() { }

    public Foo(string in1, string in2)
    {
        field1 = in1;
        field2 = in2;
    }

    public Foo(string in1, string in2, string in3, string in4)
    {
        field1 = in1;
        field2 = in2;
        field3 = in3;
    }

    public Prop1
    { get { return field1; } }
    { set { field1 = value; } }

    public Prop2
    { get { return field2; } }
    { set { field2 = value; } }

    public Prop3
    { get { return field3; } }
    { set { field3 = value; } }

    public Prop4
    { get { return field4; } }
    { set { field4 = value; } }

    public Prop5
    { get { return field5; } }
    { set { field5 = value; } }

}

Nella classe "reale", non sono tutte stringhe, ma in alcuni casi abbiamo 30 campi di supporto per proprietà completamente pubbliche.

Io odio questa classe, e non so se sto solo essere pignoli. Alcune cose da notare:

  • I campi di supporto privati ​​senza logica nelle proprietà sembrano inutili e gonfiano la classe
  • Costruttori multipli (un po 'ok) ma accoppiati a
  • tutte le proprietà hanno un setter pubblico, non sono un fan.
  • Potenzialmente a nessuna proprietà verrebbe assegnato un valore a causa del costruttore vuoto, se un chiamante non è a conoscenza, si potrebbe potenzialmente ottenere un comportamento molto indesiderato e difficile da testare.
  • Sono troppe proprietà! (nel caso 30)

Trovo molto più difficile sapere veramente in quale stato si trova l'oggetto Fooin un dato momento, come implementatore. L'argomentazione è stata fatta "potremmo non avere le informazioni necessarie da impostare Prop5al momento della costruzione dell'oggetto. Ok, immagino di poterlo capire, ma in tal caso rendere Prop5pubblico solo il setter, non sempre fino a 30 proprietà in una classe.

Sono solo schizzinoso e / o pazzo per volere un corso "facile da usare" invece di essere "facile da scrivere (tutto pubblico)"? Classi come l'urlo di cui sopra per me, non so come verrà usato, quindi renderò tutto pubblico per ogni evenienza .

Se non sono terribilmente pignolo, quali sono i buoni argomenti per combattere questo tipo di pensiero? Non sono molto bravo a articolare gli argomenti, dato che mi sento molto frustrato nel cercare di ottenere il mio punto di vista (non intenzionalmente ovviamente).



3
Un oggetto dovrebbe essere sempre in uno stato valido; cioè, non dovresti avere oggetti parzialmente istanziati . Alcune informazioni non fanno parte dello stato centrale di un oggetto mentre altre lo sono (ad esempio un tavolo deve avere le gambe ma il tessuto è facoltativo); informazioni facoltative possono essere fornite dopo l'istanza, le informazioni chiave devono essere fornite all'istanza. Se alcune informazioni sono fondamentali ma non sono note al momento dell'istanza, direi che le classi necessitano di refactoring. È difficile dire di più senza conoscere il contesto della classe.
Marvin,

1
Dire "potremmo non avere le informazioni necessarie per impostare Prop5 al momento della costruzione dell'oggetto" mi fa chiedere "stai dicendo che ci saranno alcune volte che avrai quelle informazioni al momento della costruzione e altre volte in cui non lo farai hai queste informazioni? " Mi chiedo se in realtà ci siano due cose diverse che questa classe sta cercando di rappresentare, o forse se questa classe debba essere suddivisa in classi più piccole. Ma se è fatto "per ogni evenienza", va anche male, IMO; ciò mi dice che non esiste un progetto chiaro per il modo in cui gli oggetti vengono creati / popolati.
Apprendista Dr. Wily,

3
Per i campi di supporto privati ​​senza logica nelle proprietà, c'è un motivo per cui non si utilizzano le proprietà automatiche ?
Carson63000,

2
@Kritner il punto centrale delle proprietà automatiche è che se in futuro hai bisogno di una logica reale, puoi facilmente aggiungerla al posto dell'auto geto set:-)
Carson63000

Risposte:


10

Le classi completamente pubbliche hanno una giustificazione per determinate situazioni, così come l'altro estremo, le classi con un solo metodo pubblico (e probabilmente molti metodi privati). E lezioni con del pubblico, anche con alcuni metodi privati.

Tutto dipende dal tipo di astrazione che hai intenzione di modellare con loro, quali strati hai nel tuo sistema, il grado di incapsulamento di cui hai bisogno nei diversi strati e (ovviamente) quale scuola di pensiero arriva l'autore della classe a partire dal. Puoi trovare tutti questi tipi nel codice SOLID.

Ci sono interi libri scritti su quando preferire quale tipo di design, quindi non ho intenzione di elencare qui alcuna regola al riguardo, lo spazio in questa sezione non sarebbe sufficiente. Tuttavia, se hai un esempio del mondo reale per un'astrazione che ti piace modellare con una classe, sono sicuro che la comunità qui ti aiuterà felicemente a migliorare il design.

Per affrontare i tuoi altri punti:

  • "campi di supporto privati ​​senza logica nelle proprietà": Sì, hai ragione, per banchi e setter banali questo è solo un "rumore" inutile. Per evitare questo tipo di "bloat", C # ha una sintassi di scelta rapida per i metodi get / set della proprietà:

Quindi invece di

   private string field1;
   public string Prop1
   { get { return field1; } }
   { set { field1 = value; } }

Scrivi

   public string Prop1 { get;set;}

o

   public string Prop1 { get;private set;}
  • "Costruttori multipli": questo non è un problema in sé. Si presenta un problema quando è presente una duplicazione del codice non necessaria, come mostrato nell'esempio, o la gerarchia chiamante è contorta. Questo può essere facilmente risolto rifattorizzando le parti comuni in una funzione separata e organizzando la catena del costruttore in modo unidirezionale

  • "Potenzialmente nessuna proprietà verrebbe assegnata a un valore a causa del costruttore vuoto": in C #, ogni tipo di dati ha un valore predefinito chiaramente definito. Se le proprietà non sono inizializzate esplicitamente in un costruttore, viene assegnato questo valore predefinito. Se questo viene usato intenzionalmente , è perfettamente ok - quindi un costruttore vuoto potrebbe essere ok se l'autore sa cosa sta facendo.

  • "Sono troppe proprietà! (Nel caso 30)": sì, se sei libero di progettare una tale classe in modo ecologico, 30 sono troppi, sono d'accordo. Tuttavia, non tutti noi hanno questo lusso (non hai scritto nel commento qui sotto è un sistema legacy?). A volte è necessario mappare i record da un database, file o dati esistenti da un'API di terze parti al proprio sistema. Quindi, in questi casi, 30 attributi potrebbero essere qualcosa con cui convivere.


Grazie, sì, so che i POCO / POJO hanno il loro posto e il mio esempio è piuttosto astratto. Non penso di poter essere più specifico senza entrare in un romanzo.
Kritner

@Kritner: vedi la mia modifica
Doc Brown

sì, di solito quando creo una classe nuova di zecca vado sul percorso di proprietà auto. Avere i campi di supporto privati ​​"solo perché" è (nella mia mente) non necessario e persino causa problemi. Quando hai ~ 30 proprietà in una classe e 100+ classi, non è molto inverosimile dire che ci saranno alcune impostazioni delle Proprietà o che verranno dal campo di supporto errato ... Ho già incontrato diverse volte nel nostro refactoring in effetti :)
Kritner

grazie, il valore predefinito per stringa è nullno? quindi se una delle proprietà effettivamente utilizzate in a .ToUpper(), ma non viene mai impostato un valore, genererebbe un'eccezione di runtime. Questo è un altro esempio buono perché i necessari dati per la classe dovrebbe avere essere impostato durante la costruzione dell'oggetto. Non lasciarlo solo all'utente. Grazie
Kritner

Oltre a troppe proprietà non impostate, il problema principale con questa classe è che ha una logica zero (ZRP) ed è l'archetipo di un modello anemico. Senza un bisogno di ciò che può essere espresso in parole, come un DTO, è un design scadente.
user949300

2
  1. Dicendo che "Campi di backing privati ​​senza logica nelle proprietà, sembra inutile e gonfia la classe" hai già un argomento decente.

  2. Non sembra che il problema riguardi più costruttori.

  3. Per quanto riguarda avere tutte le proprietà come pubbliche ... Forse pensaci in questo modo, se mai avessi voluto sincronizzare l'accesso a tutte queste proprietà tra i thread avresti avuto un momento difficile perché potresti dover scrivere codice di sincronizzazione ovunque siano Usato. Tuttavia, se tutte le proprietà fossero racchiuse da getter / setter, è possibile creare facilmente la sincronizzazione nella classe.

  4. Penso che quando dici "comportamento difficile da testare" l'argomento parla da solo. Il test potrebbe dover verificare se non è null o qualcosa del genere (ogni singolo posto in cui viene utilizzata la proprietà). Non lo so davvero perché non so quale sia il tuo test / applicazione.

  5. Hai ragione troppe proprietà, potresti provare a usare l'ereditarietà (se le proprietà sono abbastanza simili) e quindi avere un elenco / array che usa generici? Quindi potresti scrivere getter / setter per accedere alle informazioni e semplificare il modo in cui interagirai con tutte le proprietà.


1
Grazie - # 1 e # 4 Sicuramente mi sento più a mio agio nel fare la mia discussione. Non sono del tutto sicuro di cosa intendi per # 3. # 5 la maggior parte delle proprietà nelle classi costituisce la chiave primaria sul db. Utilizziamo chiavi della concorrenza, a volte fino a 8 colonne, ma questo è un altro problema. Stavo pensando di provare a mettere le parti che compongono la chiave nella propria classe / struttura, che eliminerebbe molte proprietà (almeno in questa classe) - ma questa è un'applicazione legacy con migliaia di righe di codice con più chiamanti. Quindi credo di avere molto a cui pensare :)
Kritner

Eh. Se la classe fosse immutabile, non ci sarebbe motivo di scrivere alcun codice di sincronizzazione all'interno della classe.
RubberDuck,

@RubberDuck Questa classe è immutabile? Cosa intendi?
Snoop

3
E 'sicuramente non @StevieV immutabile. Qualsiasi classe con setter di proprietà è modificabile.
RubberDuck,

1
@Kritner Come vengono utilizzate le proprietà come chiavi primarie? Ciò presumibilmente richiede una logica (controlli nulli) o regole (ad esempio NAME ha la priorità su AGE). Secondo OOP, questo codice appartiene a questa classe. Potresti usarlo come argomento?
user949300

2

Come mi hai detto, Fooè un oggetto business. Dovrebbe incapsulare alcuni comportamenti nei metodi in base al tuo dominio e i suoi dati devono rimanere il più possibile incapsulati.

Nell'esempio che mostri, Foosembra più un DTO e va contro tutti i principi OOP (vedi Anemic Domain Model )!

Come miglioramenti suggerirei:

  • Rendi questa classe il più immutabile possibile. Come hai detto, vuoi fare alcuni test unitari. Una capacità obbligatoria per i test unitari è il determinismo e il determinismo può essere raggiunto tramite l'immutabilità perché risolve i problemi degli effetti collaterali .
  • Suddividi questa classe divina in più classi che fanno solo 1 cosa ciascuna (vedi SRP ). Unit test una classe di 30 attributi in realtà un PITA .
  • Rimuovere la ridondanza del costruttore avendo solo 1 costruttore principale e gli altri chiamano il costruttore principale.
  • Rimuovi getter inutili, dubito seriamente che tutti i getter siano davvero utili.
  • Riporta la logica di business nelle business class, ecco dove appartiene!

Questa potrebbe essere una potenziale classe refactored con queste osservazioni:

public sealed class Foo
{
    private readonly string field1;
    private readonly string field2;
    private readonly string field3;
    private readonly string field4;

    public Foo() : this("default1", "default2")
    { }

    public Foo(string in1, string in2) : this(in1, in2, "default3", "default4")
    { }

    public Foo(string in1, string in2, string in3, string in4)
    {
        field1 = in1;
        field2 = in2;
        field3 = in3;
        field4 = in4;
    }

    //Methods with business logic

    //Really needed ?
    public Prop1
    { get; }

    //Really needed ?
    public Prop2
    { get; }

    //Really needed ?
    public Prop3
    { get; }

    //Really needed ?
    public Prop4
    { get; }

    //Really needed ?
    public Prop5
    { get; }
}

2

Come discutere contro questa mentalità "completamente pubblica" del design delle classi di oggetti business

  • "Esistono diversi metodi sparsi nelle classi client che fanno molto di più del" semplice dovere DTO ". Appartengono insieme."
  • "Potrebbe essere un 'DTO' ma ha un'identità aziendale - dobbiamo scavalcare gli uguali".
  • "Dobbiamo ordinare questi - dobbiamo implementare IComparable"
  • "Stiamo mettendo insieme molte di queste proprietà. Sostituiamo ToString in modo che ogni client non debba scrivere questo codice."
  • "Abbiamo più clienti che fanno la stessa cosa con questa classe. Dobbiamo DRY up del codice."
  • "Il client sta manipolando una raccolta di queste cose. È ovvio che dovrebbe essere una classe di raccolta personalizzata; e molti dei punti precedenti renderanno tale raccolta personalizzata più funzionale di quella attuale."
  • "Guarda tutta la noiosa, esposta, manipolazione delle stringhe! Incapsulare ciò con metodi rilevanti per il business aumenterà la nostra produttività di codifica."
  • "Mettere insieme questi metodi nella classe li renderà testabili perché non abbiamo a che fare con la classe client più complessa."
  • "Ora possiamo testare unitamente quei metodi refactored. Come è, per ragioni x, y, z il client non è testabile.

  • Nella misura in cui uno qualsiasi degli argomenti di cui sopra può essere fatto, in forma aggregata:

    • La funzionalità diventa riutilizzabile.
    • È ASCIUTTO
    • È disaccoppiato dai clienti.
    • la codifica rispetto alla classe è più veloce e meno soggetta a errori.
  • "Una classe ben fatta nasconde lo stato ed espone funzionalità. Questa è la proposta iniziale per la progettazione di classi OO."
  • "Il risultato finale fa un lavoro molto migliore nell'esprimere il dominio aziendale; le entità distinte e la loro interazione esplicita".
  • "Questa funzionalità" ambientale "(cioè non i requisiti funzionali espliciti, ma deve essere eseguita) si distingue chiaramente e aderisce al principio della responsabilità singola.
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.