Condividi tramite


Il presente articolo è stato tradotto automaticamente.

Cutting Edge:

Contratti di codice: ereditarietà e il principio di Liskov

Dino Esposito

Dino EspositoProprio come i contratti reali, contratti software si legano a ulteriori vincoli e costano qualcosa in termini di tempo. Quando un contratto è in piedi, si potrebbe voler fare in modo che voi non rompere i suoi termini. Quando si tratta di contratti software — tra cui il codice contratti in Microsoft.NET Framework — quasi ogni sviluppatore alla fine si manifesterà dubbi circa i costi computazionali di avere contratti intorno alle classi. Contratti sono appropriate per il software, indipendentemente dal tipo di generazione? O sono contratti invece principalmente un aiuto di debug che dovrebbe essere spogliato di codice al dettaglio?

Eiffel, la lingua prima di introdurre contratti software, ha parole chiave del linguaggio nativo per definire le precondizioni, postcondizioni e invarianti. Eiffel, pertanto, contratti sono parte del linguaggio. Se utilizzata nel codice sorgente di una classe, contratti diventano parte integrante del codice.

In.NET, però, contratti, fanno parte del quadro e non appartengono a lingue supportate. Questo significa che il controllo runtime può essere attivato o disabili a saranno. In particolare, in.NET ti è permesso di decidere su contratti su una base di configurazione al compilazione. In Java, le cose sono quasi le stesse. Si utilizza un quadro esterno e o aggiungere codice contratto alle fonti per essere compilato o chiedere strumenti intorno al framework di modificare di conseguenza il bytecode.

In questo articolo, che tratterò alcuni scenari dove codice contratti rivelarsi particolarmente utili in guida verso una maggiore qualità della progettazione software globale.

Quali sono i contratti di codice per

Un sempreverde migliori prassi degli sviluppatori di software sono scrittura di metodi che controllare attentamente eventuali parametri di input che ricevono. Se il parametro di input non corrisponde alle aspettative del metodo, viene generata un'eccezione. Questo è noto come il if-then-throw modello. Con contratti precondizione, questo stesso codice sembra più bello e più compatta. Più interessante, legge anche meglio, perché una precondizione consente di affermare chiaramente appena che cosa ha richiesto invece di testare contro ciò che non è auspicabile. Così, a prima vista, la contratti software guardare semplicemente come un approccio più bello da scrivere per evitare eccezioni nei metodi della classe. Beh, c'è molto molto di più proprio questo.

Il semplice fatto che pensate di contratti per ogni metodo indica che stai pensando ora più circa il ruolo di tali metodi. Alla fine, progettazione ottiene terser e terser. E contratti rappresentano anche una preziosa forma di documentazione, soprattutto per scopi di refactoring.

Codice contratti, tuttavia, non sono limitati a precondizioni, anche se le premesse sono la parte più facile dei contratti software per raccogliere. La combinazione di precondizioni, postcondizioni e invarianti — e l'ampia applicazione di loro in tutto il codice — ti dà un vantaggio decisivo e veramente porta a qualche codice di qualità superiore.

Asserzioni vs. Codice contratti vs. Test

Codice contratti non sono interamente come asserzioni e altri strumenti di debug. Mentre contratti possono aiutarti a tenere traccia dei bug, essi non sostituiscono un buon debugger o un insieme ben fatto di unit test. Come asserzioni, codice contratti indicano una condizione che deve essere verificata a un certo punto durante l'esecuzione di un programma.

Un'asserzione che non è un sintomo che qualcosa è sbagliato da qualche parte. Un'asserzione, tuttavia, non può dire perché non è riuscito e dove ha avuto origine il problema. Un contratto di codice che non riesce, invece, ti dice molto di più. Condivide particolari circa il tipo di errore. Così, ad esempio, si può imparare Se l'eccezione è stato generato perché un determinato metodo ricevuto un valore inaccettabile, non è riuscito nel calcolo del valore restituito previsto o detiene uno stato non valido. Considerando che un'asserzione dice solo circa un sintomo riconosciuto cattivo, un codice contratto può mostrare informazioni preziose su come dovrebbe essere utilizzato il metodo. Queste informazioni possono infine aiutare a capire che cosa deve essere fissato al fine di fermare un'asserzione data di violare.

Come contratti software si riferiscono a unit test? Ovviamente, uno non esclude l'altra e le due caratteristiche sono sorta di ortogonali. Un test harness è un programma esterno che funziona applicando un input fisso per classi scelte e metodi per vedere come si comportano. I contratti sono un modo per le classi per gridare quando qualcosa è andato storto. Al fine di testare i contratti, tuttavia, è necessario eseguire il codice.

Unit test sono un ottimo strumento per la cattura di regressione dopo un profondo processo di refactoring. Contratti sono forse più informativo di test per documentare il comportamento previsto dei metodi. Per ottenere il valore di progetto di test, deve essere pratica sviluppo basato su test (TDD). I contratti sono probabilmente uno strumento più semplice rispetto a TDD ai metodi documento e design.

Contratti aggiungere informazioni aggiuntive al codice e lasciano a voi decidere se questa informazione dovrebbe rendere ai file binari distribuiti. Unit test comporta un progetto esterno che può stimare come sta facendo il codice. Se si compila informazioni di contratto o no, avendo contratto chiare informazioni in anticipo aiuta immensamente come ausilio di documentazione e di design.

Contratti di codice e i dati di Input

Contratti si riferiscono a condizioni che tengono sempre vere nel flusso normale esecuzione di un programma. Ciò sembra suggerire che il luogo ideale dove si potrebbe desiderare di utilizzare contratti sia nelle librerie interne che sono solo soggetti all'ingresso rigorosamente controllate dallo sviluppatore. Classi che vengono esposte direttamente all'input dell'utente non sono necessariamente un buon posto per i contratti. Se si impostano precondizioni su dati di input non filtrati, il contratto può non riuscire e generare un'eccezione. Ma è questo veramente che cosa volete? La maggior parte del tempo, si desidera con garbo degradare o restituire un messaggio educato all'utente. Non volete un'eccezione e non si vuole buttare e quindi intercettare un'eccezione solo per recuperare con grazia.

In.NET, contratti di codice appartengono alle librerie e possono essere le annotazioni di dati un buon complemento (e, in alcuni casi, un sostituto per). Annotazioni di dati fanno un grande lavoro in relazione all'interfaccia utente perché in Silverlight e ASP.NET disporre di componenti capiscono tali annotazioni e modificare il codice o il codice HTML di output. Sul livello di dominio, però, è spesso necessario più appena attributi e contratti di codice sono un sostituto ideale. Non necessariamente sto affermando che non si può ottenere le stesse funzionalità con attributi che è possibile con contratti di codice. Trovo, tuttavia, che in termini di leggibilità ed espressività, i risultati sono meglio con codice contratti di attributi. (A proposito, questo è precisamente perché la squadra di codice contratti preferisce pianura codice sopra attributi.)

Contratti ereditati

Contratti software sono ereditabili in quasi ogni piattaforma che supporta il loro, e la.NET Framework non è fa eccezione. Quando si deriva una classe nuova da uno esistente, la classe derivata preleva il comportamento, il contesto e contratti del genitore. Che sembra essere il corso naturale delle cose. Ereditarietà dei contratti non pongono alcun problema per invarianti e postcondizioni. È un po' problematico per precondizioni, però. Let's affrontare invarianti e considerare il codice in Figura 1.

Figura 1 ereditando invarianti

public class Rectangle
{
  public virtual Int32 Width { get; set; }
  public virtual Int32 Height { get; set; }

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(Width > 0);
    Contract.Invariant(Height > 0);
  }
}
public class Square : Rectangle
{
  public Square()
  {
  }

  public Square(Int32 size)
  {
    Width = size;
    Height = size;
  }

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(Width == Height);
  }
  ...
}

La classe base rettangolo ha due invarianti: larghezza e altezza sono maggiore di zero. La classe derivata piazza aggiunge un'altra condizione invariante: deve corrispondere la larghezza e l'altezza. Anche dal punto di vista logico, questo ha un senso. Una piazza è come un rettangolo tranne che ha un ulteriore vincolo: larghezza e altezza deve essere sempre lo stesso.

Per postcondizioni, le cose funzionano principalmente nello stesso modo. Una classe derivata che esegue l'override di un metodo e aggiunge ulteriori postcondizioni appena potenzia le funzionalità della classe base e agisce come un caso speciale di classe padre che fa tutto ciò fa il genitore e altro ancora.

Che dire di precondizioni, allora? Questo è esattamente perché Riassumendo contratti attraverso una gerarchia di classi è un'operazione delicata. Logicamente parlando, un metodo della classe è la stessa di una funzione matematica. Entrambi ottenere alcuni valori di input e produrre alcuni output. In matematica, l'intervallo di valori, prodotto da una funzione è conosciuto come il codominio; il dominio è la gamma dei possibili valori di input. Aggiungendo invarianti e postcondizioni a un metodo della classe derivata, è solo aumentare le dimensioni del codominio del metodo. Ma con l'aggiunta di precondizioni, limitare il dominio del metodo. È questo qualcosa che si dovrebbe davvero essere preoccupati? Continuate a leggere.

Il principio di Liskov

SOLIDO è un acronimo popolare che deriva dalle iniziali di cinque principi chiave della progettazione software, tra cui responsabilità singola, aperto/chiuso, segregazione di interfaccia e inversione di dipendenza. I l in solido sta per il principio di sostituzione di Liskov. Si può imparare molto di più circa il principio di Liskov a bit.ly/lKXCxF.

In poche parole, il principio di Liskov afferma che dovrebbe sempre essere sicuro di utilizzare una sottoclasse in qualsiasi luogo in cui è prevista la classe padre. Enfatico come può sembrare, questo è non qualcosa che si ottiene dalla scatola con orientamento oggetto pianura. Nessun compilatore di qualsiasi linguaggio object-oriented può fare la magia di garantire che il principio tiene sempre.

È responsabilità di uno sviluppatore preciso per assicurare che sia sicuro da usare qualsiasi classe derivata in luoghi in cui è prevista la classe padre. Notate che ho detto "sicuro". Orientamento oggetto pianura rende possibile utilizzare eventuali classi derivate in luoghi in cui è prevista la classe padre. "Possibile" non è la stessa di "sicuro". Per soddisfare il principio di Liskov, è necessario rispettare una semplice regola: il dominio di un metodo non può essere ridotto in una sottoclasse.

Contratti di codice e il principio di Liskov

A parte la definizione astratta e formale, il principio di Liskov ha molto a che fare con contratti software e può facilmente essere riformulato in termini di una tecnologia specifica, ad esempio.NET codice contratti. Il punto chiave è che una classe derivata non può solo aggiungere precondizioni. In tal modo, esso si restringerà l'intervallo dei valori possibili di essere accettato per un metodo, possibilmente creando gli errori di runtime.

È importante notare che violare il principio non necessariamente risulta in un'eccezione runtime o malfunzionamento. Tuttavia, è un segno che un controesempio possibile rompe il codice. In altre parole, gli effetti della violazione possono ripple attraverso l'intero codebase e mostrare sintomi nefasti in zone apparentemente estranei. Rende l'intero codebase più difficile da mantenere e far evolvere — un peccato mortale in questi giorni. Immaginate che avete il codice in Figura 2.

Figura 2 che illustrano il principio di Liskov

public class Rectangle
{
  public Int32 Width { get; private set; }
  public Int32 Height { get; private set; }

  public virtual void SetSize(Int32 width, Int32 height)
  {
    Width = width;
    Height = height;
  }
}
public class Square : Rectangle
{
  public override void SetSize(Int32 width, Int32 height)
  {
    Contract.Requires<ArgumentException>(width == height);
    base.SetSize(width, width);
  }
}

La piazza di classe eredita da rettangolo e aggiunge solo una precondizione. A questo punto, il codice seguente (che rappresenta un controesempio possibile) avrà esito negativo:

private static void Transform(Rectangle rect)
  {
    // Height becomes twice the width
    rect.SetSize(rect.Width, 2*rect.Width);
  }

Il metodo Transform è stato originariamente scritto per affrontare le istanze della classe Rectangle, e fa il suo lavoro abbastanza bene. Supponiamo che un giorno si estendere il sistema e inizia a passaggio di istanze di Piazza allo stesso codice (intatto), come illustrato di seguito:

var square = new Square();
square.SetSize(20, 20);
Transform(square);

A seconda del rapporto tra piazza e rettangolo, il metodo Transform può iniziare non riuscendo senza apparente spiegazione.

Peggio ancora, si può facilmente posto come risolvere il problema, ma a causa della gerarchia delle classi, non può essere qualcosa che si desidera prendere alla leggera. Così si finisce il bug di fissaggio con una soluzione alternativa, come illustrato di seguito:

private static void Transform(Rectangle rect)
{
  // Height becomes twice the width
  if (rect is Square)
  {
    // ...
return;
  }
  rect.SetSize(rect.Width, 2*rect.Width);
}

Ma indipendentemente dal vostro sforzo, il famigerato palla di fango ha appena iniziato a crescere più grande. La cosa bella.NET e il compilatore c# è che se si utilizza codice contratti per esprimere le precondizioni, si ottiene un avviso dal compilatore se stai violando il principio di Liskov (vedere la figura 3).

The Warning You Get When You’re Violating the Liskov Principle

Figura 3 l'avvertimento che si ottiene quando si sta violando il principio di Liskov

Meno comprensibile e applicati a meno solido principio

Dopo aver insegnato un.Classe netto di progettazione per un paio d'anni ormai, penso che posso tranquillamente dire che dei principi solidi, il principio di Liskov è di gran lunga quelli meno comprensibili e applicata. Molto spesso, un comportamento strano rilevato in un sistema software può essere rintracciato a una violazione del principio Liskov. Piacevolmente abbastanza, contratti di codice può aiutare in modo significativo in questo settore, se solo si prende uno sguardo attento al avvisi del compilatore.

Dino Esposito è l'autore di "Programming Microsoft ASP.NET 4" (Microsoft Press, 2011) e uno degli autori di "Microsoft .NET: Architecting Applications for the Enterprise" (Microsoft Press, 2008). Con sede in Italia, Esposito è un oratore frequente a eventi del settore in tutto il mondo. Si può seguirlo su Twitter a twitter.com/despos.

Grazie all'esperto tecnica seguente per la revisione di questo articolo: Manuel fahndrich