Pregunta

Let's say that we have the following interface -

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

The precondition is that ConnectionString must be set/intialized before any of the methods can be run.

This precondition can be somewhat achieved by passing a connectionString via a constructor if IDatabase were an abstract or concrete class -

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
}

Alternatively, we can create connectionString a parameter for each method, but it looks worse than just creating an abstract class -

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

Questions -

  1. Is there a way to specify this precondition within the interface itself? It is a valid "contract" so I'm wondering if there is a language feature or pattern for this (the abstract class solution is more of a hack imo besides the need of creating two types - an interface and an abstract class - every time this is needed)
  2. This is more of a theoretical curiosity - Does this precondition actually fall into the definition of a precondition as in the context of LSP?
¿Fue útil?

Solución

  1. Yes. From .Net 4.0 upward, Microsoft provides Code Contracts. These can be used to define preconditions in the form Contract.Requires( ConnectionString != null );. However, to make this work for an interface, you will still need a helper class IDatabaseContract, which becomes attached to IDatabase, and the precondition needs to be defined for every individual method of your interface where it shall hold. See here for an extensive example for interfaces.

  2. Yes, the LSP deals with both syntactical and semantical parts of a contract.

Otros consejos

Connecting and querying are two separate concerns. As such, they should have two separate interfaces.

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

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

This both ensures that IDatabase will be connected when used and makes client not depend on interface it doesn't need.

Let's take a step back and look at the bigger picture here.

What is IDatabase's responsibility?

It has a few different operations:

  • Parse a connection string
  • Open a connection with a database (an external system)
  • Send messages to the database; the messages command the database to alter it's state
  • Receive responses from the database and transform them into a format the caller can use
  • Close the connection

Looking at this list, you might be thinking, "Doesn't this violate SRP?" But I don't think it does. All of the operations are part of a single, cohesive concept: manage a stateful connection to the database (an external system). It establishes the connection, it keeps track of the current state of the connection (in relation to operations done on other connections, in particular), it signals when to commit the connection's current state, etc. In this sense, it acts as an API that hides a lot of implementation details most callers won't care about. For example, does it use HTTP, sockets, pipes, custom TCP, HTTPS? Calling code doesn't care; it just wants to send messages and get responses. This is a good example of encapsulation.

Are we sure? Couldn't we split up some of these operations? Maybe, but there's no benefit. If you try to split them up, you're still going to need a central object that keeps the connection open and/or manages what the current state is. All the other operations are strongly coupled to the same state, and if you try to separate them, they're just going to end up delegating back to the connection object anyway. These operations are naturally and logically coupled to the state, and there's no way to separate them. Decoupling is great when we can do it, but in this case, we actually can't. At least not without a very different, stateless protocol to talk to the DB, and that would actually make very important problems like ACID compliance much harder. Also, in the process of trying to decouple these operations from the connection, you'll be forced to expose details about the protocol that callers don't care about, since you'll need a way of sending some kind of "arbitrary" message to the database.

Note that the fact we're dealing with a stateful protocol pretty solidly rules out your last alternative (passing connection string as a parameter).

Do we really need connection string to be set?

Yes. You can't open the connection until you have a connection string, and you can't do anything with the protocol until you open the connection. So it's pointless to have a connection object without one.

How do we solve the problem of requiring the connection string?

The problem we're trying to solve is that we want the object to be in a usable state at all times. What kind of entity is used to manage state in OO languages? Objects, not interfaces. Interfaces don't have state to manage. Because the problem you're trying to solve is a state management problem, an interface isn't really appropriate here. An abstract class is much more natural. So use an abstract class with a constructor.

You might also want to consider actually opening the connection during the constructor as well, since the connection is also useless before it's opened. That would require an abstract protected Open method since the process of opening a connection may be database specific. It would also be a good idea to make the ConnectionString property read only in this case, since changing the connection string after the connection is open would be meaningless. (Honestly, I'd make it read only anyway. If you want a connection with a different string, make another object.)

Do we need an interface at all?

An interface that specifies the available messages you can send over the connection and the types of responses you can get back could be useful. This would allow us to write code that executes these operations but isn't coupled to the logic of opening a connection. But that's the point: managing the connection is not part of the interface of, "What messages can I send and what messages can I get back to/from the database?", so the connection string shouldn't even be a part of that interface.

If we go this route, our code might look something like this:

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
}

I really don't see the reason to have an interface at all here. Your database class is SQL-specific, and really just gives you a convenient/safe way to make sure you aren't querying on a connection that isn't opened properly. If you insist on an interface though, here's how I'd do it.

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

The usage might look like this:

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");
}
Licenciado bajo: CC-BY-SA con atribución
scroll top