Wie gebe ich eine Vorbedingung (LSP) in einer Schnittstelle in C # an?

11

Nehmen wir an, wir haben die folgende Schnittstelle -

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

Voraussetzung ist, dass ConnectionString gesetzt / initialisiert wird, bevor eine der Methoden ausgeführt werden kann.

Diese Voraussetzung kann etwas erreicht werden, indem ein connectionString über einen Konstruktor übergeben wird, wenn IDatabase eine abstrakte oder konkrete Klasse ist -

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
}

Alternativ können wir für jede Methode einen connectionString-Parameter erstellen, der jedoch schlechter aussieht als nur eine abstrakte Klasse zu erstellen.

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

Fragen -

  1. Gibt es eine Möglichkeit, diese Voraussetzung in der Schnittstelle selbst anzugeben? Da es sich um einen gültigen "Vertrag" handelt, frage ich mich, ob es dafür ein Sprachmerkmal oder -muster gibt (die Lösung für abstrakte Klassen ist eher ein Hack-Imo als die Notwendigkeit, jedes Mal zwei Typen zu erstellen - eine Schnittstelle und eine abstrakte Klasse dies wird benötigt)
  2. Dies ist eher eine theoretische Kuriosität - Fällt diese Voraussetzung tatsächlich in die Definition einer Vorbedingung wie im Kontext von LSP?
Achilles
quelle
2
Mit "LSP" redet ihr über das Liskov-Substitutionsprinzip? Das Prinzip "Wenn es wie eine Ente quakt, aber Batterien braucht, ist es keine Ente"? Denn aus meiner Sicht ist es eher eine Verletzung des ISP und des SRP, vielleicht sogar des OCP, aber nicht wirklich des LSP.
Sebastien
2
Nur damit Sie wissen, ist dieses gesamte Konzept von "ConnectionString muss gesetzt / initialisiert werden, bevor eine der Methoden ausgeführt werden kann" ein Beispiel für die zeitliche Kopplung blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling und sollte vermieden werden, wenn möglich.
Richiban
Seemann ist wirklich ein großer Fan von Abstract Factory.
Adrian Iftode

Antworten:

10
  1. Ja. Ab .NET 4.0 bietet Microsoft Codeverträge an . Diese können verwendet werden, um Voraussetzungen im Formular zu definieren Contract.Requires( ConnectionString != null );. Damit dies für eine Schnittstelle funktioniert, benötigen Sie jedoch noch eine Hilfsklasse IDatabaseContract, die angehängt wird IDatabase, und die Voraussetzung muss für jede einzelne Methode Ihrer Schnittstelle definiert werden, für die sie gelten soll. Hier finden Sie ein ausführliches Beispiel für Schnittstellen.

  2. Ja , der LSP behandelt sowohl syntaktische als auch semantische Teile eines Vertrags.

Doc Brown
quelle
Ich dachte nicht, dass Sie Codeverträge in einer Schnittstelle verwenden könnten. Das von Ihnen bereitgestellte Beispiel zeigt, wie sie in Klassen verwendet werden. Die Klassen entsprechen zwar einer Schnittstelle, aber die Schnittstelle selbst enthält keine Code-Vertragsinformationen (wirklich schade. Das wäre der ideale Ort, um es auszudrücken).
Robert Harvey
1
@ RobertHarvey: Ja, du hast recht. Technisch gesehen benötigen Sie natürlich eine zweite Klasse, aber sobald der Vertrag definiert ist, funktioniert er automatisch für jede Implementierung der Schnittstelle.
Doc Brown
21

Verbinden und Abfragen sind zwei getrennte Anliegen. Als solche sollten sie zwei separate Schnittstellen haben.

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

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

Dies stellt sicher, dass IDatabasedie Verbindung bei Verwendung hergestellt wird, und macht den Client nicht von der Schnittstelle abhängig, die er nicht benötigt.

Euphorisch
quelle
Könnte expliziter sein über "dies ist ein Muster der Durchsetzung der Voraussetzungen durch Typen"
Caleth
@Caleth: Dies ist kein "allgemeines Muster zur Durchsetzung von Voraussetzungen". Dies ist eine Lösung für diese spezielle Anforderung, sicherzustellen, dass die Verbindung vor allem anderen erfolgt. Andere Voraussetzungen erfordern andere Lösungen (wie die, die ich in meiner Antwort erwähnt habe). Ich möchte für diese Anforderung hinzufügen, dass ich den Vorschlag von Euphoric eindeutig meinem vorziehen würde, da er viel einfacher ist und keine zusätzliche Komponente von Drittanbietern benötigt.
Doc Brown
Die spezifische Anforderung, dass etwas vor etwas anderem passiert , ist weit verbreitet. Ich denke auch, dass Ihre Antwort besser zu dieser Frage passt , aber diese Antwort kann verbessert werden
Caleth
1
Diese Antwort geht völlig daneben. Die IDatabaseSchnittstelle definiert ein Objekt, das eine Verbindung zu einer Datenbank herstellen und dann beliebige Abfragen ausführen kann. Es ist das Objekt, das als Grenze zwischen der Datenbank und dem Rest des Codes fungiert. Daher muss dieses Objekt einen Status (z. B. eine Transaktion) beibehalten , der das Verhalten der Abfragen beeinflussen kann. Es ist sehr praktisch, sie in dieselbe Klasse zu bringen.
jpmc26
4
@ jpmc26 Keine Ihrer Einwände ist sinnvoll, da der Status innerhalb der Klasse, die IDatabase implementiert, beibehalten werden kann. Es kann auch auf die übergeordnete Klasse verweisen, die es erstellt hat, und erhält so Zugriff auf den gesamten Datenbankstatus.
Euphoric
5

Lassen Sie uns einen Schritt zurücktreten und das Gesamtbild hier betrachten.

Was ist IDatabasedie Verantwortung?

Es hat ein paar verschiedene Operationen:

  • Analysieren Sie eine Verbindungszeichenfolge
  • Öffnen Sie eine Verbindung mit einer Datenbank (einem externen System).
  • Nachrichten an die Datenbank senden; Die Nachrichten befehlen der Datenbank, ihren Status zu ändern
  • Empfangen Sie Antworten aus der Datenbank und wandeln Sie sie in ein Format um, das der Anrufer verwenden kann
  • Schließen Sie die Verbindung

Wenn Sie sich diese Liste ansehen, denken Sie vielleicht: "Verstößt dies nicht gegen SRP?" Aber ich glaube nicht. Alle Vorgänge sind Teil eines einzigen zusammenhängenden Konzepts: Verwalten einer zustandsbehafteten Verbindung zur Datenbank (einem externen System) . Es stellt die Verbindung her, verfolgt den aktuellen Status der Verbindung (insbesondere in Bezug auf Operationen, die an anderen Verbindungen ausgeführt werden), signalisiert, wann der aktuelle Status der Verbindung festgeschrieben werden soll usw. In diesem Sinne fungiert es als API Das verbirgt viele Implementierungsdetails, die den meisten Anrufern egal sind. Verwendet es beispielsweise HTTP, Sockets, Pipes, benutzerdefiniertes TCP und HTTPS? Das Aufrufen des Codes ist egal; Es möchte nur Nachrichten senden und Antworten erhalten. Dies ist ein gutes Beispiel für die Einkapselung.

Sind wir sicher Könnten wir einige dieser Operationen nicht aufteilen? Vielleicht, aber es gibt keinen Vorteil. Wenn Sie versuchen, sie aufzuteilen, benötigen Sie weiterhin ein zentrales Objekt, das die Verbindung offen hält und / oder den aktuellen Status verwaltet. Alle anderen Vorgänge sind stark an denselben Status gekoppelt. Wenn Sie versuchen, sie zu trennen, werden sie ohnehin nur an das Verbindungsobjekt zurückdelegiert. Diese Operationen sind natürlich und logisch an den Zustand gekoppelt, und es gibt keine Möglichkeit, sie zu trennen. Entkopplung ist großartig, wenn wir es schaffen, aber in diesem Fall können wir es tatsächlich nicht. Zumindest nicht ohne ein ganz anderes, zustandsloses Protokoll, um mit der DB zu sprechen, und das würde tatsächlich sehr wichtige Probleme wie die ACID-Konformität viel schwieriger machen. Wenn Sie versuchen, diese Vorgänge von der Verbindung zu entkoppeln, müssen Sie außerdem Details zu dem Protokoll offenlegen, das Anrufern egal ist, da Sie eine Art "willkürliche" Nachricht senden müssen in die Datenbank.

Beachten Sie, dass die Tatsache, dass es sich um ein Stateful-Protokoll handelt, Ihre letzte Alternative (Übergabe der Verbindungszeichenfolge als Parameter) ziemlich solide ausschließt.

Müssen wir wirklich eine Verbindungszeichenfolge setzen?

Ja. Sie können die Verbindung erst öffnen , wenn Sie eine Verbindungszeichenfolge haben, und Sie können nichts mit dem Protokoll tun, bis Sie die Verbindung öffnen. Es ist also sinnlos , ein Verbindungsobjekt ohne eines zu haben.

Wie lösen wir das Problem, dass die Verbindungszeichenfolge erforderlich ist?

Das Problem, das wir zu lösen versuchen, ist, dass das Objekt jederzeit in einem verwendbaren Zustand sein soll. Welche Art von Entität wird zum Verwalten des Status in OO-Sprachen verwendet? Objekte , keine Schnittstellen. Schnittstellen müssen nicht verwaltet werden. Da das Problem, das Sie lösen möchten, ein Problem der Statusverwaltung ist, ist eine Schnittstelle hier nicht wirklich geeignet. Eine abstrakte Klasse ist viel natürlicher. Verwenden Sie also eine abstrakte Klasse mit einem Konstruktor.

Möglicherweise möchten Sie auch in Betracht ziehen, die Verbindung auch während des Konstruktors zu öffnen , da die Verbindung auch vor dem Öffnen unbrauchbar ist. Dies würde eine abstrakte protected OpenMethode erfordern , da der Prozess des Öffnens einer Verbindung datenbankspezifisch sein kann. ConnectionStringIn diesem Fall ist es auch eine gute Idee, die Eigenschaft schreibgeschützt zu machen , da das Ändern der Verbindungszeichenfolge nach dem Öffnen der Verbindung bedeutungslos wäre. (Ehrlich gesagt würde ich es sowieso nur lesbar machen. Wenn Sie eine Verbindung mit einer anderen Zeichenfolge wünschen, erstellen Sie ein anderes Objekt.)

Benötigen wir überhaupt eine Schnittstelle?

Eine Schnittstelle, die die verfügbaren Nachrichten angibt, die Sie über die Verbindung senden können, und die Arten von Antworten, die Sie zurückerhalten können, kann hilfreich sein. Dies würde es uns ermöglichen, Code zu schreiben, der diese Operationen ausführt, aber nicht an die Logik des Öffnens einer Verbindung gekoppelt ist. Aber das ist der Punkt: Die Verwaltung der Verbindung ist nicht Teil der Schnittstelle von "Welche Nachrichten kann ich senden und welche Nachrichten kann ich zur / von der Datenbank zurückholen?", Daher sollte die Verbindungszeichenfolge nicht einmal Teil davon sein Schnittstelle.

Wenn wir diesen Weg gehen, könnte unser Code ungefähr so ​​aussehen:

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
}
jpmc26
quelle
Würde mich freuen, wenn der Downvoter den Grund für die Ablehnung erklären würde.
jpmc26
Einverstanden, re: downvoter. Dies ist die richtige Lösung. Die Verbindungszeichenfolge sollte im Konstruktor für die konkrete / abstrakte Klasse bereitgestellt werden. Das unordentliche Geschäft, eine Verbindung zu öffnen / zu schließen, ist kein Problem des Codes, der dieses Objekt verwendet, und sollte innerhalb der Klasse selbst bleiben. Ich würde argumentieren, dass die OpenMethode sein sollte privateund Sie eine geschützte ConnectionEigenschaft verfügbar machen sollten , die die Verbindung herstellt und eine Verbindung herstellt. Oder legen Sie eine geschützte OpenConnectionMethode offen.
Greg Burghardt
Diese Lösung ist sehr elegant und sehr gut gestaltet. Aber ich denke, dass einige der Gründe für die Entwurfsentscheidungen falsch sind. Hauptsächlich in den ersten Absätzen über die SRP. Es verstößt gegen die SRP, auch wenn in "Was ist die Verantwortung von IDatabase?" Erläutert wird. Verantwortlichkeiten für die SRP sind nicht nur Dinge, die eine Klasse tut oder verwaltet. Es ist auch "Schauspieler" oder "Gründe zu ändern". Und ich denke, dass es gegen die SRP verstößt, weil "Antworten aus der Datenbank empfangen und in ein Format umwandeln, das der Anrufer verwenden kann" einen ganz anderen Grund zur Änderung hat als "Verbindungszeichenfolge analysieren".
Sebastien
Trotzdem stimme ich dem zu.
Sebastien
1
Und übrigens, SOLID sind nicht das Evangelium. Sicher, sie sind sehr wichtig, wenn Sie eine Lösung entwerfen. Aber Sie KÖNNEN sie verletzen, wenn Sie wissen, WARUM Sie es tun, WIE es sich auf Ihre Lösung auswirkt und WIE Sie Probleme mit Refactoring beheben können, wenn Sie dadurch in Schwierigkeiten geraten. Daher denke ich, auch wenn die oben genannte Lösung gegen die SRP verstößt, ist sie die bisher beste.
Sebastien
0

Ich sehe wirklich keinen Grund, hier überhaupt eine Schnittstelle zu haben. Ihre Datenbankklasse ist SQL-spezifisch und bietet Ihnen nur eine bequeme / sichere Möglichkeit, um sicherzustellen, dass Sie keine Verbindung zu einer Verbindung abfragen, die nicht ordnungsgemäß geöffnet wurde. Wenn Sie jedoch auf einer Schnittstelle bestehen, gehen Sie wie folgt vor.

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();
    }
}

Die Verwendung könnte folgendermaßen aussehen:

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");
}
Graham
quelle