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 debba essere impostato / inizializzato prima di poter eseguire qualsiasi metodo.

Questa precondizione può essere in qualche modo ottenuta passando una connectionString 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 ogni metodo, ma sembra peggio che creare 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 precondizione all'interno dell'interfaccia stessa? È un "contratto" valido, quindi mi chiedo se esiste una caratteristica linguistica o un modello 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 precondizione in realtà rientra nella definizione di una precondizione come nel contesto di LSP?
posta Achilles 17.01.2017 - 07:07
fonte

4 risposte

10
  1. Sì. Da .Net 4.0 verso l'alto, Microsoft fornisce Contratti di codice . Questi possono essere usati per definire le precondizioni nella forma Contract.Requires( ConnectionString != null ); . Tuttavia, per fare in modo che funzioni per un'interfaccia, avrai ancora bisogno di una classe helper IDatabaseContract , che verrà allegata a IDatabase , e la precondizione deve essere definita per ogni singolo metodo dell'interfaccia in cui deve essere conservato. Vedi qui per un ampio esempio di interfacce.

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

risposta data 17.01.2017 - 07:46
fonte
21

La connessione e l'interrogazione sono due preoccupazioni separate. In quanto tali, dovrebbero avere due interfacce separate.

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

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

Entrambi garantiscono che IDatabase sarà connesso quando usato e rende il client non dipendente dall'interfaccia di cui non ha bisogno.

    
risposta data 17.01.2017 - 10:07
fonte
5

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

Qual è la responsabilità di IDatabase ?

Ha alcune operazioni diverse:

  • Analizza una stringa di connessione
  • Apri una connessione con un database (un sistema esterno)
  • inviare messaggi al database; i messaggi comandano al database di alterarne lo stato
  • Ricevi risposte dal database e trasformale in un formato che il chiamante può utilizzare
  • Chiudi la connessione

Guardando questo elenco, potresti pensare: "Questo non viola l'SRP?" Ma non penso che lo faccia. Tutte le operazioni fanno parte di un unico concetto coeso: 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 commettere lo 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 cura. Ad esempio, usa HTTP, socket, pipe, TCP personalizzato, HTTPS? Chiamare il codice non interessa; vuole solo inviare messaggi e ottenere risposte. Questo è un buon esempio di incapsulamento.

Siamo sicuri? Non potremmo suddividere alcune di queste operazioni? Forse, ma non c'è alcun vantaggio. Se provi a dividerli, avrai comunque bisogno di un oggetto centrale che mantenga la connessione aperta e / o che controlli lo stato corrente. Tutte le altre operazioni sono strongmente accoppiate allo stesso stato, e se provate a separarle, finiranno comunque per delegare nuovamente all'oggetto di connessione. Queste operazioni sono naturalmente e logicamente accoppiate allo stato, e non c'è modo di separarle. Il disaccoppiamento è ottimo 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 questo renderebbe molto più difficili problemi molto importanti come la compliance ACID. Inoltre, nel tentativo di disaccoppiare queste operazioni dalla connessione, sarai costretto ad esporre dettagli sul protocollo a cui i chiamanti non interessano, dal momento che avrai bisogno di un modo per inviare una sorta di messaggio "arbitrario" al database.

Si noti che il fatto che abbiamo a che fare con un protocollo stateful esclude abbastanza decisamente l'ultima alternativa (passare la stringa di connessione come parametro).

Abbiamo veramente bisogno che la stringa di connessione sia impostata?

Sì. Non puoi aprire la connessione finché non hai una stringa di connessione e non puoi fare nulla con il protocollo finché non apri la connessione. Quindi è inutile avere un oggetto di 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 in uno stato utilizzabile. Che tipo di entità viene utilizzata per gestire lo stato nelle lingue OO? Oggetti , non interfacce. Le interfacce non hanno lo stato da gestire. Poiché il problema che stai cercando di risolvere è un problema di gestione dello stato, un'interfaccia non è proprio appropriata qui. Una classe astratta è molto più naturale. Quindi usa una classe astratta con un costruttore.

Potresti anche prendere in considerazione aprire la connessione anche durante il costruttore, poiché anche la connessione è inutile prima di essere aperta. Ciò richiederebbe un metodo protected Open astratto poiché il processo di apertura di una connessione potrebbe essere specifico del database. Sarebbe anche una buona idea rendere la proprietà ConnectionString di sola lettura in questo caso, poiché modificare la stringa di connessione dopo che la connessione è stata aperta non avrebbe avuto senso. (Onestamente, lo farei comunque solo leggere. 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 puoi inviare tramite la connessione e i tipi di risposte che puoi recuperare potrebbero essere utili. Ciò consentirebbe di scrivere codice che esegue queste operazioni ma non è accoppiato alla logica di apertura di una connessione. Ma questo è il punto: gestire la connessione non fa parte dell'interfaccia di "Quali messaggi posso inviare e quali messaggi posso tornare dal / al database?", Quindi la stringa di connessione non dovrebbe nemmeno far parte di quella interfaccia.

Se seguiamo questa strada, il nostro codice potrebbe essere simile 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
}
    
risposta data 17.01.2017 - 18:47
fonte
0

Non vedo davvero la ragione per avere un'interfaccia qui. La classe del tuo database è specifica per SQL e ti dà davvero un modo comodo e sicuro per assicurarti di non eseguire query su una connessione che non è stata aperta correttamente. Se insisti su un'interfaccia, ecco come farei io.

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 a questo:

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");
}
    
risposta data 18.01.2017 - 16:25
fonte

Leggi altre domande sui tag