Давайте сделаем шаг назад и посмотрим на общую картину здесь.
Какова IDatabase
ответственность?
У него есть несколько разных операций:
- Разобрать строку подключения
- Открыть соединение с базой данных (внешняя система)
- Отправлять сообщения в базу данных; сообщения дают команду базе данных изменить ее состояние
- Получать ответы из базы данных и преобразовывать их в формат, который может использовать вызывающий
- Закрыть соединение
Глядя на этот список, вы можете подумать: «Разве это не нарушает SRP?» Но я не думаю, что это так. Все операции являются частью единой связной концепции: управление подключением с сохранением состояния к базе данных (внешней системе) . Он устанавливает соединение, отслеживает текущее состояние соединения (в частности, в отношении операций, выполняемых на других соединениях), сигнализирует, когда следует зафиксировать текущее состояние соединения, и т. Д. В этом смысле он действует как API это скрывает много деталей реализации, о которых большинство абонентов не заботятся. Например, он использует HTTP, сокеты, каналы, пользовательские TCP, HTTPS? Код вызова не волнует; он просто хочет отправлять сообщения и получать ответы. Это хороший пример инкапсуляции.
Мы уверены? Разве мы не можем разделить некоторые из этих операций? Может быть, но нет никакой пользы. Если вы попытаетесь разделить их, вам все равно понадобится центральный объект, который поддерживает соединение открытым и / или управляет текущим состоянием. Все остальные операции тесно связаны с одним и тем же состоянием, и если вы попытаетесь разделить их, они все равно будут в конечном итоге делегировать обратно объекту соединения. Эти операции естественно и логически связаны с государством, и их невозможно отделить. Разъединение замечательно, когда мы можем это сделать, но в этом случае мы на самом деле не можем, По крайней мере, без совсем другого протокола без сохранения состояния для связи с БД, и это на самом деле значительно усложнит очень важные проблемы, такие как соответствие 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
}