Як вказати передумову (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 повинен бути встановлений / ініціалізований перед тим, як запустити будь-який із методів.

Цю передумову можна дещо досягти, передаючи з'єднанняString через конструктор, якщо 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. Це скоріше теоретична цікавість - чи ця передумова насправді підпадає під визначення передумови як у контексті ЛСП?

2
"ЛСП" ви, хлопці, говорите про принцип заміни Ліскова? Принцип "якщо його квакання подобається качці, але потрібні батарейки, це не качка"? Тому що, як я бачу, це більше порушення ISP і SRP, можливо, навіть OCP, але насправді не LSP.
Себастьян

2
Так що ви знаєте, вся ця концепція "ConnectionString повинна бути встановлена ​​/ ініціалізована перед тим, як запустити будь-який з методів", є прикладом тимчасового з'єднання blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling, і цього слід уникати, якщо можливо.
Річібан

Seemann є дійсно великим шанувальником абстрактного заводу.
Адріан Іфтоде

Відповіді:


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


Може бути більш явним щодо "це схема виконання передумов за допомогою типів"
Caleth

@Caleth: це не "загальна модель виконання передумов". Це рішення цієї конкретної вимоги щодо забезпечення того, щоб з'єднання відбулося раніше, ніж будь-що інше. Інші передумови потребуватимуть різних рішень (як той, про який я згадував у своїй відповіді). Я хотів би додати цю вимогу, я чітко віддав би пропозицію Ейфорика над моєю, оскільки це набагато простіше і не потребує додаткових сторонніх компонентів.
Док Браун

Конкретна вимога, що щось відбувається раніше, ніж щось інше , широко застосовується. Я також думаю, що ваша відповідь краще відповідає цьому питанню , але цю відповідь можна вдосконалити
Калет

1
Ця відповідь повністю пропускає суть. IDatabaseІнтерфейс визначає об'єкт , здатний встановити з'єднання з базою даних , а потім виконання довільних запитів. Він є об'єктом , який виступає в якості кордону між базою даних і решти коду. Таким чином, цей об'єкт повинен підтримувати стан (наприклад, транзакцію), який може впливати на поведінку запитів. Посадити їх в один клас дуже практично.
jpmc26

4
@ jpmc26 Жодне з ваших заперечень не має сенсу, оскільки стан можна підтримувати в класі, що реалізує IDatabase. Він також може посилатися на батьківський клас, який його створив, отримуючи таким чином доступ до всього стану бази даних.
Ейфорій

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

Домовились, re: downvoter. Це правильне рішення. Рядок з'єднання повинен бути наданий у конструкторі до класу конкретного / абстрактного. Безладний бізнес відкриття / закриття з'єднання не стосується коду, що використовує цей об'єкт, і повинен залишатися внутрішнім для самого класу. Я заперечую, що Openметод повинен бути, privateі ви повинні відкрити захищене Connectionвластивість, яке створює з'єднання та з'єднує. Або викрийте захищений OpenConnectionметод.
Грег Бургхардт

Це рішення досить елегантний і дуже вдалий дизайн. Але я думаю, що деякі міркування дизайнерських рішень неправильні. Головним чином у перших кількох абзацах про СРП. Це порушує SRP, навіть як це пояснено в "Що таке відповідальність IDatabase?". Обов'язки, які поглядають на SRP, - це не лише те, що клас чи керує ним. Її також "актори" або "причини зміни". І я думаю, що це порушує SRP, оскільки "Отримувати відповіді з бази даних та перетворювати їх у формат, який може використовувати абонент", має зовсім іншу причину для зміни, ніж "Розбір рядка з'єднання".
Себастьєн

Все-таки я підтримую це.
Себастьян

1
І BTW, SOLID - не євангелія. Впевнені, що це дуже важливо пам’ятати при розробці рішення. Але ви МОЖЕТЕ порушити їх, якщо знаєте, ЧОМУ ви це зробите, ЯК це вплине на ваше рішення і ЯК виправити речі за допомогою рефакторингу, якщо це призведе до проблем. Тому я думаю, навіть якщо вищезгадане рішення порушує СРП, воно є найкращим поки що.
Себастьян

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");
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.