Domanda

Supponiamo che tu abbia una classe chiamata Cliente, che contiene i seguenti campi:

  • Nome utente
  • E-mail
  • Nome di battesimo
  • Cognome

Diciamo anche che, in base alla logica aziendale, tutti gli oggetti Cliente devono avere queste quattro proprietà definite.

Ora possiamo farlo abbastanza facilmente forzando il costruttore a specificare ciascuna di queste proprietà.Ma è abbastanza facile vedere come questo possa andare fuori controllo quando sei costretto ad aggiungere più campi obbligatori all'oggetto Cliente.

Ho visto classi che accettano più di 20 argomenti nel loro costruttore ed è semplicemente una seccatura usarli.Ma, in alternativa, se non si richiedono questi campi si corre il rischio di avere informazioni non definite o, peggio, errori di riferimento all'oggetto se si fa affidamento sul codice chiamante per specificare queste proprietà.

Ci sono alternative a questo o devi solo decidere se la quantità X di argomenti del costruttore è troppa per te con cui convivere?

È stato utile?

Soluzione

Due approcci progettuali da considerare

IL essenza modello

IL interfaccia fluente modello

Entrambi sono simili nell'intento, nel senso che costruiamo lentamente un oggetto intermedio e quindi creiamo il nostro oggetto di destinazione in un unico passaggio.

Un esempio dell'interfaccia fluente in azione sarebbe:

public class CustomerBuilder {
    String surname;
    String firstName;
    String ssn;
    public static CustomerBuilder customer() {
        return new CustomerBuilder();
    }
    public CustomerBuilder withSurname(String surname) {
        this.surname = surname; 
        return this; 
    }
    public CustomerBuilder withFirstName(String firstName) {
        this.firstName = firstName;
        return this; 
    }
    public CustomerBuilder withSsn(String ssn) {
        this.ssn = ssn; 
        return this; 
    }
    // client doesn't get to instantiate Customer directly
    public Customer build() {
        return new Customer(this);            
    }
}

public class Customer {
    private final String firstName;
    private final String surname;
    private final String ssn;

    Customer(CustomerBuilder builder) {
        if (builder.firstName == null) throw new NullPointerException("firstName");
        if (builder.surname == null) throw new NullPointerException("surname");
        if (builder.ssn == null) throw new NullPointerException("ssn");
        this.firstName = builder.firstName;
        this.surname = builder.surname;
        this.ssn = builder.ssn;
    }

    public String getFirstName() { return firstName;  }
    public String getSurname() { return surname; }
    public String getSsn() { return ssn; }    
}
import static com.acme.CustomerBuilder.customer;

public class Client {
    public void doSomething() {
        Customer customer = customer()
            .withSurname("Smith")
            .withFirstName("Fred")
            .withSsn("123XS1")
            .build();
    }
}

Altri suggerimenti

Vedo che alcune persone raccomandano sette come limite massimo.Apparentemente non è vero che le persone possano tenere in testa sette cose contemporaneamente;ne ricordano solo quattro (Susan Weinschenk, 100 cose che ogni designer deve sapere sulle persone, 48).Anche così, considero quattro una sorta di orbita terrestre alta.Ma è perché il mio modo di pensare è stato modificato da Bob Martin.

In Codice pulito, Lo zio Bob sostiene che tre sia un limite superiore generale per il numero di parametri.Egli fa un’affermazione radicale (40):

Il numero ideale di argomenti per una funzione è zero (niladico).Poi viene uno (monadico) seguito da vicino da due (diadico).Tre argomenti (triadici) dovrebbero essere evitati ove possibile.Più di tre (poliadico) richiedono una giustificazione molto speciale e quindi non dovrebbero essere usati comunque.

Lo dice per ragioni di leggibilità;ma anche a causa della testabilità:

Immaginate la difficoltà di scrivere tutti i casi di test per garantire che tutte le varie combinazioni di argomenti funzionino correttamente.

Ti incoraggio a trovare una copia del suo libro e leggere la sua discussione completa sugli argomenti delle funzioni (40-43).

Sono d'accordo con coloro che hanno menzionato il principio di responsabilità unica.È difficile per me credere che una classe che necessita di più di due o tre valori/oggetti senza valori predefiniti ragionevoli abbia in realtà una sola responsabilità e non starebbe meglio con un'altra classe estratta.

Ora, se stai iniettando le tue dipendenze attraverso il costruttore, gli argomenti di Bob Martin su quanto sia facile invocare il costruttore non si applicano molto (perché di solito c'è solo un punto nella tua applicazione in cui lo colleghi, o addirittura avere un framework che lo faccia per te).Tuttavia, il principio di responsabilità unica è ancora rilevante:una volta che una classe ha quattro dipendenze, ritengo che un odore stia svolgendo una grande quantità di lavoro.

Tuttavia, come per tutto ciò che riguarda l'informatica, ci sono senza dubbio casi validi per avere un gran numero di parametri del costruttore.Non distorcere il codice per evitare di utilizzare un numero elevato di parametri;ma se usi un gran numero di parametri, fermati e pensaci, perché potrebbe significare che il tuo codice è già contorto.

Nel tuo caso, rimani con il costruttore.Le informazioni appartengono a Cliente e 4 campi vanno bene.

Nel caso in cui siano presenti molti campi obbligatori e facoltativi, il costruttore non è la soluzione migliore.Come ha detto @boojiboy, è difficile da leggere ed è anche difficile scrivere il codice client.

@contagious ha suggerito di utilizzare il modello e i setter predefiniti per gli attributi opzionali.Ciò impone che i campi siano mutevoli, ma questo è un problema minore.

Joshua Block su Effective Java 2 afferma che in questo caso dovresti considerare un builder.Un esempio tratto dal libro:

 public class NutritionFacts {  
   private final int servingSize;  
   private final int servings;  
   private final int calories;  
   private final int fat;  
   private final int sodium;  
   private final int carbohydrate;  

   public static class Builder {  
     // required parameters  
     private final int servingSize;  
     private final int servings;  

     // optional parameters  
     private int calories         = 0;  
     private int fat              = 0;  
     private int carbohydrate     = 0;  
     private int sodium           = 0;  

     public Builder(int servingSize, int servings) {  
      this.servingSize = servingSize;  
       this.servings = servings;  
    }  

     public Builder calories(int val)  
       { calories = val;       return this; }  
     public Builder fat(int val)  
       { fat = val;            return this; }  
     public Builder carbohydrate(int val)  
       { carbohydrate = val;   return this; }  
     public Builder sodium(int val)  
       { sodium = val;         return this; }  

     public NutritionFacts build() {  
       return new NutritionFacts(this);  
     }  
   }  

   private NutritionFacts(Builder builder) {  
     servingSize       = builder.servingSize;  
     servings          = builder.servings;  
     calories          = builder.calories;  
     fat               = builder.fat;  
     soduim            = builder.sodium;  
     carbohydrate      = builder.carbohydrate;  
   }  
}  

E poi usarlo in questo modo:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
      calories(100).sodium(35).carbohydrate(27).build();

L'esempio sopra è stato tratto da Java 2 efficace

E questo non vale solo per il costruttore.Citando Kent Beck in Modelli di implementazione:

setOuterBounds(x, y, width, height);
setInnerBounds(x + 2, y + 2, width - 4, height - 4);

Rendere esplicito il rettangolo come oggetto spiega meglio il codice:

setOuterBounds(bounds);
setInnerBounds(bounds.expand(-2));

Penso che la risposta "pura OOP" sia che se le operazioni sulla classe non sono valide quando determinati membri non sono inizializzati, allora questi membri devono essere impostati dal costruttore.C'è sempre il caso in cui è possibile utilizzare i valori predefiniti, ma suppongo che non stiamo considerando questo caso.Questo è un buon approccio quando l'API viene corretta, perché modificare il singolo costruttore consentito dopo che l'API diventa pubblica sarà un incubo per te e per tutti gli utenti del tuo codice.

In C#, ciò che capisco delle linee guida di progettazione è che questo non è necessariamente l'unico modo per gestire la situazione.In particolare con gli oggetti WPF, scoprirai che le classi .NET tendono a favorire i costruttori senza parametri e genereranno eccezioni se i dati non sono stati inizializzati su uno stato desiderabile prima di chiamare il metodo.Tuttavia, questo è probabilmente principalmente specifico della progettazione basata su componenti;Non riesco a trovare un esempio concreto di una classe .NET che si comporti in questo modo.Nel tuo caso, causerebbe sicuramente un maggiore onere sui test per garantire che la classe non venga mai salvata nell'archivio dati a meno che le proprietà non siano state convalidate.Onestamente per questo motivo preferirei l'approccio "il costruttore imposta le proprietà richieste" se la tua API è fissa o non pubblica.

L'unica cosa che io Sono La cosa certa è che probabilmente esistono innumerevoli metodologie in grado di risolvere questo problema, e ognuna di esse introduce una propria serie di problemi.La cosa migliore da fare è imparare quanti più modelli possibili e scegliere quello migliore per il lavoro.(Non è una tale risposta come una scappatoia?)

Penso che la tua domanda riguardi più la progettazione delle tue classi che il numero di argomenti nel costruttore.Se avessi bisogno di 20 dati (argomenti) per inizializzare con successo un oggetto, probabilmente prenderei in considerazione la possibilità di suddividere la classe.

Steve McConnell scrive in Code Complete che le persone hanno difficoltà a tenere in testa più di 7 cose alla volta, quindi quello sarebbe il numero sotto il quale cerco di rimanere.

Se hai molti argomenti sgradevoli, impacchettali semplicemente insieme in strutture/classi POD, preferibilmente dichiarate come classi interne della classe che stai costruendo.In questo modo puoi comunque richiedere i campi rendendo ragionevolmente leggibile il codice che chiama il costruttore.

Penso che tutto dipenda dalla situazione.Per qualcosa come il tuo esempio, una classe cliente, non rischierei la possibilità che tali dati non siano definiti quando necessario.D'altro canto, passare una struttura ripulirebbe l'elenco degli argomenti, ma avresti ancora molte cose da definire nella struttura.

Penserei che il modo più semplice sarebbe trovare un valore predefinito accettabile per ciascun valore.In questo caso, sembra che ogni campo debba essere costruito, quindi eventualmente sovraccaricare la chiamata di funzione in modo che se qualcosa non è definito nella chiamata, impostarlo su un valore predefinito.

Quindi, crea funzioni getter e setter per ciascuna proprietà in modo che i valori predefiniti possano essere modificati.

Implementazione Java:

public static void setEmail(String newEmail){
    this.email = newEmail;
}

public static String getEmail(){
    return this.email;
}

Questa è anche una buona pratica per mantenere sicure le variabili globali.

Lo stile conta molto e mi sembra che se esiste un costruttore con più di 20 argomenti, il design dovrebbe essere modificato.Fornire valori predefiniti ragionevoli.

Sono d'accordo sul limite di 7 elementi menzionato da Boojiboy.Oltre a ciò, potrebbe valere la pena esaminare tipi anonimi (o specializzati), IDictionary o reindirizzamenti indiretti tramite chiave primaria a un'altra origine dati.

Incapsulerei campi simili in un oggetto a sé stante con la propria logica di costruzione/convalida.

Diciamo ad esempio, se hai

  • Telefono aziendale
  • Recapito di lavoro
  • Telefono di casa
  • Indirizzo di casa

Creerei una classe che memorizzi il telefono e l'indirizzo insieme a un tag che specifica se si tratta di un telefono/indirizzo "di casa" o "di lavoro".E quindi ridurre i 4 campi semplicemente a un array.

ContactInfo cinfos = new ContactInfo[] {
    new ContactInfo("home", "+123456789", "123 ABC Avenue"),
    new ContactInfo("biz", "+987654321", "789 ZYX Avenue")
};

Customer c = new Customer("john", "doe", cinfos);

Questo dovrebbe farlo sembrare meno simile agli spaghetti.

Sicuramente se hai molti campi, ci deve essere qualche modello che puoi estrarre che costituirebbe una bella unità di funzione a sé stante.E rendi anche il codice più leggibile.

E anche le seguenti sono possibili soluzioni:

  • Distribuisci la logica di convalida invece di archiviarla in un'unica classe.Convalidare quando l'utente li inserisce e quindi convalidare nuovamente a livello di database, ecc...
  • Fare un CustomerFactory classe che mi aiuterebbe a costruire CustomerS
  • Interessante anche la soluzione di @marcio...

Usa semplicemente argomenti predefiniti.In un linguaggio che supporta gli argomenti del metodo predefinito (PHP, ad esempio), potresti farlo nella firma del metodo:

public function doSomethingWith($this = val1, $this = val2, $this = val3)

Esistono altri modi per creare valori predefiniti, ad esempio nei linguaggi che supportano l'overload del metodo.

Naturalmente, potresti anche impostare valori predefiniti quando dichiari i campi, se lo ritieni opportuno.

Dipende solo se è appropriato o meno impostare questi valori predefiniti o se i tuoi oggetti devono essere sempre selezionati durante la costruzione.Questa è davvero una decisione che solo tu puoi prendere.

A meno che non sia più di 1 argomento, utilizzo sempre array o oggetti come parametri del costruttore e faccio affidamento sul controllo degli errori per assicurarmi che i parametri richiesti siano presenti.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top