Как указать предварительное условие (LSP) в интерфейсе в C #?

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 должен быть установлен / инициализирован, прежде чем любой из методов может быть запущен.

Это предварительное условие может быть несколько достигнуто путем передачи 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
}

Вопросы -

  1. Есть ли способ указать это предварительное условие в самом интерфейсе? Это действительный «контракт», так что мне интересно, есть ли для этого языковая функция или шаблон (решение абстрактного класса - это скорее взлом, помимо необходимости создания двух типов - интерфейса и абстрактного класса - каждый раз это нужно)
  2. Это скорее теоретическое любопытство - действительно ли это предварительное условие подпадает под определение предварительного условия, как в контексте LSP?
Ахиллес
источник
2
По "ЛСП" вы, ребята, говорите о принципе замены Лискова? Принцип "если это шарлатан как утка, но нуждается в батареях, это не утка"? Потому что, на мой взгляд, это скорее нарушение ISP и SRP, может быть, даже OCP, но не LSP.
Себастьян
2
Точно так же вы знаете, что вся эта концепция «ConnectionString должна быть установлена ​​/ инициализирована до того, как какой-либо из методов может быть запущен», является примером временной привязки blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling, и ее следует избегать, если возможный.
Ричибан
Симанн действительно большой поклонник абстрактной фабрики.
Адриан

Ответы:

10
  1. Да. Начиная с .Net 4.0 и выше, Microsoft предоставляет кодовые контракты . Они могут быть использованы для определения предварительных условий в форме Contract.Requires( ConnectionString != null );. Тем не менее, чтобы сделать это для интерфейса, вам все равно понадобится вспомогательный класс 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при подключении он будет подключен, и делает клиента не зависимым от интерфейса, который ему не нужен.

Euphoric
источник
Можно было бы более четко
заявить
@Caleth: это не «общая схема соблюдения предварительных условий». Это решение для этого конкретного требования обеспечения того, чтобы соединение происходило раньше всего. Другие предварительные условия потребуют других решений (таких, которые я упомянул в своем ответе). Я хотел бы добавить для этого требования, я бы явно предпочел предложение Euphoric, чем мое, потому что оно намного проще и не требует каких-либо дополнительных сторонних компонентов.
Док Браун
Конкретная requrement что - то происходит , прежде чем что - то еще широко применяется. Я также думаю, что ваш ответ лучше соответствует этому вопросу , но этот ответ может быть улучшен
Caleth
1
Этот ответ совершенно не соответствует сути. IDatabaseИнтерфейс определяет объект , способный установить соединение с базой данных , а затем выполнение произвольных запросов. Он является объектом , который выступает в качестве границы между базой данных и остальной части кода. Таким образом, этот объект должен поддерживать состояние (например, транзакцию), которое может повлиять на поведение запросов. Поместить их в один класс очень практично.
jpmc26
4
@ jpmc26 Ни одно из ваших возражений не имеет смысла, так как состояние может поддерживаться в классе, реализующем IDatabase. Он также может ссылаться на родительский класс, который его создал, тем самым получая доступ ко всему состоянию базы данных.
Euphoric
5

Давайте сделаем шаг назад и посмотрим на общую картину здесь.

Какова 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
}
jpmc26
источник
Был бы признателен, если downvoter объяснил бы их причину несогласия.
jpmc26
Договорились, re: downvoter. Это правильное решение. Строка подключения должна быть предоставлена ​​в конструкторе к конкретному / абстрактному классу. Грязное дело открытия / закрытия соединения не касается кода, использующего этот объект, и должно оставаться внутренним для самого класса. Я бы сказал, что Openметод должен быть, privateи вы должны предоставить защищенное Connectionсвойство, которое создает соединение и соединяется. Или выставить защищенный OpenConnectionметод.
Грег Бургхардт
Это решение довольно элегантно и очень удачно оформлено. Но я думаю, что некоторые из обоснований проектных решений неверны. Главным образом в первых нескольких параграфах о SRP. Это нарушает SRP, даже как объяснено в разделе «Какова ответственность IDatabase?». Обязанности, представленные для SRP, - это не просто то, что делает класс или управляет им. Это также «актеры» или «причины для перемен». И я думаю, что это нарушает SRP, потому что «Получать ответы из базы данных и преобразовывать их в формат, который может использовать вызывающий абонент» имеет совершенно другую причину для изменения, чем «Анализ строки подключения».
Себастьян
Тем не менее я поддерживаю это.
Себастьян
1
И кстати, твердые не Евангелие. Конечно, их очень важно помнить при разработке решения. Но вы МОЖЕТЕ нарушить их, если знаете, ПОЧЕМУ вы это делаете, КАК это повлияет на ваше решение, и КАК исправить ситуацию с помощью рефакторинга, если это приведет к неприятностям. Таким образом, я думаю, что даже если вышеупомянутое решение нарушает ПСП, оно пока лучшее.
Себастьян
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");
}
Грэхем
источник