Come specificare una precondizione (LSP) in un'interfaccia in C #?


11

Diciamo che abbiamo la seguente interfaccia:

interface IDatabase { 
    string ConnectionString{get;set;}
    void ExecuteNoQuery(string sql);
    void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

Il presupposto è che ConnectionString deve essere impostato / inizializzato prima di poter eseguire uno qualsiasi dei metodi.

Questa condizione preliminare può essere in qualche modo raggiunta passando una stringa di connessione tramite un costruttore se IDatabase fosse una classe astratta o concreta -

abstract class Database { 
    public string ConnectionString{get;set;}
    public Database(string connectionString){ ConnectionString = connectionString;}

    public void ExecuteNoQuery(string sql);
    public void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

In alternativa, possiamo creare connectionString un parametro per ciascun metodo, ma sembra peggiore della semplice creazione di una classe astratta:

interface IDatabase { 
    void ExecuteNoQuery(string connectionString, string sql);
    void ExecuteNoQuery(string connectionString, string[] sql);
    //Various other methods all with the connectionString parameter
}

Domande -

  1. C'è un modo per specificare questa condizione preliminare all'interno dell'interfaccia stessa? È un "contratto" valido, quindi mi chiedo se esiste una caratteristica o un modello linguistico per questo (la soluzione di classe astratta è più di un hack imo oltre alla necessità di creare due tipi - un'interfaccia e una classe astratta - ogni volta questo è necessario)
  2. Questa è più di una curiosità teorica - Questa condizione preliminare rientra effettivamente nella definizione di una condizione preliminare come nel contesto di LSP?

2
Con "LSP" state parlando del principio di sostituzione di Liskov? Il principio "se è un ciarlatano come un'anatra ma ha bisogno di batterie non è un'anatra"? Perché a mio avviso è più una violazione dell'ISP e dell'SRP forse anche dell'OCP ma non proprio dell'LSP.
Sebastien,

2
Proprio per questo sai, l'intero concetto di "ConnectionString deve essere impostato / inizializzato prima che uno qualsiasi dei metodi possa essere eseguito" è un esempio di accoppiamento temporale blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling e dovrebbe essere evitato, se possibile.
Richiban,

Seemann è davvero un grande fan di Abstract Factory.
Adrian Iftode,

Risposte:


10
  1. Sì. Da .Net 4.0 in poi, Microsoft fornisce Contratti di codice . Questi possono essere utilizzati per definire i presupposti nel modulo Contract.Requires( ConnectionString != null );. Tuttavia, per far funzionare questo per un'interfaccia, avrai comunque bisogno di una classe di supporto IDatabaseContract, a cui si attacca IDatabase, e la condizione preliminare deve essere definita per ogni singolo metodo della tua interfaccia in cui deve valere. Vedi qui per un ampio esempio di interfacce.

  2. , l'LSP si occupa sia di parti sintattiche che semantiche di un contratto.


Non pensavo che potessi usare i Contratti di codice in un'interfaccia. L'esempio fornito mostra che vengono utilizzati nelle classi. Le classi sono conformi a un'interfaccia, ma l'interfaccia stessa non contiene informazioni sul Contratto di codice (un peccato, davvero. Sarebbe il posto ideale per dirlo).
Robert Harvey,

1
@RobertHarvey: sì, hai ragione. Tecnicamente, hai bisogno di una seconda classe, ovviamente, ma una volta definito, il contratto funziona automaticamente per ogni implementazione dell'interfaccia.
Doc Brown,

21

Connessione e query sono due preoccupazioni separate. Come tali, dovrebbero avere due interfacce separate.

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

interface IDatabase
{
    public void ExecuteNoQuery(string sql);
    public void ExecuteNoQuery(string[] sql);
}

Ciò garantisce che IDatabasesarà collegato quando utilizzato e rende il client non dipendente dall'interfaccia di cui non ha bisogno.


Potrebbe essere più esplicito su "questo è un modello per far rispettare le precondizioni attraverso i tipi"
Caleth,

@Caleth: questo non è un "modello generale di applicazione delle precondizioni". Questa è una soluzione per questo requisito specifico di assicurare che la connessione avvenga prima di ogni altra cosa. Altre condizioni preliminari avranno bisogno di soluzioni diverse (come quella che ho menzionato nella mia risposta). Vorrei aggiungere questo requisito, preferirei chiaramente il suggerimento di Euphoric al mio, perché è molto più semplice e non necessita di alcun componente aggiuntivo di terze parti.
Doc Brown,

La specifica requrement che qualcosa accade prima di qualcos'altro è ampiamente applicabile. Penso anche che la tua risposta si adatti meglio a questa domanda , ma questa risposta può essere migliorata
Caleth,

1
Questa risposta manca completamente il punto. L' IDatabaseinterfaccia definisce un oggetto in grado di stabilire una connessione a un database e quindi eseguire query arbitrarie. Si è l'oggetto che funge da confine tra il database e il resto del codice. Pertanto, questo oggetto deve mantenere lo stato (come una transazione) che può influire sul comportamento delle query. Metterli nella stessa classe è molto pratico.
jpmc26,

4
@ jpmc26 Nessuna delle tue obiezioni ha senso, poiché lo stato può essere mantenuto all'interno della classe che implementa IDatabase. Può anche fare riferimento alla classe padre che l'ha creata, ottenendo così l'accesso all'intero stato del database.
Euforico

5

Facciamo un passo indietro e guardiamo l'immagine più grande qui.

Qual è IDatabasela responsabilità?

Ha alcune operazioni diverse:

  • Analizzare una stringa di connessione
  • Apri una connessione con un database (un sistema esterno)
  • Invia messaggi al database; i messaggi comandano al database di modificarne lo stato
  • Ricevi le risposte dal database e trasformale in un formato utilizzabile dal chiamante
  • Chiudi la connessione

Guardando questo elenco, potresti pensare: "Questo non viola SRP?" Ma non credo. Tutte le operazioni fanno parte di un unico concetto coerente: gestire una connessione stateful al database (un sistema esterno) . Stabilisce la connessione, tiene traccia dello stato corrente della connessione (in relazione alle operazioni eseguite su altre connessioni, in particolare), segnala quando eseguire il commit dello stato corrente della connessione, ecc. In questo senso, funge da API che nasconde molti dettagli di implementazione di cui la maggior parte dei chiamanti non si preoccuperà. Ad esempio, utilizza HTTP, socket, pipe, TCP personalizzato, HTTPS? Chiamare il codice non importa; vuole solo inviare messaggi e ottenere risposte. Questo è un buon esempio di incapsulamento.

Siamo sicuri? Non potremmo dividere alcune di queste operazioni? Forse, ma non ci sono benefici. Se provi a dividerli, avrai comunque bisogno di un oggetto centrale che mantenga aperta la connessione e / o gestisca lo stato corrente. Tutte le altre operazioni sono fortemente accoppiate allo stesso stato e, se si tenta di separarle, finiranno comunque per delegare nuovamente l'oggetto di connessione. Queste operazioni sono naturalmente e logicamente accoppiate allo stato e non c'è modo di separarle. Il disaccoppiamento è fantastico quando possiamo farlo, ma in questo caso, in realtà non possiamo. Almeno non senza un protocollo stateless molto diverso per parlare con il DB, e ciò renderebbe molto più difficili problemi molto importanti come la conformità ACID. Inoltre, nel tentativo di disaccoppiare queste operazioni dalla connessione, sarai costretto a esporre dettagli sul protocollo che non interessano ai chiamanti, poiché avrai bisogno di un modo per inviare un tipo di messaggio "arbitrario" al database.

Nota che il fatto che abbiamo a che fare con un protocollo con stato esclude in modo abbastanza solido la tua ultima alternativa (passando la stringa di connessione come parametro).

Abbiamo davvero bisogno di impostare una stringa di connessione?

Sì. Non è possibile aprire la connessione finché non si dispone di una stringa di connessione e non è possibile eseguire alcuna operazione con il protocollo finché non si apre la connessione. Quindi è inutile avere un oggetto connessione senza uno.

Come risolviamo il problema di richiedere la stringa di connessione?

Il problema che stiamo cercando di risolvere è che vogliamo che l'oggetto sia sempre utilizzabile. Che tipo di entità viene utilizzata per gestire lo stato nelle lingue OO? Oggetti , non interfacce. Le interfacce non hanno stato da gestire. Poiché il problema che stai cercando di risolvere è un problema di gestione dello stato, un'interfaccia non è davvero appropriata qui. Una classe astratta è molto più naturale. Quindi usa una classe astratta con un costruttore.

Potresti anche considerare di aprire effettivamente la connessione anche durante il costruttore, poiché la connessione è anche inutile prima che venga aperta. Ciò richiederebbe un protected Openmetodo astratto poiché il processo di apertura di una connessione potrebbe essere specifico del database. Sarebbe anche una buona idea far ConnectionStringleggere la proprietà solo in questo caso, poiché cambiare la stringa di connessione dopo l'apertura della connessione non avrebbe senso. (Onestamente, lo farei leggere solo in ogni caso. Se vuoi una connessione con una stringa diversa, crea un altro oggetto.)

Abbiamo bisogno di un'interfaccia?

Un'interfaccia che specifica i messaggi disponibili che è possibile inviare tramite la connessione e i tipi di risposte che è possibile ottenere potrebbero essere utili. Ciò ci consentirebbe di scrivere codice che esegue queste operazioni ma non è associato alla logica di apertura di una connessione. Ma questo è il punto: la gestione della connessione non fa parte dell'interfaccia di "Quali messaggi posso inviare e quali messaggi posso tornare al / dal database?", Quindi la stringa di connessione non dovrebbe far parte di questo interfaccia.

Se seguiamo questa strada, il nostro codice potrebbe assomigliare a questo:

interface IDatabase {
    void ExecuteNoQuery(string sql);
    void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

abstract class ConnectionStringDatabase : IDatabase { 

    public string ConnectionString { get; }

    public Database(string connectionString) {
        this.ConnectionString = connectionString;
        this.Open();
    }

    protected abstract void Open();

    public abstract void ExecuteNoQuery(string sql);
    public abstract void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

Gradirei se il downvoter spiegasse la ragione del suo disaccordo.
jpmc26,

D'accordo, ri: downvoter. Questa è la soluzione corretta La stringa di connessione deve essere fornita nel costruttore alla classe concrete / abstract. L'attività disordinata di apertura / chiusura di una connessione non è una preoccupazione del codice che utilizza questo oggetto e dovrebbe rimanere interna alla classe stessa. Direi che il Openmetodo dovrebbe essere privatee dovresti esporre una Connectionproprietà protetta che crea la connessione e si connette. O esporre un OpenConnectionmetodo protetto .
Greg Burghardt,

Questa soluzione è abbastanza elegante e molto ben progettata. Ma penso che alcuni dei ragionamenti alla base delle decisioni di progettazione siano sbagliati. Principalmente nei primi paragrafi sull'SRP. Ciò viola l'SRP anche come spiegato in "Qual è la responsabilità di IDatabase?". Le responsabilità viste per l'SRP non sono solo cose che una classe fa o gestisce. Sono anche "attori" o "ragioni per cambiare". E penso che viola l'SRP perché "Ricevi risposte dal database e trasformale in un formato utilizzabile dal chiamante" ha ragioni molto diverse per cambiare rispetto a "Analizza una stringa di connessione".
Sebastien,

Ho ancora votato questo.
Sebastien,

1
E a proposito, i SOLIDI non sono il Vangelo. Sicuramente sono molto importanti da tenere a mente quando si progetta una soluzione. Ma PUOI violarli se sai PERCHÉ lo fai, COME influenzerà la tua soluzione e COME risolvere le cose con il refactoring se ti mette nei guai. Quindi penso che anche se la soluzione sopra menzionata viola l'SRP, è la migliore in assoluto.
Sebastien,

0

Davvero non vedo il motivo per avere un'interfaccia qui. La tua classe di database è specifica per SQL e ti offre davvero un modo comodo / sicuro per assicurarti di non eseguire query su una connessione che non è stata aperta correttamente. Se insisti su un'interfaccia, ecco come lo farei.

public interface IDatabase : IDisposable
{
    string ConnectionString { get; }
    void ExecuteNoQuery(string sql);
    void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

public class SqlDatabase : IDatabase
{
    public string ConnectionString { get; }
    SqlConnection sqlConnection;
    SqlTransaction sqlTransaction; // optional

    public SqlDatabase(string connectionStr)
    {
        if (String.IsNullOrEmpty(connectionStr)) throw new ArgumentException("connectionStr empty");
        ConnectionString = connectionStr;
        instantiateSqlProps();
    }

    private void instantiateSqlProps()
    {
        sqlConnection.Open();
        sqlTransaction = sqlConnection.BeginTransaction();
    }

    public void ExecuteNoQuery(string sql) { /*run query*/ }
    public void ExecuteNoQuery(string[] sql) { /*run query*/ }

    public void Dispose()
    {
        sqlTransaction.Commit();
        sqlConnection.Dispose();
    }

    public void Commit()
    {
        Dispose();
        instantiateSqlProps();
    }
}

L'utilizzo potrebbe essere simile al seguente:

using (IDatabase dbase = new SqlDatabase("Data Source = servername; Initial Catalog = MyDb; Integrated Security = True"))
{
    dbase.ExecuteNoQuery("delete from dbo.Invoices");
    dbase.ExecuteNoQuery("delete from dbo.Customers");
}
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.