Come avvisare altri programmatori dell'implementazione di classe


96

Sto scrivendo lezioni che "devono essere usate in un modo specifico" (suppongo che tutte le classi debbano ...).

Ad esempio, creo la fooManagerclasse, che richiede una chiamata, per esempio, a Initialize(string,string). E, per spingere un po 'più in là l'esempio, la classe sarebbe inutile se non ascoltiamo la sua ThisHappenedazione.

Il mio punto è che la classe che sto scrivendo richiede chiamate di metodo. Ma si compilerà bene se non si chiamano questi metodi e finirà con un nuovo FooManager vuoto. Ad un certo punto, o non funzionerà, o forse andrà in crash, a seconda della classe e di ciò che fa. Il programmatore che implementa la mia classe ovviamente guarderebbe al suo interno e realizzerebbe "Oh, non ho chiamato Initialize!", E andrebbe bene.

Ma non mi piace. Ciò che vorrei idealmente è che il codice NON venga compilato se il metodo non è stato chiamato; Immagino che semplicemente non sia possibile. O qualcosa che sarebbe immediatamente visibile e chiaro.

Mi trovo turbato dall'attuale approccio che ho qui, che è il seguente:

Aggiungi un valore booleano privato nella classe e controlla ovunque sia necessario se la classe è inizializzata; in caso contrario, lancerò un'eccezione dicendo "La classe non è stata inizializzata, sei sicuro di chiamare .Initialize(string,string)?".

Sono un po 'd'accordo con questo approccio, ma porta a un sacco di codice che viene compilato e, alla fine, non è necessario per l'utente finale.

Inoltre, a volte è ancora più codice quando ci sono più metodi di un semplice da Initiliazechiamare. Cerco di mantenere le mie lezioni con non troppi metodi / azioni pubblici, ma questo non risolve il problema, mantenendolo ragionevole.

Quello che sto cercando qui è:

  • Il mio approccio è corretto?
  • Ce n'è uno migliore?
  • Che cosa fate / che consigli ragazzi?
  • Sto cercando di risolvere un problema? I colleghi mi hanno detto che è il programmatore a controllare la lezione prima di provare a usarla. Rispetto rispettosamente, ma credo che sia un'altra questione.

In parole povere, sto cercando di capire un modo per non dimenticare di implementare le chiamate quando quella classe viene riutilizzata in seguito, o da qualcun altro.

CHIARIMENTI:

Per chiarire molte domande qui:

  • Sicuramente non sto solo parlando della parte Inizializzazione di una classe, ma piuttosto è tutta la sua vita. Impedisci ai colleghi di chiamare un metodo due volte, assicurandoti di chiamare X prima di Y, ecc. Tutto ciò che finirebbe per essere obbligatorio e nella documentazione, ma che vorrei nel codice e nel modo più semplice e piccolo possibile. Mi è davvero piaciuta l'idea degli Assert, anche se sono abbastanza sicuro che dovrò mescolare alcune altre idee in quanto gli Assert non saranno sempre possibili.

  • Sto usando il linguaggio C #! Come non l'ho detto ?! Sono in un ambiente Xamarin e sto costruendo app mobili di solito usando circa 6-9 progetti in una soluzione, inclusi progetti PCL, iOS, Android e Windows. Sono stato uno sviluppatore per circa un anno e mezzo (scuola e lavoro insieme), quindi le mie dichiarazioni / domande a volte ridicole. Tutto ciò che probabilmente è irrilevante qui, ma troppe informazioni non è sempre una cosa negativa.

  • Non riesco sempre a mettere tutto ciò che è obbligatorio nel costruttore, a causa delle restrizioni della piattaforma e dell'utilizzo di Dependency Injection, con parametri diversi da Interfaces è fuori dal tavolo. O forse la mia conoscenza non è sufficiente, il che è altamente possibile. Il più delle volte non è un problema di inizializzazione, ma di più

come posso assicurarmi che si sia registrato a quell'evento?

come posso assicurarmi che non abbia dimenticato di "interrompere il processo ad un certo punto"

Qui ricordo una lezione di recupero di annunci. Finché la vista in cui l'annuncio è visibile è visibile, la classe recupera un nuovo annuncio ogni minuto. Quella classe ha bisogno di una vista quando costruita dove può visualizzare l'annuncio, che potrebbe ovviamente entrare in un parametro. Ma una volta sparita la vista, è necessario chiamare StopFetching (). Altrimenti la classe continuerebbe a recuperare annunci per una vista che non è nemmeno lì, e questo è male.

Inoltre, quella classe ha eventi che devono essere ascoltati, come ad esempio "AdClicked". Tutto funziona bene se non ascoltato, ma perdiamo il monitoraggio delle analisi lì se i tocchi non sono registrati. L'annuncio funziona comunque, quindi l'utente e lo sviluppatore non vedranno alcuna differenza e le analisi avranno solo dati errati. Deve essere evitato, ma non sono sicuro di come gli sviluppatori possano sapere che devono registrarsi all'evento tao. Questo è un esempio semplificato, ma l'idea è lì, "assicurati che usi l'azione pubblica che è disponibile" e nei momenti giusti ovviamente!


13
Garantire che le chiamate ai metodi di una classe avvengano in un ordine specifico non è possibile in generale. Questo è uno dei tanti, molti problemi equivalenti al problema dell'arresto. Sebbene in alcuni casi sia possibile rendere la non conformità un errore in fase di compilazione , non esiste una soluzione generale. Questo è presumibilmente il motivo per cui poche lingue supportano costrutti che ti permetterebbero di diagnosticare almeno i casi ovvi, anche se l'analisi del flusso di dati è diventata piuttosto sofisticata.
Kilian Foth,


5
La risposta all'altra domanda è irrilevante, la domanda non è la stessa, quindi sono domande diverse. Se qualcuno cerca quella discussione, non scriverebbe il titolo dell'altra domanda.
Gil Sand,

6
Cosa impedisce di unire il contenuto dell'inizializzazione nel ctor? La chiamata deve initializeessere effettuata in ritardo dopo la creazione dell'oggetto? Il direttore sarebbe troppo "rischioso" nel senso che potrebbe generare eccezioni e spezzare la catena della creazione?
Spottato il

7
Questo problema si chiama accoppiamento temporale . Se puoi, cerca di evitare di mettere l'oggetto in uno stato non valido lanciando eccezioni nel costruttore quando incontri input non validi, in questo modo non supererai mai la chiamata newse l'oggetto non è pronto per essere utilizzato. Fare affidamento su un metodo di inizializzazione è destinato a tornare a perseguitarti e dovrebbe essere evitato a meno che non sia assolutamente necessario, almeno questa è la mia esperienza.
Seralizza il

Risposte:


161

In questi casi, è meglio utilizzare il sistema di tipi della tua lingua per aiutarti con una corretta inizializzazione. Come possiamo evitare che un FooManagervenga utilizzato senza essere inizializzato? Impedendo che un FooManagervenga creato senza le informazioni necessarie per inizializzarlo correttamente. In particolare, tutte le inizializzazioni sono a carico del costruttore. Non dovresti mai lasciare che il tuo costruttore crei un oggetto in uno stato illegale.

Ma i chiamanti devono costruire un FooManagerprima di poterlo inizializzare, ad esempio perché FooManagerviene passato come dipendenza.

Non creare un FooManagerse non ne hai uno. Quello che puoi fare invece è passare un oggetto in giro che ti consenta di recuperare un oggetto completamente costruito FooManager, ma solo con le informazioni di inizializzazione. (Nella programmazione funzionale, sto suggerendo di utilizzare un'applicazione parziale per il costruttore.) Ad esempio:

ctorArgs = ...;
getFooManager = (initInfo) -> new FooManager(ctorArgs, initInfo);
...
getFooManager(myInitInfo).fooMethod();

Il problema è che devi fornire le informazioni di init ogni volta che accedi a FooManager.

Se è necessario nella tua lingua, puoi concludere l' getFooManager()operazione in una classe simile a una fabbrica o a un costruttore.

Voglio davvero fare i controlli di runtime che il initialize()metodo è stato chiamato, piuttosto che utilizzare una soluzione a livello di sistema.

È possibile trovare un compromesso. Creiamo una classe wrapper MaybeInitializedFooManagerche ha un get()metodo che restituisce il FooManager, ma genera se FooManagernon è stato completamente inizializzato. Funziona solo se l'inizializzazione viene eseguita tramite il wrapper o se esiste un FooManager#isInitialized()metodo.

class MaybeInitializedFooManager {
  private final FooManager fooManager;

  public MaybeInitializedFooManager(CtorArgs ctorArgs) {
    fooManager = new FooManager(ctorArgs);
  }

  public FooManager initialize(InitArgs initArgs) {
    fooManager.initialize(initArgs);
    return fooManager;
  }

  public FooManager get() {
    if (fooManager.isInitialized()) return fooManager;
    throw ...;
  }
}

Non voglio cambiare l'API della mia classe.

In tal caso, ti consigliamo di evitare i if (!initialized) throw;condizionali in ogni metodo. Fortunatamente, esiste un modello semplice per risolvere questo problema.

L'oggetto fornito agli utenti è solo una shell vuota che delega tutte le chiamate a un oggetto di implementazione. Per impostazione predefinita, l'oggetto di implementazione genera un errore per ogni metodo che non è stato inizializzato. Tuttavia, il initialize()metodo sostituisce l'oggetto di implementazione con un oggetto completamente costruito.

class FooManager {
  private CtorArgs ctorArgs;
  private Impl impl;

  public FooManager(CtorArgs ctorArgs) {
    this.ctorArgs = ctorArgs;
    this.impl = new UninitializedImpl();
  }

  public void initialize(InitArgs initArgs) {
    impl = new MainImpl(ctorArgs, initArgs);
  }

  public X foo() { return impl.foo(); }
  public Y bar() { return impl.bar(); }
}

interface Impl {
  X foo();
  Y bar();
}

class UninitializedImpl implements Impl {
  public X foo() { throw ...; }
  public Y bar() { throw ...; }
}

class MainImpl implements Impl {
  public MainImpl(CtorArgs c, InitArgs i);
  public X foo() { ... }
  public Y bar() { ... }
}

Questo estrae il comportamento principale della classe in MainImpl.


9
Mi piacciono le prime due soluzioni (+1), ma non credo che il tuo ultimo frammento con la sua ripetizione dei metodi in FooManagere in UninitialisedImplsia migliore della ripetizione if (!initialized) throw;.
Bergi,

3
@Bergi Alcune persone adorano troppo le lezioni. Anche se FooManagerha molti metodi, potrebbe essere più facile che dimenticare potenzialmente alcuni dei if (!initialized)controlli. Ma in tal caso probabilmente dovresti preferire interrompere la lezione.
user253751

1
Un MaybeInitializedFoo non sembra migliore di un Foo inizializzato anche a me, ma +1 per dare alcune opzioni / idee.
Matthew James Briggs,

7
+1 La fabbrica è l'opzione corretta. Non è possibile migliorare un progetto senza cambiarlo, quindi IMHO l'ultima parte della risposta su come dimezzare non aiuterà l'OP.
Nathan Cooper,

6
@Bergi Ho trovato estremamente utile la tecnica presentata nell'ultima sezione (vedi anche la tecnica di refactoring “Sostituisci i condizionali attraverso il polimorfismo” e il modello di stato). È facile dimenticare il controllo in un metodo se utilizziamo un if / else, è molto più difficile dimenticare l'implementazione di un metodo richiesto dall'interfaccia. Ancora più importante, MainImpl corrisponde esattamente a un oggetto in cui il metodo di inizializzazione si fonde con il costruttore. Ciò significa che l'implementazione di MainImpl può godere delle maggiori garanzie che ciò offre, il che semplifica il codice principale. ...
Amon,

79

Il modo più efficace e utile per impedire ai clienti di "abusare" di un oggetto è renderlo impossibile.

La soluzione più semplice è quella di fondersi Initializecon il costruttore. In questo modo, l'oggetto non sarà mai disponibile per il client nello stato non inizializzato, quindi l'errore non è possibile. Nel caso in cui non sia possibile eseguire l'inizializzazione nel costruttore stesso, è possibile creare un metodo factory. Ad esempio, se la classe richiede la registrazione di determinati eventi, è possibile richiedere il listener di eventi come parametro nel costruttore o nella firma del metodo factory.

Se è necessario poter accedere all'oggetto non inizializzato prima dell'inizializzazione, è possibile implementare i due stati come classi separate, quindi iniziare con UnitializedFooManagerun'istanza, che ha un Initialize(...)metodo che restituisce InitializedFooManager. I metodi che possono essere chiamati solo nello stato inizializzato esistono solo su InitializedFooManager. Questo approccio può essere esteso a più stati, se necessario.

Rispetto alle eccezioni di runtime, è più lavoro rappresentare gli stati come classi distinte, ma offre anche garanzie in fase di compilazione che non si stanno chiamando metodi non validi per lo stato dell'oggetto e documenta le transizioni di stato in modo più chiaro nel codice.

Ma più in generale, la soluzione ideale è progettare le classi e i metodi senza vincoli come la necessità di chiamare metodi in determinati momenti e in un determinato ordine. Potrebbe non essere sempre possibile, ma può essere evitato in molti casi utilizzando il modello appropriato.

Se hai un accoppiamento temporale complesso (un certo numero di metodi deve essere chiamato in un certo ordine), una soluzione potrebbe essere quella di utilizzare l' inversione del controllo , quindi crei una classe che chiama i metodi nell'ordine appropriato, ma usa metodi o eventi modello per consentire al client di eseguire operazioni personalizzate nelle fasi appropriate del flusso. In questo modo, la responsabilità di eseguire le operazioni nel giusto ordine viene trasferita dal client alla classe stessa.

Un semplice esempio: hai un oggetto Fileche ti permette di leggere da un file. Tuttavia, il client deve chiamare Openprima di poter chiamare il ReadLinemetodo e deve ricordare di chiamare sempre Close(anche se si verifica un'eccezione) dopo di che il ReadLinemetodo non deve più essere chiamato. Questo è un accoppiamento temporale. Può essere evitato avendo un solo metodo che accetta un callback o un delegato come argomento. Il metodo gestisce l'apertura del file, la richiamata e la chiusura del file. Può passare un'interfaccia distinta con un Readmetodo al callback. In questo modo, non è possibile per il cliente dimenticare di chiamare i metodi nell'ordine corretto.

Interfaccia con accoppiamento temporale:

class File {
     /// Must be called on a closed file.
     /// Remember to always call Close() when you are finished
     public void Open();

     /// must be called on on open file
     public string ReadLine();

     /// must be called on an open file
     public void Close();
}

Senza accoppiamento temporale:

class File {
    /// Opens the file, executes the callback, and closes the file again.
    public void Consume(Action<IOpenFile> callback);
}

interface IOpenFile {
    string ReadLine();
}

Una soluzione più pesante sarebbe quella di definire Fileuna classe astratta che richiede l'implementazione di un metodo (protetto) che verrà eseguito sul file aperto. Questo si chiama modello di metodo del modello.

abstract class File {
    /// Opens the file, executes ConsumeOpenFile(), and closes the file again.
    public void Consume();

    /// override this
    abstract protected ConsumeOpenFile();

    /// call this from your ConsumeOpenFile() implementation
    protected string ReadLine();
}

Il vantaggio è lo stesso: il client non deve ricordare di chiamare i metodi in un determinato ordine. Semplicemente non è possibile chiamare i metodi nell'ordine sbagliato.


2
Ricordo anche casi in cui semplicemente non era possibile passare dal costruttore, ma non ho un esempio da mostrare in questo momento
Gil Sand,

2
L'esempio ora descrive un modello di fabbrica, ma la prima frase in realtà descrive il paradigma RAII . Quindi quale dovrebbe essere questa risposta raccomandata?
Ext3h

11
@ Ext3h Non credo che il modello di fabbrica e RAII si escludano a vicenda.
Pieter B,

@PieterB No, non lo sono, una fabbrica può garantire RAII. Ma solo con l'implicazione aggiuntiva che il template / costruttore args (chiamato UnitializedFooManager) non deve condividere uno stato con le istanze ( InitializedFooManager), come Initialize(...)ha effettivamente Instantiate(...)semantico. Altrimenti, questo esempio porta a nuovi problemi se lo stesso modello viene utilizzato due volte da uno sviluppatore. E questo non è possibile prevenire / convalidare staticamente se la lingua non supporta esplicitamente la movesemantica al fine di consumare in modo affidabile il modello.
Ext3h

2
@ Ext3h: raccomando la prima soluzione poiché è la più semplice. Ma se il client deve essere in grado di accedere all'oggetto prima di chiamare Initialize, non è possibile utilizzarlo, ma è possibile utilizzare la seconda soluzione.
JacquesB,

39

Normalmente verificherei solo l'inizializzazione e lancerei (diciamo) IllegalStateExceptionse provi ad usarlo mentre non sei inizializzato.

Tuttavia, se si desidera essere sicuri per la compilazione (ed è lodevole e preferibile), perché non considerare l'inizializzazione come un metodo di fabbrica che restituisce un oggetto costruito e inizializzato, ad es.

ComponentBuilder builder = new ComponentBuilder();
Component forClients = builder.initialise(); // the 'Component' is what you give your clients

e così si controllare la creazione di oggetti e del ciclo di vita, e ai vostri clienti di ottenere il Componentcome risultato della vostra inizializzazione. È effettivamente un'istanza pigra.


1
Buona risposta - 2 buone opzioni in una bella risposta concisa. Altre risposte sopra la presente ginnastica di sistema di tipo che sono (teoricamente) valide; ma per la maggior parte dei casi, queste 2 opzioni più semplici sono le scelte pratiche ideali.
Thomas W,

1
Nota: in .NET, InvalidOperationException è promosso da Microsoft come quello che hai chiamato IllegalStateException.
miroxlav,

1
Non sto votando verso il basso questa risposta, ma in realtà non è una buona risposta. Lanciando IllegalStateExceptiono InvalidOperationExceptionimprovvisamente, un utente non capirà mai cosa ha fatto di sbagliato. Queste eccezioni non dovrebbero diventare un bypass per correggere il difetto di progettazione.
displayName

Noterai che il lancio dell'eccezione è un'opzione possibile e proseguo con l'elaborazione della mia opzione preferita, rendendo questo tempo di compilazione sicuro. Ho modificato la mia risposta a sottolinearlo
Brian Agnew

14

Sto per staccare un po 'dalle altre risposte e non sono d'accordo: è impossibile rispondere a questa domanda senza sapere in quale lingua stai lavorando. Se questo è un piano utile o meno e il giusto tipo di "avvertimento" da dare i tuoi utenti dipendono interamente dai meccanismi forniti dalla tua lingua e dalle convenzioni che altri sviluppatori di quella lingua tendono a seguire.

Se hai un FooManagerin Haskell, sarebbe criminale consentire ai tuoi utenti di crearne uno che non è in grado di gestirli Foo, perché il sistema dei tipi lo rende così facile e quelle sono le convenzioni che gli sviluppatori Haskell si aspettano.

D'altra parte, se stai scrivendo C, i tuoi colleghi sarebbero interamente nei loro diritti di portarti indietro e spararti per la definizione di tipi separati struct FooManagere struct UninitializedFooManagerche supportano diverse operazioni, perché porterebbe a un codice inutilmente complesso per un beneficio molto scarso .

Se stai scrivendo Python, non c'è alcun meccanismo nella lingua che ti permetta di farlo.

Probabilmente non stai scrivendo Haskell, Python o C, ma sono esempi illustrativi in ​​termini di quanto / quanto poco è previsto il sistema di tipi e in grado di fare.

Segui le ragionevoli aspettative di uno sviluppatore nella tua lingua e resisti all'impulso di progettare in modo eccessivo una soluzione che non ha un'implementazione idiomatica naturale (a meno che l'errore non sia così facile da fare e così difficile da cogliere che vale la pena andare all'estremo lunghezze per renderlo impossibile). Se non hai abbastanza esperienza nella tua lingua per giudicare ciò che è ragionevole, segui i consigli di qualcuno che la conosce meglio di te.


Sono un po 'confuso da "Se stai scrivendo Python, non c'è alcun meccanismo nella lingua che ti permetta di farlo." Python ha costruttori ed è abbastanza banale creare metodi di fabbrica. Quindi forse non capisco cosa intendi con "questo"?
AL Flanagan,

3
Con "questo" intendevo un errore di compilazione, al contrario di un runtime.
Patrick Collins,

Solo saltando per citare Python 3.5 che è la versione corrente ha tipi attraverso un sistema di tipi collegabile. È sicuramente possibile ottenere Python in errore prima di eseguire il codice solo perché è stato importato. Fino a 3,5 era del tutto corretto però.
Benjamin Gruenbaum,

Oh va bene. +1 tardivo. Anche confuso, questo è stato molto perspicace.
AL Flanagan,

Mi piace il tuo stile Patrick.
Gil Sand,

4

Poiché sembra che non desideri spedire il controllo del codice al cliente (ma sembra che lo faccia per il programmatore), puoi utilizzare le funzioni assert , se sono disponibili nel tuo linguaggio di programmazione.

In questo modo hai i controlli nell'ambiente di sviluppo (e qualsiasi test che un altro sviluppatore chiamerebbe fallirà prevedibilmente), ma non spedirai il codice al cliente poiché le asserzioni (almeno in Java) vengono compilate solo selettivamente.

Quindi, una classe Java che usa questo sarebbe simile a questa:

/** For some reason, you have to call init and connect before the manager works. */
public class FooManager {
   private int state = 0;

   public FooManager () {
   }

   public synchronized void init() {
      assert state==0 : "you called init twice.";
      // do something
      state = 1;
   }

   public synchronized void connect() {
      assert state==1 : "you called connect before init, or connect twice.";
      // do something
      state = 2;
   }

   public void someWork() {
      assert state==2 : "You did not properly init FooManager. You need to call init and connect first.";
      // do the actual work.
   }
}

Le asserzioni sono un ottimo strumento per verificare lo stato di runtime del programma richiesto, ma in realtà non aspettarti che qualcuno faccia mai del male in un ambiente reale.

Inoltre sono piuttosto sottili e non occupano enormi porzioni di if () lanciano ... sintassi, non devono essere catturati, ecc.


3

Esaminare questo problema più in generale rispetto alle risposte attuali, che si sono concentrate principalmente sull'inizializzazione. Considera un oggetto che avrà due metodi a()e b(). Il requisito è che a()viene sempre chiamato prima b(). È possibile creare un controllo in fase di compilazione che ciò avvenga restituendo un nuovo oggetto a()e spostandosi b()nel nuovo oggetto anziché in quello originale. Esempio di implementazione:

class SomeClass
{
   private int valueRequiredByMethodB;

   public IReadyForB a (int v) { valueRequiredByMethodB = v; return new ReadyForB(this); }

   public interface IReadyForB { public void b (); }

   private class ReadyForB : IReadyForB
   {
      SomeClass owner;
      private ReadyForB (SomeClass owner) { this.owner = owner; }
      public void b () { Console.WriteLine (owner.valueRequiredByMethodB); }
   }
}

Ora, è impossibile chiamare b () senza prima chiamare a (), poiché è implementato in un'interfaccia nascosta fino a quando non viene chiamato a (). Certo, questo è un grande sforzo, quindi di solito non userei questo approccio, ma ci sono situazioni in cui può essere utile (specialmente se la tua classe verrà riutilizzata in molte situazioni da programmatori che potrebbero non essere familiarità con la sua implementazione, o nel codice in cui l'affidabilità è fondamentale). Si noti inoltre che questa è una generalizzazione del modello del builder, come suggerito da molte delle risposte esistenti. Funziona praticamente allo stesso modo, l'unica vera differenza è dove sono archiviati i dati (nell'oggetto originale piuttosto che in quello restituito) e quando è previsto che vengano utilizzati (in qualsiasi momento rispetto all'inizializzazione).


2

Quando implemento una classe base che richiede informazioni di inizializzazione extra o collegamenti ad altri oggetti prima che diventi utile, tendo a rendere quella classe base astratta, quindi definisco diversi metodi astratti in quella classe che vengono utilizzati nel flusso della classe base (come abstract Collection<Information> get_extra_information(args);e abstract Collection<OtherObjects> get_other_objects(args);che per contratto di eredità devono essere implementati dalla classe concreta, costringendo l'utente di quella classe base a fornire tutto ciò che è richiesto dalla classe base.

Pertanto, quando implemento la classe base, so immediatamente ed esplicitamente cosa devo scrivere per far funzionare correttamente la classe base, dato che ho solo bisogno di implementare i metodi astratti e basta.

EDIT: per chiarire, questo è quasi lo stesso che fornire argomenti al costruttore della classe base, ma le implementazioni del metodo astratto consentono all'implementazione di elaborare gli argomenti passati alle chiamate al metodo astratto. O anche nel caso in cui non vengano utilizzati argomenti, può essere comunque utile se il valore di ritorno del metodo astratto dipendesse da uno stato che è possibile definire nel corpo del metodo, il che non è possibile quando si passa la variabile come argomento a il costruttore. Concesso, puoi sempre passare un argomento che avrà un comportamento dinamico basato sullo stesso principio se preferisci usare la composizione sull'ereditarietà.


2

La risposta è: Sì, no, e talvolta. :-)

Alcuni dei problemi che descrivi possono essere facilmente risolti, almeno in molti casi.

Il controllo in fase di compilazione è sicuramente preferibile al controllo in fase di esecuzione, che è preferibile a nessun controllo.

Tempo di esecuzione RE:

È almeno concettualmente semplice forzare la chiamata di funzioni in un certo ordine, ecc., Con i controlli di runtime. Basta inserire i flag che indicano quale funzione è stata eseguita e lasciare che ciascuna funzione inizi con qualcosa del tipo "se non pre-condizione-1 o non pre-condizione-2, allora getta un'eccezione". Se i controlli diventano complessi, è possibile inserirli in una funzione privata.

Puoi fare in modo che alcune cose accadano automagicamente. Per fare un semplice esempio, i programmatori parlano spesso di "popolazione pigra" di un oggetto. Quando viene creata l'istanza, si imposta un flag di popolamento su false o un riferimento all'oggetto chiave su null. Quindi quando viene chiamata una funzione che necessita dei dati rilevanti, controlla la bandiera. Se è falso, popola i dati e imposta il flag su true. Se è vero, continua solo supponendo che i dati siano lì. Puoi fare la stessa cosa con altre condizioni preliminari. Imposta un flag nel costruttore o come valore predefinito. Quindi quando raggiungi un punto in cui avrebbe dovuto essere chiamata una funzione pre-condizione, se non è stata ancora chiamata, chiamala. Naturalmente questo funziona solo se si dispone dei dati per chiamarlo a quel punto, ma in molti casi è possibile includere tutti i dati richiesti nei parametri della funzione "

Tempo di compilazione RE:

Come altri hanno già detto, se è necessario inizializzare prima che un oggetto possa essere utilizzato, questo mette l'inizializzazione nel costruttore. Questo è un consiglio abbastanza tipico per OOP. Se possibile, dovresti fare in modo che il costruttore crei un'istanza valida e utilizzabile dell'oggetto.

Vedo alcune discussioni su Cosa fare se è necessario passare i riferimenti all'oggetto prima che venga inizializzato. Non riesco a pensare a un vero esempio di ciò dalla parte superiore della mia testa, ma suppongo che potrebbe accadere. Sono state suggerite soluzioni, ma le soluzioni che ho visto qui e a cui riesco a pensare sono disordinate e complicano il codice. Ad un certo punto devi chiedere: vale la pena creare un codice brutto e difficile da capire, così possiamo avere un controllo in fase di compilazione piuttosto che in fase di esecuzione? O sto creando molto lavoro per me e gli altri per un obiettivo che è bello ma non richiesto?

Se due funzioni devono sempre essere eseguite insieme, la soluzione semplice è renderle una funzione. Come se ogni volta che aggiungessi un annuncio pubblicitario a una pagina dovessi anche aggiungere un gestore metriche per esso, quindi inserisci il codice per aggiungere il gestore metriche all'interno della funzione "aggiungi annuncio".

Alcune cose sarebbero quasi impossibili da controllare al momento della compilazione. Come un requisito che una determinata funzione non può essere chiamata più di una volta. (Ho scritto molte funzioni del genere. Di recente ho scritto una funzione che cerca i file in una directory magica, elabora quelli che trova e quindi li elimina. Naturalmente una volta eliminato il file non è possibile rieseguire. Ecc.) Non conosco nessun linguaggio che abbia una funzione che ti consenta di impedire che una funzione venga chiamata due volte sulla stessa istanza al momento della compilazione. Non so come il compilatore potesse capire definitivamente che stava succedendo. Tutto quello che puoi fare è inserire i controlli del tempo di esecuzione. Quel particolare controllo del tempo di esecuzione è ovvio, non è vero? "se già-stato-qui = vero quindi getta l'eccezione altrimenti imposta già-stato-qui = vero".

RE impossibile da applicare

Non è raro che una classe richieda una sorta di ripulitura al termine: chiusura di file o connessioni, liberazione della memoria, scrittura dei risultati finali nel DB, ecc. Non esiste un modo semplice per forzare che ciò avvenga in entrambi i tempi di compilazione o tempo di esecuzione. Penso che la maggior parte delle lingue OOP abbia una sorta di disposizione per una funzione di "finalizzatore" che viene chiamata quando un'istanza viene raccolta in modo inutile, ma penso che la maggior parte dica anche che non possono garantire che verrà mai eseguita. I linguaggi OOP spesso includono una funzione "dispose" con una sorta di disposizione per impostare un ambito sull'uso di un'istanza, una clausola "using" o "with" e quando il programma esce da tale ambito viene eseguita la disposizione. Ma ciò richiede che il programmatore utilizzi l'identificatore di ambito appropriato. Non costringe il programmatore a farlo bene,

Nei casi in cui è impossibile forzare il programmatore a utilizzare correttamente la classe, provo a farlo in modo che il programma esploda se lo fa in modo sbagliato, piuttosto che dare risultati errati. Esempio semplice: ho visto molti programmi che inizializzano un sacco di dati su valori fittizi, in modo che l'utente non ottenga mai un'eccezione puntatore null anche se non riesce a chiamare le funzioni per popolare correttamente i dati. E mi chiedo sempre perché. Stai facendo di tutto per rendere gli insetti più difficili da trovare. Se, quando un programmatore non riusciva a chiamare la funzione "carica dati" e quindi provava allegramente a usare i dati, riceveva un'eccezione puntatore null, l'eccezione gli avrebbe mostrato rapidamente dove si trovava il problema. Ma se lo nascondi rendendo i dati vuoti o zero in questi casi, il programma potrebbe essere eseguito fino al completamento ma produrre risultati imprecisi. In alcuni casi il programmatore potrebbe anche non rendersi conto che c'era un problema. Se se ne accorge, potrebbe essere necessario seguire un lungo sentiero per scoprire dove si è sbagliato. Meglio fallire presto che tentare coraggiosamente di andare avanti.

In generale, certo, è sempre bene quando è possibile fare un uso errato di un oggetto semplicemente impossibile. È utile se le funzioni sono strutturate in modo tale che il programmatore non abbia semplicemente modo di effettuare chiamate non valide o se chiamate non valide generano errori durante la compilazione.

Ma c'è anche un punto in cui devi dire: il programmatore dovrebbe leggere la documentazione sulla funzione.


1
Per quanto riguarda la forzatura della pulizia, esiste un modello che può essere utilizzato in cui una risorsa può essere allocata solo chiamando un metodo particolare (ad esempio in un linguaggio OO tipico potrebbe avere un costruttore privato). Questo metodo alloca la risorsa, chiama una funzione (o un metodo di un'interfaccia) che le viene passata con un riferimento alla risorsa e quindi distrugge la risorsa una volta restituita questa funzione. Oltre alla terminazione improvvisa del thread, questo modello garantisce che la risorsa sia sempre distrutta. È un approccio comune nei linguaggi funzionali, ma l'ho visto anche usato nei sistemi OO con buoni risultati.
Jules,


2

Sì, il tuo istinto è perfetto. Molti anni fa ho scritto articoli su riviste (un po 'come questo posto, ma su un albero morto e pubblicato una volta al mese) discutendo di debugging , abugging e antibugging .

Se riesci a far sì che il compilatore rilevi l'abuso, questo è il modo migliore. Le funzionalità della lingua disponibili dipendono dalla lingua e non è stato specificato. Tecniche specifiche sarebbero comunque dettagliate per le singole domande su questo sito.

Ma avere un test per rilevare l'utilizzo in fase di compilazione anziché in fase di runtime è davvero lo stesso tipo di test: devi ancora sapere di chiamare prima le funzioni appropriate e usarle nel modo giusto.

Progettare il componente per evitare che anche essere un problema in primo luogo sia molto più sottile, e mi piace il Buffer è come l' esempio Element . La parte 4 (pubblicata vent'anni fa! Wow!) Contiene un esempio con discussione che è abbastanza simile al tuo caso: questa classe deve essere usata con attenzione, poiché buffer () potrebbe non essere chiamato dopo aver iniziato a usare read (). Piuttosto, deve essere usato solo una volta, immediatamente dopo la costruzione. Avere la classe a gestire il buffer in modo automatico e invisibile per il chiamante eviterebbe concettualmente il problema.

Chiedete, se i test possono essere eseguiti in fase di esecuzione per garantire un uso corretto, dovreste farlo?

Direi di sì . Un giorno risparmierà al tuo team un sacco di lavoro di debug, quando il codice viene mantenuto e l'utilizzo specifico viene disturbato. Il test può essere impostato come un insieme formale di vincoli e invarianti che possono essere testati. Ciò non significa che l'overhead dello spazio extra per tracciare lo stato e il lavoro per fare il controllo sia sempre lasciato in ogni chiamata. È nella classe, quindi potrebbe essere chiamato, e il codice documenta i vincoli e le ipotesi reali.

Forse il controllo viene eseguito solo in una build di debug.

Forse il controllo è complesso (si pensi al heapcheck, ad esempio), ma è presente e può essere fatto di tanto in tanto, o chiamate aggiunte qua e là quando il codice viene elaborato e qualche problema è emerso, o per fare test specifici per assicurarsi che non vi siano problemi di questo tipo in un programma di test di unità o test di integrazione che si collega alla stessa classe.

Dopotutto potresti decidere che il sovraccarico è banale. Se la classe esegue il file I / O, un byte di stato extra e un test per questo non è nulla. Le classi iostream comuni verificano che lo stream sia in cattivo stato.

Il tuo istinto è buono. Continuate così!


0

Il principio dell'Information Hiding (Encapsulation) è che le entità esterne alla classe non dovrebbero sapere più di quanto è necessario per utilizzare questa classe correttamente.

Il tuo caso sembra che tu stia cercando di far funzionare correttamente un oggetto di una classe senza dire abbastanza agli utenti esterni della classe. Hai nascosto più informazioni di quelle che dovresti avere.

  • O chiedere esplicitamente le informazioni richieste nel costruttore;
  • Oppure mantieni intatti i costruttori e modifica i metodi in modo che, per poterli chiamare, devi fornire i dati mancanti.

Perle di saggezza:

* In entrambi i casi è necessario riprogettare la classe e / o il costruttore e / o i metodi.

* Non progettando correttamente e facendo morire di fame la classe dalle informazioni corrette si rischia che la classe si interrompa in non uno ma potenzialmente in più luoghi.

* Se hai scritto male le eccezioni e i messaggi di errore, la tua classe darà ancora di più su se stessa.

* Infine, se si suppone che l'estraneo sappia che deve inizializzare a, bec e quindi chiamare d (), e (), f (int), allora si ha una perdita nell'astrazione.


0

Dato che hai specificato che stai usando C #, una soluzione praticabile per la tua situazione è quella di sfruttare gli analizzatori di codice Roslyn . Ciò ti consentirà di rilevare immediatamente le violazioni e persino di suggerire correzioni di codice .

Un modo per implementarlo sarebbe decorare i metodi temporalmente accoppiati con un attributo che specifica l'ordine in cui i metodi devono essere chiamati 1 . Quando l'analizzatore trova questi attributi in una classe, verifica che i metodi siano chiamati in ordine. Ciò renderebbe la tua classe simile al seguente:

public abstract class Foo
{
   [TemporallyCoupled(1)]
   public abstract void Init();

   [TemporallyCoupled(2)]
   public abstract void DoBar();

   [TemporallyCoupled(3)]
   public abstract void Close();
}

1: Non ho mai scritto un analizzatore di codice Roslyn, quindi questa potrebbe non essere la migliore implementazione. L'idea di utilizzare Roslyn Code Analyzer per verificare che l'API venga utilizzata correttamente è comunque valida al 100%.


-1

Il modo in cui ho risolto questo problema in precedenza era con un costruttore privato e un MakeFooManager()metodo statico all'interno della classe. Esempio:

public class FooManager
{
     public string Foo { get; private set; }
     public string Bar { get; private set; }

     private FooManager(string foo, string bar)
     {
         Foo = foo;
         Bar = bar;
     }

     public static FooManager MakeFooManager(string foo, string bar)
     {
         // Can do other checks here too.
         if(foo == null || bar == null)
         {
             return null;
         }
         else
         {
             return new FooManager(foo, bar);
         }
     }
}

Poiché un costruttore è implementato, non c'è modo per nessuno di creare un'istanza FooManagersenza passare attraverso MakeFooManager().


1
questo sembra semplicemente ripetere il punto sollevato e spiegato in una risposta precedente che era stata pubblicata diverse ore prima
moscerino del

1
questo ha qualche vantaggio rispetto al semplice controllo null nel ctor? sembra che tu abbia appena creato un metodo che non fa altro che avvolgere un altro metodo. perché?
Sara,

Inoltre, perché sono le variabili membro foo e bar?
Patrick M,

-1

Esistono diversi approcci:

  1. Rendi la lezione facile da usare giusta e difficile da usare sbagliata. Alcune delle altre risposte si concentrano esclusivamente su questo punto, quindi citerò semplicemente RAII e il modello del costruttore .

  2. Fai attenzione a come usare i commenti. Se la classe si spiega da sé, non scrivere commenti. * In questo modo le persone hanno maggiori probabilità di leggere i tuoi commenti quando la classe ne ha effettivamente bisogno. E se hai uno di questi rari casi in cui una classe è difficile da usare correttamente e necessita di commenti, includi degli esempi.

  3. Usa assert nelle tue build di debug.

  4. Genera eccezioni se l'utilizzo della classe non è valido.

* Questi sono i tipi di commenti che non dovresti scrivere:

// gets Foo
GetFoo();
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.