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
}