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


11

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

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. これは理論上の好奇心に近い-この前提条件は、実際にはLSPのコンテキストのように前提条件の定義に該当しますか?

2
「LSP」では、皆さんはリスコフ代替原理について話していますか?「カモはカモに似ているが、カモではなく電池が必要な場合」の原則は?私が見るように、それはISPとSRPの違反である可能性があります。おそらくOCPでさえ実際にはLSPではありません。
セバスチャン

2
だけ知っているので、この全体の概念時間的カップリングの一例である「のConnectionStringは、メソッドのいずれかの前に、intializedセット/でなければなりませんが実行することができます」blog.ploeh.dk/2011/05/24/DesignSmellTemporalCouplingとあれば、避けるべきです可能。
リチバン

SeemannはAbstract Factoryの大ファンです。
エイドリアンイフトデ

回答:


10
  1. はい。.Net 4.0以降、MicrosoftはCode Contractsを提供しています。これらを使用して、フォームの前提条件を定義できます Contract.Requires( ConnectionString != null );。しかし、インタフェースのためのこの仕事をするために、あなたはまだヘルパークラスが必要になりますIDatabaseContractに付着する、IDatabaseとの前提条件は、それが保持しなければならあなたのインターフェイスのすべての個々の方法のために定義する必要があります。インターフェイスの広範な例については、こちらご覧ください

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


インターフェイスでコードコントラクトを使用できるとは思いませんでした。提供する例は、クラスで使用されていることを示しています。 クラスはインターフェイスに準拠していますが、インターフェイス自体にはコードコントラクト情報が含まれていません(実際、残念です。それを置くのが理想的な場所です)。
ロバートハーヴェイ

1
@RobertHarvey:はい、あなたは正しいです。技術的には、もちろん2番目のクラスが必要ですが、定義されると、インターフェイスのすべての実装に対してコントラクトが自動的に機能します。
Doc Brown

21

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

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

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

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


「これは型を介して前提条件を強制するパターンです」
カレス

@Caleth:これは「前提条件を強制する一般的なパターン」ではありません。これは、接続が他の何よりも先に行われるようにするというこの特定の要件に対するソリューションです。他の前提条件にはさまざまな解決策が必要になります(回答で述べたような)。この要件に追加したいのですが、Euphoricの提案は私のものよりも明らかに好みます。これは、はるかに単純で、追加のサードパーティコンポーネントを必要としないためです。
Doc Brown

何か他の何かの前に起こるという特定の要件は広く適用可能です。また、あなたの答えはこの質問によく合っていると思いますが、この答えは改善される可能性があります
カレス

1
この答えは、ポイントを完全に逃しています。IDatabaseインターフェースは、データベースへの接続を確立した後、任意のクエリを実行することが可能なオブジェクトを定義します。これは、あるデータベースとコードの残りの部分との間の境界として機能するオブジェクト。そのため、このオブジェクトは、クエリの動作に影響を与える可能性のある状態(トランザクションなど)を維持する必要があります。それらを同じクラスに入れることは非常に実用的です。
jpmc26

4
@ jpmc26 IDatabaseを実装するクラス内で状態を維持できるため、異議はありません。また、それを作成した親クラスを参照できるため、データベース全体の状態にアクセスできます。
陶酔

5

一歩戻って、ここでより大きな画像を見てみましょう。

IDatabaseの責任は何ですか?

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

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

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

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

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

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

はい。あなたはできません開く接続文字列を持つまでの接続を、そしてあなたは、接続をオープンするまでは、プロトコルと何もできません。そのため、接続オブジェクトがなくても接続オブジェクトを使用しても意味がありません。

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

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

また、接続も開かれる前は役に立たないため、コンストラクターで実際に接続を開くことを検討することもできます。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
}

反対票を投じた人が意見が合わない理由を説明してくれたら幸いです。
jpmc26

同意しました、再:投票者。これが正しい解決策です。接続文字列は、constructorでconcrete / abstractクラスに提供する必要があります。接続を開いたり閉じたりする面倒なビジネスは、このオブジェクトを使用するコードの懸念ではなく、クラス自体の内部にとどまる必要があります。私は、Openメソッドが必要でprivateありConnection、接続を作成して接続する保護されたプロパティを公開する必要があると主張します。または、保護されたOpenConnectionメソッドを公開します。
グレッグブルクハート

このソリューションは非常にエレガントで、非常にうまく設計されています。しかし、設計決定の背後にある推論のいくつかは間違っていると思います。主にSRPに関する最初の数段落で。「IDatabaseの責任とは」で説明されているように、SRPに違反します。SRPに見られる責任は、クラスが行うまたは管理するものだけではありません。また、「俳優」または「変化する理由」。また、「データベースからの応答を受信し、呼び出し元が使用できる形式に変換する」という変更の理由が「接続文字列の解析」とは非常に異なるため、SRPに違反すると思います。
セバスチャン

それでも私はこれを支持します。
セバスチャン

1
ところで、ソリッドは福音ではありません。確かに、ソリューションを設計する際に留意する必要がある非常に重要です。しかし、あなたがそれをする理由、それがあなたのソリューションにどのように影響するか、そしてそれがあなたをトラブルに巻き込む場合にリファクタリングで物事を修正する方法を知っているなら、あなたはそれらに違反することができます。したがって、上記のソリューションがSRPに違反している場合でも、それはまだ最高のソリューションであると思います。
セバスチャン

0

ここにインターフェイスを用意する理由はまったくわかりません。データベースクラスは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");
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.