web-dev-qa-db-ja.com

C#のインターフェイスで前提条件(LSP)を指定する方法

次のインターフェースがあるとしましょう-

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

前提条件は、任意のメソッドを実行する前に、ConnectionStringを設定または初期化する必要があることです。

この前提条件は、IDatabaseが抽象クラスまたは具象クラスである場合は、コンストラクターを介してconnectionStringを渡すことである程度達成できます。

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
}

別の方法として、各メソッドのパラメーターとしてconnectionStringを作成することもできますが、抽象クラスを作成するよりも悪く見えます-

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

質問-

  1. インターフェイス自体の中でこの前提条件を指定する方法はありますか?これは有効な「契約」なので、このための言語機能またはパターンがあるかどうか疑問に思っています(抽象クラ​​スソリューションは、インターフェイスと抽象クラスの2つのタイプを毎回作成する必要があるだけでなく、ハックイモのようなものです)これは必要です)
  2. これは理論的な好奇心のようなものです-この前提条件は実際にLSPのコンテキストのように前提条件の定義に該当しますか?
11
Achilles
  1. はい。.Net 4.0以降、Microsoftは Code Contracts を提供しています。これらは、Contract.Requires( ConnectionString != null );の形式で前提条件を定義するために使用できます。ただし、これをインターフェイスで機能させるには、ヘルパークラスIDatabaseContractが必要です。これはIDatabaseにアタッチされ、インターフェイスの個々のメソッドごとに前提条件を定義する必要があります。それは保持するものとします。 ここを参照 インターフェースの広範な例。

  2. はい、LSPは契約の構文部分と意味部分の両方を扱います。

10
Doc Brown

接続とクエリは、2つの別個の問題です。したがって、2つの別々のインターフェースが必要です。

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

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

これにより、使用時にIDatabaseが確実に接続され、クライアントが不要なインターフェースに依存しなくなります。

21
Euphoric

一歩下がって、ここの全体像を見てみましょう。

IDatabaseの責任は何ですか?

いくつかの異なる操作があります:

  • 接続文字列を解析する
  • データベースとの接続を開く(外部システム)
  • データベースにメッセージを送信します。メッセージはデータベースにその状態を変更するように命令します
  • データベースから応答を受け取り、発信者が使用できる形式に変換する
  • 接続を閉じる

このリストを見て、「これはSRPに違反していないのではないか」と考えているかもしれません。しかし、そうは思いません。すべての操作は、単一のまとまりのある概念の一部です:データベースへのステートフル接続を管理します(外部システム)。これは、接続を確立し、接続の現在の状態を追跡し(特に他の接続で行われた操作に関連して)、接続の現在の状態をいつコミットするかなどを通知します。この意味で、APIとして機能しますこれにより、ほとんどの呼び出し元が気にしない多くの実装の詳細が隠されます。たとえば、HTTP、ソケット、パイプ、カスタムTCP、HTTPSを使用していますか?コードの呼び出しは関係ありません。メッセージを送信して応答を取得したいだけです。これはカプセル化の良い例です。

よろしいですか?これらの操作のいくつかを分割できませんか? たぶん、しかし、メリットはありません。それらを分割しようとする場合でも、接続を開いたままにしたり、現在の状態を管理したりする中央オブジェクトが必要になります。他のすべての操作は強く同じ状態に結合されており、それらを分離しようとすると、最終的にとにかく接続オブジェクト。これらの操作はnaturallylogicallyが状態に結合されており、それらを分離する方法。デカップリングはできれば素晴らしいですが、この場合、実際にはできません。少なくとも、DBと通信するための非常に異なるステートレスプロトコルがないと、ACIDコンプライアンスなどの非常に重要な問題がはるかに困難になります。また、これらの操作を接続から切り離そうとするプロセスでは、ある種の「任意」のメッセージを送信する方法が必要になるため、呼び出し側が気にしないプロトコルに関する詳細を公開する必要があります。データベースに。

ステートフルなプロトコルを扱っているという事実は、最後の代替案(接続文字列をパラメーターとして渡す)をかなりしっかりと除外することに注意してください。

本当に接続文字列を設定する必要がありますか?

はい。接続文字列を取得するまで接続をopenできません。接続を開くまでプロトコル。したがって、接続オブジェクトを持たない接続オブジェクトがあるのは意味がないです。

接続文字列を要求する問題をどのように解決しますか?

私たちが解決しようとしている問題は、オブジェクトを常に使用可能な状態にしたいということです。 OO言語)で状態を管理するためにどのようなエンティティが使用されますか?オブジェクト、インターフェイスではありません。インターフェイスにはありません管理する状態です。解決しようとしている問題は状態管理の問題であるため、ここではインターフェイスは適切ではありません。抽象クラスの方がはるかに自然です。そのため、コンストラクターで抽象クラスを使用してください。

接続が開かれる前は接続も役に立たないので、コンストラクタ中に接続を実際にopeningすることを検討することもできます。それには抽象protected Openメソッド。接続を開くプロセスはデータベース固有である可能性があるためです。この場合、ConnectionStringプロパティを読み取り専用にすることもお勧めします。接続が開いた後に接続文字列を変更しても意味がないためです。 (正直に言えば、読み取り専用にします。別の文字列との接続が必要な場合は、別のオブジェクトを作成します。)

インターフェースが必要ですか?

接続を介して送信できる使用可能なメッセージと、取得できる応答のタイプを指定するインターフェースが役立つ場合があります。これにより、これらの操作を実行するコードを記述できますが、接続を開くロジックとは連動しません。しかし、それがポイントです。接続の管理は、「どのメッセージを送信でき、どのメッセージをデータベースとの間でやり取りできるか」というインターフェースの一部ではないので、接続文字列はその一部であるべきではありません。インターフェース。

このルートを使用すると、コードは次のようになります。

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
}
5
jpmc26

ここにインターフェイスを用意する理由がまったくわかりません。データベースクラスはSQL固有であり、適切に開かれていない接続でクエリを実行していないことを確認する便利で安全な方法を提供します。ただし、インターフェースを主張する場合は、次のようにします。

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

使用法は次のようになります。

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