Como as interfaces abstratas do banco de dados são escritas para suportar vários tipos de banco de dados?

12

Como se começa a projetar uma classe abstrata em seu aplicativo maior que pode interagir com vários tipos de bancos de dados, como MySQL, SQLLite, MSSQL, etc?

Como é chamado esse padrão de design e onde exatamente ele começa?

Digamos que você precise escrever uma classe que possua os seguintes métodos

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();
    }
}

A única coisa que consigo pensar é uma declaração if em cada Databasemétodo

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

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

   }
}
tones31
fonte

Respostas:

11

O que você deseja é várias implementações para a interface que seu aplicativo usa.

igual a:

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!");
    }
}

Quanto a uma maneira melhor de configurar a IDatabaseimplementação correta em tempo de execução no seu aplicativo, você deve procurar coisas como " Método de Fábrica " e " Injeção de Dependência ".

Caleb
fonte
25

A resposta de Caleb, enquanto ele está no caminho certo, está realmente errado. Sua Footurma atua tanto como fachada quanto fábrica de banco de dados. Essas são duas responsabilidades e não devem ser colocadas em uma única classe.


Esta pergunta, especialmente no contexto do banco de dados, já foi feita muitas vezes. Aqui, tentarei mostrar minuciosamente o benefício do uso de abstração (usando interfaces) para tornar seu aplicativo menos acoplado e mais versátil.

Antes de ler mais, recomendo que você leia e obtenha um entendimento básico da injeção de dependência , se ainda não o conhece. Você também pode verificar o padrão de design do adaptador , que é basicamente o que significa ocultar os detalhes da implementação por trás dos métodos públicos da interface.

A injeção de dependência, juntamente com o padrão de design da fábrica , é a pedra fundamental e uma maneira fácil de codificar o padrão de design da estratégia , que faz parte do princípio da IoC .

Não ligue para nós, nós ligaremos para você . (Também conhecido como o princípio de Hollywood ).


Desacoplar um aplicativo usando abstração

1. Fazendo a camada de abstração

Você cria uma interface - ou classe abstrata, se estiver codificando em uma linguagem como C ++ - e adiciona métodos genéricos a essa interface. Como as interfaces e as classes abstratas têm o comportamento de você não poder usá-las diretamente, mas você deve implementá-las (no caso de interface) ou estendê-las (no caso de classe abstrata), o próprio código já sugere, você É necessário ter implementações específicas para cumprir o contrato fornecido pela interface ou pela classe abstrata.

Sua interface de banco de dados (exemplo muito simples) pode ter esta aparência (as classes DatabaseResult ou DbQuery, respectivamente, seriam suas próprias implementações que representam operações de banco de dados):

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

Por ser uma interface, ela própria não faz nada. Então você precisa de uma classe para implementar essa interface.

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
    }
}

Agora você tem uma classe que implementa o Database, a interface tornou-se útil.

2. Usando a camada de abstração

Em algum lugar do seu aplicativo, você tem um método, vamos chamá-lo SecretMethodapenas por diversão, e dentro desse método você precisa usar o banco de dados, porque deseja buscar alguns dados.

Agora você tem uma interface que não pode ser criada diretamente (como é que eu uso então), mas você tem uma classe MyMySQLDatabaseque pode ser construída usando a newpalavra - chave.

ÓTIMO! Eu quero usar um banco de dados, então usarei o MyMySQLDatabase.

Seu método pode ficar assim:

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

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

Isto não é bom. Você está criando diretamente uma classe dentro desse método e, se estiver fazendo isso dentro do SecretMethod, é seguro assumir que você faria o mesmo em outros 30 métodos. Se você quisesse mudar MyMySQLDatabasepara uma classe diferente, como, por exemplo MyPostgreSQLDatabase, teria que alterá-lo em todos os seus 30 métodos.

Outro problema é que, se a criação da MyMySQLDatabasefalha, o método nunca seria concluído e, portanto, seria inválido.

Começamos refatorando a criação do MyMySQLDatabase, passando-o como um parâmetro para o método (isso é chamado de injeção de dependência).

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

Isso resolve o problema, que o MyMySQLDatabaseobjeto nunca pode ser criado. Como o objeto SecretMethodespera um MyMySQLDatabaseobjeto válido , se algo acontecesse e o objeto nunca fosse passado para ele, o método nunca seria executado. E isso é totalmente bom.


Em alguns aplicativos, isso pode ser suficiente. Você pode estar satisfeito, mas vamos refatorá-lo para ser ainda melhor.

O objetivo de outra refatoração

Você pode ver, agora, que SecretMethodusa um MyMySQLDatabaseobjeto. Vamos supor que você mudou do MySQL para o MSSQL. Você realmente não deseja alterar toda a lógica dentro do seu SecretMethod, um método que chama a BeginTransactione CommitTransactionmétodos na databasevariável passada como parâmetro, para criar uma nova classe MyMSSQLDatabase, que também terá os métodos BeginTransactione CommitTransaction.

Então vá em frente e altere a declaração de SecretMethodpara o seguinte.

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

E como as classes MyMSSQLDatabasee MyMySQLDatabasetêm os mesmos métodos, você não precisa alterar mais nada e ainda funcionará.

Oh espere!

Você tem uma Databaseinterface, que MyMySQLDatabaseimplementa, também possui a MyMSSQLDatabaseclasse, que possui exatamente os mesmos métodos que MyMySQLDatabase, talvez o driver MSSQL também possa implementar a Databaseinterface, portanto, você a adiciona à definição.

public class MyMSSQLDatabase : Database { }

Mas e se eu, no futuro, não quiser MyMSSQLDatabasemais usar , porque mudei para o PostgreSQL? Eu teria que, novamente, substituir a definição de SecretMethod?

Sim, você seria. E isso não parece certo. Agora nós sabemos, que MyMSSQLDatabasee MyMySQLDatabasetêm os mesmos métodos e ambos implementar a Databaseinterface. Então você refatora SecretMethodpara ficar assim.

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

Observe como você SecretMethodnão sabe mais se está usando MySQL, MSSQL ou PotgreSQL. Ele sabe que usa um banco de dados, mas não se importa com a implementação específica.

Agora, se você quiser criar seu novo driver de banco de dados, para o PostgreSQL, por exemplo, não precisará alterar SecretMethodnada. Você criará MyPostgreSQLDatabase, implementará a Databaseinterface e, assim que terminar de codificar o driver PostgreSQL e ele funcionar, você criará sua instância e a injetará no SecretMethod.

3. Obtenção da implementação desejada de Database

Você ainda precisa decidir, antes de chamar SecretMethod, qual implementação da Databaseinterface você deseja (seja MySQL, MSSQL ou PostgreSQL). Para isso, você pode usar o padrão de design de fábrica.

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;
    }
}

A fábrica, como você pode ver, sabe qual tipo de banco de dados usar em um arquivo de configuração (novamente, a Configclasse pode ser sua própria implementação).

Idealmente, você terá o DatabaseFactoryinterior do contêiner de injeção de dependência. Seu processo pode ser assim.

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);
    }
}

Veja como em nenhum lugar do processo você está criando um tipo de banco de dados específico. Não é só isso, você não está criando nada. Você está chamando um GetDatabasemétodo no DatabaseFactoryobjeto armazenado no contêiner de injeção de dependência (a _divariável), um método que retornará a instância correta da Databaseinterface, com base em sua configuração.

Se, após 3 semanas de uso do PostgreSQL, você quiser voltar ao MySQL, abra um único arquivo de configuração e altere o valor do DatabaseDrivercampo de DatabaseEnum.PostgreSQLpara DatabaseEnum.MySQL. E você terminou. De repente, o restante do seu aplicativo usa corretamente o MySQL novamente, alterando uma única linha.


Se você ainda não se surpreender, recomendo que você mergulhe um pouco mais na IoC. Como você pode tomar certas decisões não a partir de uma configuração, mas de uma entrada do usuário. Essa abordagem é chamada de padrão de estratégia e, embora possa ser e é usada em aplicativos corporativos, é muito mais frequentemente usada no desenvolvimento de jogos de computador.

Andy
fonte
Ame sua resposta, David. Mas, como todas essas respostas, deixa de descrever como alguém pode colocá-lo em prática. O problema real não é abstrair a capacidade de chamar diferentes mecanismos de banco de dados; o problema é a sintaxe SQL real. Pegue seu DbQueryobjeto, por exemplo. Supondo que esse objeto contivesse um membro para que uma string de consulta SQL fosse executada, como alguém pode torná-lo genérico?
23416 DonBoitnott
1
@DonBoitnott Acho que você nunca precisaria de tudo para ser genérico. Você geralmente deseja introduzir abstração entre as camadas do aplicativo (domínio, serviços, persitência), também pode introduzir abstração para módulos, pode querer introduzir abstração em uma biblioteca pequena, mas reutilizável e altamente personalizável que você está desenvolvendo para um projeto maior, etc. Você pode simplesmente abstrair tudo nas interfaces, mas isso raramente é necessário. É realmente difícil dar uma resposta tudo por tudo, porque, infelizmente, realmente não existe e vem de requisitos.
Andy
2
Entendido. Mas eu realmente quis dizer isso literalmente. Depois de ter sua classe abstraída e chegar ao ponto em que você deseja chamar, _secret.SecretMethod(database);como alguém reconcilia tudo isso com o fato de que agora SecretMethodainda preciso saber em que banco de dados estou trabalhando para usar o dialeto SQL adequado ? Você trabalhou muito para manter a maioria do código ignorante desse fato, mas, na 11ª hora, novamente deve saber. Estou nessa situação agora e tentando descobrir como os outros resolveram esse problema.
22416 DonBoitnott
@DonBoitnott Eu não sabia o que você queria dizer, eu vejo agora. Você poderia usar uma interface em vez de implementações concretas da DbQueryclasse, fornecer implementações da referida interface e usá-la, tendo uma fábrica para construir a IDbQueryinstância. Eu não acho que você precisaria de um tipo genérico para a DatabaseResultclasse; você sempre pode esperar que os resultados de um banco de dados sejam formatados de maneira semelhante. A coisa aqui é, quando se trata de bancos de dados e SQL cru, você já está em um nível tão baixo de sua aplicação (por trás DAL e Repositórios), que não há necessidade de ...
Andy
... abordagem genérica mais.
Andy