Допустим, у нас есть следующий интерфейс -
interface IDatabase {
string ConnectionString{get;set;}
void ExecuteNoQuery(string sql);
void ExecuteNoQuery(string[] sql);
//Various other methods all requiring ConnectionString to be set
}
Предварительное условие состоит в том, что ConnectionString должен быть установлен / инициализирован, прежде чем любой из методов может быть запущен.
Это предварительное условие может быть несколько достигнуто путем передачи connectionString через конструктор, если IDatabase был абстрактным или конкретным классом -
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
}
Вопросы -
- Есть ли способ указать это предварительное условие в самом интерфейсе? Это действительный «контракт», так что мне интересно, есть ли для этого языковая функция или шаблон (решение абстрактного класса - это скорее взлом, помимо необходимости создания двух типов - интерфейса и абстрактного класса - каждый раз это нужно)
- Это скорее теоретическое любопытство - действительно ли это предварительное условие подпадает под определение предварительного условия, как в контексте LSP?
c#
solid
liskov-substitution
Ахиллес
источник
источник
Ответы:
Да. Начиная с .Net 4.0 и выше, Microsoft предоставляет кодовые контракты . Они могут быть использованы для определения предварительных условий в форме
Contract.Requires( ConnectionString != null );
. Тем не менее, чтобы сделать это для интерфейса, вам все равно понадобится вспомогательный классIDatabaseContract
, к которому присоединяетсяIDatabase
, и предварительное условие должно быть определено для каждого отдельного метода вашего интерфейса, где он должен храниться. Смотрите здесь для обширного примера для интерфейсов.Да , LSP имеет дело как с синтаксической, так и с семантической частью контракта.
источник
Подключение и запросы - это две разные проблемы. Как таковые, они должны иметь два отдельных интерфейса.
Это гарантирует, что
IDatabase
при подключении он будет подключен, и делает клиента не зависимым от интерфейса, который ему не нужен.источник
IDatabase
Интерфейс определяет объект , способный установить соединение с базой данных , а затем выполнение произвольных запросов. Он является объектом , который выступает в качестве границы между базой данных и остальной части кода. Таким образом, этот объект должен поддерживать состояние (например, транзакцию), которое может повлиять на поведение запросов. Поместить их в один класс очень практично.Давайте сделаем шаг назад и посмотрим на общую картину здесь.
Какова
IDatabase
ответственность?У него есть несколько разных операций:
Глядя на этот список, вы можете подумать: «Разве это не нарушает SRP?» Но я не думаю, что это так. Все операции являются частью единой связной концепции: управление подключением с сохранением состояния к базе данных (внешней системе) . Он устанавливает соединение, отслеживает текущее состояние соединения (в частности, в отношении операций, выполняемых на других соединениях), сигнализирует, когда следует зафиксировать текущее состояние соединения, и т. Д. В этом смысле он действует как API это скрывает много деталей реализации, о которых большинство абонентов не заботятся. Например, он использует HTTP, сокеты, каналы, пользовательские TCP, HTTPS? Код вызова не волнует; он просто хочет отправлять сообщения и получать ответы. Это хороший пример инкапсуляции.
Мы уверены? Разве мы не можем разделить некоторые из этих операций? Может быть, но нет никакой пользы. Если вы попытаетесь разделить их, вам все равно понадобится центральный объект, который поддерживает соединение открытым и / или управляет текущим состоянием. Все остальные операции тесно связаны с одним и тем же состоянием, и если вы попытаетесь разделить их, они все равно будут в конечном итоге делегировать обратно объекту соединения. Эти операции естественно и логически связаны с государством, и их невозможно отделить. Разъединение замечательно, когда мы можем это сделать, но в этом случае мы на самом деле не можем, По крайней мере, без совсем другого протокола без сохранения состояния для связи с БД, и это на самом деле значительно усложнит очень важные проблемы, такие как соответствие ACID. Кроме того, в процессе отсоединения этих операций от соединения вы будете вынуждены раскрывать подробности о протоколе, который не имеет значения для вызывающих абонентов, поскольку вам потребуется способ отправки какого-либо «произвольного» сообщения. в базу данных.
Обратите внимание, что тот факт, что мы имеем дело с протоколом с отслеживанием состояния, довольно твердо исключает вашу последнюю альтернативу (передача строки подключения в качестве параметра).
Нам действительно нужно установить строку подключения?
Да. Вы не можете открыть соединение, пока у вас нет строки соединения, и вы не можете ничего сделать с протоколом, пока не откроете соединение. Поэтому бессмысленно иметь объект соединения без него.
Как мы решаем проблему с требованием строки подключения?
Проблема, которую мы пытаемся решить, заключается в том, что мы хотим, чтобы объект постоянно находился в рабочем состоянии. Какой тип сущности используется для управления состоянием на ОО-языках? Объекты , а не интерфейсы. Интерфейсы не имеют состояния для управления. Поскольку проблема, которую вы пытаетесь решить, является проблемой управления состоянием, интерфейс здесь не совсем подходит. Абстрактный класс гораздо естественнее. Так что используйте абстрактный класс с конструктором.
Вы также можете рассмотреть возможность фактического открытия соединения во время конструктора, так как соединение также бесполезно до его открытия. Это потребует абстрактного
protected Open
метода, поскольку процесс открытия соединения может зависеть от базы данных. В этом случае также было бы неплохо сделатьConnectionString
свойство доступным только для чтения, поскольку изменение строки соединения после открытия соединения не имеет смысла. (Честно говоря, я бы сделал это только для чтения. Если вам нужно соединение с другой строкой, создайте другой объект.)Нужен ли вообще интерфейс?
Может быть полезен интерфейс, который определяет доступные сообщения, которые вы можете отправлять через соединение, и типы ответов, которые вы можете получить обратно. Это позволило бы нам написать код, который выполняет эти операции, но не связан с логикой открытия соединения. Но в том-то и дело: управление соединением не является частью интерфейса «какие сообщения я могу отправлять и какие сообщения я могу возвращать в / из базы данных?», Поэтому строка соединения даже не должна быть частью этого интерфейс.
Если мы пойдем по этому пути, наш код может выглядеть примерно так:
источник
Open
метод должен быть,private
и вы должны предоставить защищенноеConnection
свойство, которое создает соединение и соединяется. Или выставить защищенныйOpenConnection
метод.Я действительно не вижу причины иметь интерфейс вообще здесь. Ваш класс базы данных специфичен для SQL и на самом деле просто дает вам удобный / безопасный способ убедиться, что вы не запрашиваете соединение, которое не открыто должным образом. Если вы настаиваете на интерфейсе, вот как я это сделаю.
Использование может выглядеть так:
источник