Как пишутся абстрактные интерфейсы базы данных для поддержки нескольких типов баз данных?

12

Как начать разработку абстрактного класса в более крупном приложении, которое может взаимодействовать с несколькими типами баз данных, такими как MySQL, SQLLite, MSSQL и т. Д.?

Как называется этот шаблон проектирования и где он начинается?

Допустим, вам нужно написать класс, который имеет следующие методы

public class Database {
   public DatabaseType databaseType;
   public Database (DatabaseType databaseType){
      this.databaseType = databaseType;
   }

   public void SaveToDatabase(){
       // Save some data to the db
   }
   public void ReadFromDatabase(){
      // Read some data from db
   }
}

//Application
public class Foo {
    public Database db = new Database (DatabaseType.MySQL);
    public void SaveData(){
        db.SaveToDatabase();
    }
}

Единственное, о чем я могу думать, это оператор if в каждом Databaseметоде.

public void SaveToDatabase(){
   if(databaseType == DatabaseType.MySQL){

   }
   else if(databaseType == DatabaseType.SQLLite){

   }
}
tones31
источник

Ответы:

11

То , что вы хотите , это несколько реализаций для интерфейса , что ваше приложение использует.

вот так:

public interface IDatabase
{
    void SaveToDatabase();
    void ReadFromDatabase();
}

public class MySQLDatabase : IDatabase
{
   public MySQLDatabase ()
   {
      //init stuff
   }

   public void SaveToDatabase(){
       //MySql implementation
   }
   public void ReadFromDatabase(){
      //MySql implementation
   }
}

public class SQLLiteDatabase : IDatabase
{
   public SQLLiteDatabase ()
   {
      //init stuff
   }

   public void SaveToDatabase(){
       //SQLLite implementation
   }
   public void ReadFromDatabase(){
      //SQLLite implementation
   }
}

//Application
public class Foo {
    public IDatabase db = GetDatabase();

    public void SaveData(){
        db.SaveToDatabase();
    }

    private IDatabase GetDatabase()
    {
        if(/*some way to tell if should use MySql*/)
            return new MySQLDatabase();
        else if(/*some way to tell if should use MySql*/)
            return new SQLLiteDatabase();

        throw new Exception("You forgot to configure the database!");
    }
}

Что касается лучшего способа настройки правильной IDatabaseреализации во время выполнения в вашем приложении, вы должны рассмотреть такие вещи, как « Factory Method » и « Dependancy Injection ».

Калеб
источник
25

Ответ Калеба, хотя он на правильном пути, на самом деле неверен. Его Fooкласс выступает в качестве фасада базы данных и фабрики. Это две обязанности, и их не следует объединять.


Этот вопрос, особенно в контексте базы данных, задавался слишком много раз. Здесь я попытаюсь подробно показать вам преимущества использования абстракции (использования интерфейсов), чтобы сделать ваше приложение менее связанным и более универсальным.

Прежде чем читать дальше, я рекомендую вам прочитать и получить базовое представление о внедрении зависимости , если вы еще этого не знаете. Вы также можете проверить шаблон проектирования адаптера , который в основном означает скрытие деталей реализации за открытыми методами интерфейса.

Внедрение зависимостей в сочетании с шаблоном проектирования Factory является основой и простым способом кодирования шаблона проектирования Strategy , являющегося частью принципа IoC .

Не звоните нам, мы вам позвоним . (АКА Голливудский принцип ).


Отделение приложения с помощью абстракции

1. Создание слоя абстракции

Вы создаете интерфейс - или абстрактный класс, если вы программируете на языке, подобном C ++, - и добавляете универсальные методы в этот интерфейс. Поскольку и интерфейсы, и абстрактные классы ведут себя так, что вы не можете использовать их напрямую, но вы должны либо реализовать (в случае интерфейса), либо расширить (в случае абстрактного класса) их, сам код уже предполагает, что вы будете необходимо иметь конкретные реализации для выполнения контракта, заданного интерфейсом или абстрактным классом.

Ваш (очень простой пример) интерфейс базы данных может выглядеть следующим образом (классы DatabaseResult или DbQuery соответственно будут вашими собственными реализациями, представляющими операции с базой данных):

public interface Database
{
    DatabaseResult DoQuery(DbQuery query);
    void BeginTransaction();
    void RollbackTransaction();
    void CommitTransaction();
    bool IsInTransaction();
}

Поскольку это интерфейс, он сам ничего не делает. Поэтому вам нужен класс для реализации этого интерфейса.

public class MyMySQLDatabase : Database
{
    private readonly CSharpMySQLDriver _mySQLDriver;

    public MyMySQLDatabase(CSharpMySQLDriver mySQLDriver)
    {
        _mySQLDriver = mySQLDriver;
    }

    public DatabaseResult DoQuery(DbQuery query)
    {
        // This is a place where you will use _mySQLDriver to handle the DbQuery
    }

    public void BeginTransaction()
    {
        // This is a place where you will use _mySQLDriver to begin transaction
    }

    public void RollbackTransaction()
    {
    // This is a place where you will use _mySQLDriver to rollback transaction
    }

    public void CommitTransaction()
    {
    // This is a place where you will use _mySQLDriver to commit transaction
    }

    public bool IsInTransaction()
    {
    // This is a place where you will use _mySQLDriver to check, whether you are in a transaction
    }
}

Теперь у вас есть класс, который реализует Database, интерфейс стал просто полезным.

2. Использование слоя абстракции

Где-то в вашем приложении у вас есть метод, давайте вызовем метод SecretMethod, просто для удовольствия, и внутри этого метода вы должны использовать базу данных, потому что вы хотите получить некоторые данные.

Теперь у вас есть интерфейс, который вы не можете создать напрямую (ну, как мне тогда его использовать), но у вас есть класс MyMySQLDatabase, который может быть создан с использованием newключевого слова.

ОТЛИЧНЫЙ! Я хочу использовать базу данных, поэтому я буду использовать MyMySQLDatabase.

Ваш метод может выглядеть так:

public void SecretMethod()
{
    var database = new MyMySQLDatabase(new CSharpMySQLDriver());

    // you will use the database here, which has the DoQuery,
    // BeginTransaction, RollbackTransaction and CommitTransaction methods
}

Это не хорошо. Вы непосредственно создаете класс внутри этого метода, и если вы делаете это внутри SecretMethod, можно с уверенностью предположить, что вы будете делать то же самое в 30 других методах. Если вы хотите изменить MyMySQLDatabaseкласс на другой, например MyPostgreSQLDatabase, вам придется изменить его во всех ваших 30 методах.

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

Мы начнем с рефакторинга создания MyMySQLDatabase, передав его в качестве параметра методу (это называется внедрением зависимости).

public void SecretMethod(MyMySQLDatabase database)
{
    // use the database here
}

Это решает вам проблему, что MyMySQLDatabaseобъект никогда не может быть создан. Поскольку объект SecretMethodожидает действительный MyMySQLDatabaseобъект, если что-то случится и объект никогда не будет передан ему, метод никогда не запустится. И это совершенно нормально.


В некоторых приложениях этого может быть достаточно. Вы можете быть удовлетворены, но давайте сделаем рефакторинг еще лучше.

Цель другого рефакторинга

Вы можете видеть, прямо сейчас SecretMethodиспользует MyMySQLDatabaseобъект. Предположим, вы перешли с MySQL на MSSQL. Вы действительно не чувствуете , как меняется вся логика внутри вашего SecretMethod, метод , который называет BeginTransactionи CommitTransactionметоды по databaseпеременной , переданной в качестве параметра, так что вы создать новый класс MyMSSQLDatabase, который также будет иметь BeginTransactionи CommitTransactionметоды.

Тогда вы идете вперед и измените объявление SecretMethodна следующее.

public void SecretMethod(MyMSSQLDatabase database)
{
    // use the database here
}

А поскольку классы MyMSSQLDatabaseи MyMySQLDatabaseимеют одинаковые методы, вам не нужно ничего менять, и все равно будет работать.

Ой, подождите!

У вас есть Databaseинтерфейс, который MyMySQLDatabaseреализует, у вас также есть MyMSSQLDatabaseкласс, который имеет те же методы, что и MyMySQLDatabase, возможно, драйвер MSSQL мог бы также реализовать Databaseинтерфейс, поэтому вы добавляете его в определение.

public class MyMSSQLDatabase : Database { }

Но что, если я в будущем больше не буду использовать MyMSSQLDatabase, потому что я перешел на PostgreSQL? Я должен был бы снова заменить определение SecretMethod?

Да, вы бы. И это не звучит правильно. Прямо сейчас мы знаем, что MyMSSQLDatabaseи у MyMySQLDatabaseнас одни и те же методы и оба реализуют Databaseинтерфейс. Таким образом, вы рефакторинг SecretMethodвыглядит так.

public void SecretMethod(Database database)
{
    // use the database here
}

Обратите внимание, как SecretMethodбольше не знает, используете ли вы MySQL, MSSQL или PotgreSQL. Он знает, что использует базу данных, но не заботится о конкретной реализации.

Теперь, если вы хотите создать свой новый драйвер базы данных, например, для PostgreSQL, вам не нужно менять его SecretMethodвообще. Вы сделаете MyPostgreSQLDatabase, сделаете так, чтобы он реализовывал Databaseинтерфейс, и как только вы закончите кодировать драйвер PostgreSQL, и он заработает, вы создадите его экземпляр и внедрите его в SecretMethod.

3. Получение желаемой реализации Database

Вы все еще должны решить, прежде чем вызывать SecretMethod, какую реализацию Databaseинтерфейса вы хотите (будь то MySQL, MSSQL или PostgreSQL). Для этого вы можете использовать шаблон фабричного дизайна.

public class DatabaseFactory
{
    private Config _config;

    public DatabaseFactory(Config config)
    {
        _config = config;
    }

    public Database getDatabase()
    {
        var databaseType = _config.GetDatabaseType();

        Database database = null;

        switch (databaseType)
        {
        case DatabaseEnum.MySQL:
            database = new MyMySQLDatabase(new CSharpMySQLDriver());
            break;
        case DatabaseEnum.MSSQL:
            database = new MyMSSQLDatabase(new CSharpMSSQLDriver());
            break;
        case DatabaseEnum.PostgreSQL:
            database = new MyPostgreSQLDatabase(new CSharpPostgreSQLDriver());
            break;
        default:
            throw new DatabaseDriverNotImplementedException();
            break;
        }

        return database;
    }
}

Как вы видите, фабрика знает, какой тип базы данных использовать из файла конфигурации (опять же, Configкласс может быть вашей собственной реализацией).

В идеале у вас будет DatabaseFactoryвнутри вашего контейнера инъекций зависимости. Ваш процесс может выглядеть следующим образом.

public class ProcessWhichCallsTheSecretMethod
{
    private DIContainer _di;
    private ClassWithSecretMethod _secret;

    public ProcessWhichCallsTheSecretMethod(DIContainer di, ClassWithSecretMethod secret)
    {
        _di = di;
        _secret = secret;
    }

    public void TheProcessMethod()
    {
        Database database = _di.Factories.DatabaseFactory.getDatabase();
        _secret.SecretMethod(database);
    }
}

Посмотрите, как нигде в процессе вы создаете базу данных определенного типа. Мало того, вы вообще ничего не создаете. Вы вызываете GetDatabaseметод для DatabaseFactoryобъекта, хранящегося в контейнере внедрения зависимостей ( _diпеременная), метод, который вернет вам правильный экземпляр Databaseинтерфейса на основе вашей конфигурации.

Если после 3 недель использования PostgreSQL вы захотите вернуться к MySQL, вы откроете один файл конфигурации и измените значение DatabaseDriverполя с DatabaseEnum.PostgreSQLна DatabaseEnum.MySQL. И вы сделали. Внезапно остальная часть вашего приложения снова правильно использует MySQL, изменив одну строку.


Если вы все еще не удивлены, я рекомендую вам немного погрузиться в IoC. Как вы можете принимать определенные решения не из конфига, а из пользовательского ввода. Этот подход называется шаблоном стратегии и, хотя он может использоваться и используется в корпоративных приложениях, он гораздо чаще используется при разработке компьютерных игр.

Энди
источник
Люблю твой ответ, Дэвид. Но, как и все такие ответы, он не дает описания того, как его можно применить на практике. Настоящая проблема заключается не в том, чтобы абстрагироваться от возможности обращения к различным механизмам баз данных, а в том, что касается фактического синтаксиса SQL. Возьмите ваш DbQueryобъект, например. Предполагая, что этот объект содержит элемент для строки SQL-запроса, который может быть выполнен, как можно сделать это универсальным?
DonBoitnott
1
@ DonBoitnott Я не думаю, что вам когда-нибудь понадобится все, чтобы быть универсальным. Обычно вы хотите ввести абстракцию между уровнями приложения (домен, сервисы, постоянство), вы можете также захотеть ввести абстракцию для модулей, вы можете захотеть ввести абстракцию для небольшой, но многократно используемой и настраиваемой библиотеки, которую вы разрабатываете для более крупного проекта, и т. Д. Вы можете просто абстрагировать все до интерфейсов, но это редко необходимо. Действительно сложно дать ответ «один за все», потому что, к сожалению, на самом деле его нет, и он исходит из требований.
Энди
2
Понял. Но я действительно имел в виду это буквально. Как только у вас есть абстрагированный класс, и вы дошли до того, что вы хотите вызвать, _secret.SecretMethod(database);как можно согласовать всю эту работу с тем фактом, что теперь мне SecretMethodеще нужно знать, с какой БД я работаю, чтобы использовать правильный диалект SQL ? Вы очень усердно работали, чтобы сохранить большую часть кода в неведении об этом факте, но затем, на 11-м часу, вы снова должны знать. Я сейчас в такой ситуации и пытаюсь выяснить, как другие решили эту проблему.
DonBoitnott
@ DonBoitnott Я не знал, что ты имел в виду, я вижу это сейчас. Вы могли бы использовать интерфейс вместо конкретных реализаций DbQueryкласса, предоставить реализации указанного интерфейса и использовать его вместо этого, имея фабрику для создания IDbQueryэкземпляра. Я не думаю, что вам понадобится универсальный тип для DatabaseResultкласса, вы всегда можете ожидать, что результаты из базы данных будут отформатированы аналогичным образом. Дело в том, что при работе с базами данных и необработанным SQL вы уже находитесь на таком низком уровне в своем приложении (за DAL и репозиториями), что в этом нет необходимости ...
Энди,
... универсальный подход больше.
Энди