如何在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. 有没有办法在接口本身中指定此前提条件?这是一个有效的“合同”,所以我想知道是否有语言功能或模式(抽象类解决方案更像是一种hack imo,除了每次都需要创建两种类型-接口和抽象类之外)这是必需的)
  2. 这更多是一种理论上的好奇-前提条件实际上是否属于LSP上下文中前提条件的定义?

2
通过“ LSP”,你们正在谈论Liskov替代原理?“如果它像鸭子一样嘎嘎,但需要电池而不是鸭子”的原则?因为如我所见,它更违反了ISP和SRP,甚至违反了OCP,但实际上并没有违反LSP。
塞巴斯蒂安

2
众所周知,整个概念“必须先设置/初始化ConnectionString,然后才能运行任何方法”是时间耦合blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling的示例,如果出现这种情况,应避免使用可能。
Richiban

Seemann确实是Abstract Factory的忠实拥护者。
Adrian Iftode

Answers:


10
  1. 是。从.Net 4.0以后,Microsoft提供代码合同。这些可以用来定义形式中的前提条件 Contract.Requires( ConnectionString != null );。但是,要使该功能适用​​于接口,您仍将需要一个helper类IDatabaseContract,该类将附加到IDatabase,并且需要为接口所包含的每个单独方法定义前提条件。有关接口的详细示例,请参见此处

  2. 是的,LSP处理合同的句法和语义部分。


我认为您不能在界面中使用代码协定。您提供的示例显示了它们在中的使用情况 这些类确实符合接口,但是接口本身不包含任何代码约定信息(确实很遗憾。这是放置它的理想位置)。
罗伯特·哈维

1
@RobertHarvey:是的,你是对的。当然,从技术上讲,您需要第二个类,但是一旦定义好,合同将对接口的每个实现自动起作用。
布朗

21

连接和查询是两个独立的问题。因此,它们应具有两个单独的接口。

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

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

这既确保了IDatabase在使用时将被连接,又使客户端不依赖于不需要的接口。


可能是更加明确:“这是通过强制类型的先决条件的模式”
Caleth

@Caleth:这不是“执行前提条件的一般模式”。这是确保连接先于其他特定要求的解决方案。其他前提条件将需要不同的解决方案(例如我在回答中提到的解决方案)。我想补充一下这一要求,我显然更喜欢Euphoric的建议,而不是我的建议,因为它更简单并且不需要任何其他第三方组件。
布朗

那具体requrement 东西之前发生别的广泛适用。我也认为您的答案更适合这个问题,但是这个答案可以改善
Caleth

1
这个答案完全错了。该IDatabase接口定义了一个对象,该对象能够建立与数据库的连接,然后执行任意查询。它充当数据库和代码的其余部分之间的边界的对象。这样,该对象必须维护会影响查询行为的状态(例如事务)。将它们放在同一个类中非常实用。
jpmc26

4
@ jpmc26您的任何反对都没有道理,因为可以在实现IDatabase的类中维护状态。它还可以引用创建它的父类,从而可以访问整个数据库状态。
欣快感'17

5

让我们退后一步,看看这里的大图。

什么是IDatabase责任?

它具有一些不同的操作:

  • 解析连接字符串
  • 打开与数据库(外部系统)的连接
  • 发送消息到数据库;消息命令数据库更改其状态
  • 从数据库接收响应并将其转换为调用者可以使用的格式
  • 关闭连接

查看此列表,您可能会想:“这是否违反了SRP?” 但是我不这样认为。所有操作都是一个统一的概念的一部分:管理与数据库(外部系统)的有状态连接。它建立连接,跟踪连接的当前状态(特别是与在其他连接上完成的操作有关),发出何时提交连接的当前状态的信号,等等。在这种意义上,它充当API隐藏了大多数调用者不关心的许多实现细节。例如,它是否使用HTTP,套接字,管道,自定义TCP,HTTPS?调用代码无关紧要;它只是想发送消息并获得响应。这是封装的一个很好的例子。

我们确定吗?我们不能拆分其中一些操作吗?也许吧,但是没有好处。如果尝试将它们拆分,则仍然需要一个中央对象,该对象可以保持连接打开和/或管理当前状态。所有其他操作都强烈地耦合到相同的状态,并且,如果您尝试将它们分开,则它们最终都将最终委托给连接对象。这些操作自然逻辑上与状态耦合,因此无法将它们分开。当我们能够做到时,去耦是很棒的,但是在这种情况下,我们实际上不能。至少没有没有一个非常不同的,无状态的协议可以与数据库进行通信,这实际上会使诸如ACID合规性等非常重要的问题变得更加困难。同样,在尝试将这些操作与连接分离的过程中,由于需要一种发送某种“任意”消息的方法,因此您将不得不公开与呼叫者无关的协议的详细信息。到数据库。

请注意,我们正在处理有状态协议,这一事实确实排除了您的最后一个选择(将连接字符串作为参数传递)。

我们真的需要设置连接字符串吗?

是。只有拥有连接字符串,您才能打开连接;打开连接之前,您不能对协议进行任何操作。因此,没有一个连接对象是没有意义的

我们如何解决需要连接字符串的问题?

我们试图解决的问题是我们希望对象始终处于可用状态。哪种类型的实体用于管理OO语言中的状态?对象,而不是接口。接口没有要管理的状态。因为您要解决的问题是状态管理问题,所以这里的接口并不适合。抽象类更为自然。因此,请使用带有构造函数的抽象类。

您可能还需要考虑在构造函数期间实际打开连接,因为连接在打开之前也是无用的。这将需要一种抽象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

同意,回​​复:下注者。这是正确的解决方案。连接字符串应在构造函数中提供给concrete / abstract类。使用该对象的代码无关紧要的是打开/关闭连接的麻烦事,它应该保留在类本身的内部。我认为该Open方法应该是,private并且您应该公开Connection创建连接和连接的受保护属性。或公开受保护的OpenConnection方法。
Greg Burghardt

该解决方案非常优雅并且设计得很好。但是我认为设计决策背后的某些推理是错误的。主要在关于SRP的前几段中。它确实违反了SRP,即使如“ IDatabase的责任是什么?”中所述。对于SRP而言,责任不仅仅是班级要做或管理的事情。它也是“行为者”或“改变的理由”。而且我认为它确实违反了SRP,因为“从数据库接收响应并将其转换为调用者可以使用的格式”与“解析连接字符串”相比,更改原因非常不同。
塞巴斯蒂安

我还是支持这个。
塞巴斯蒂安

1
顺便说一句,SOLID不是福音。确保在设计解决方案时记住它们非常重要。但是,如果您知道为什么这样做,就会违反它们,这将如何影响您的解决方案,如果这样做确实会给您带来麻烦,则如何通过重构来解决问题。因此,我认为即使上述解决方案违反了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");
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.