¿Cómo se escriben las interfaces de bases de datos abstractas para admitir múltiples tipos de bases de datos?

12

¿Cómo se comienza a diseñar una clase abstracta en su aplicación más grande que pueda interactuar con varios tipos de bases de datos, como MySQL, SQLLite, MSSQL, etc.?

¿Cómo se llama este patrón de diseño y dónde comienza exactamente?

Digamos que necesita escribir una clase que tenga los siguientes 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();
    }
}

Lo único que se me ocurre es una declaración if en cada Databasemétodo

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

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

   }
}
tonos31
fuente

Respuestas:

11

Lo que desea es implementaciones múltiples para la interfaz que utiliza su aplicación.

al igual que:

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

En cuanto a una mejor manera de configurar la IDatabaseimplementación correcta en tiempo de ejecución en su aplicación, debe buscar cosas como " Método de fábrica " e " Inyección de dependencia ".

Caleb
fuente
25

La respuesta de Caleb, mientras está en el camino correcto, es realmente incorrecta. Su Fooclase actúa tanto como fachada de base de datos como fábrica. Esas son dos responsabilidades y no se deben poner en una sola clase.


Esta pregunta, especialmente en el contexto de la base de datos, se ha hecho demasiadas veces. Aquí intentaré mostrarle a fondo el beneficio de usar la abstracción (usando interfaces) para hacer que su aplicación sea menos acoplada y más versátil.

Antes de seguir leyendo, le recomiendo que lea y obtenga una comprensión básica de la inyección de dependencia , si aún no lo sabe. También es posible que desee verificar el patrón de diseño del Adaptador , que es básicamente lo que significa ocultar los detalles de implementación detrás de los métodos públicos de la interfaz.

La inyección de dependencia, junto con el patrón de diseño de fábrica , es la piedra angular y una forma fácil de codificar el patrón de diseño de estrategia , que es parte del principio de IoC .

No nos llames, te llamaremos . (También conocido como el principio de Hollywood ).


Desacoplar una aplicación usando abstracción

1. Hacer la capa de abstracción

Crea una interfaz, o una clase abstracta, si está codificando en un lenguaje como C ++, y agrega métodos genéricos a esta interfaz. Debido a que tanto las interfaces como las clases abstractas tienen el comportamiento de que no puedes usarlas directamente, pero debes implementarlas (en el caso de la interfaz) o extenderlas (en el caso de la clase abstracta), el código en sí ya sugiere que lo harás. necesita tener implementaciones específicas para completar el contrato dado por la interfaz o la clase abstracta.

Su interfaz de base de datos (ejemplo muy simple) podría verse así (las clases DatabaseResult o DbQuery respectivamente serían sus propias implementaciones que representan las operaciones de la base de datos):

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

Como se trata de una interfaz, en sí misma no hace nada. Entonces necesita una clase para implementar esta interfaz.

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

Ahora que tiene una clase que implementa Database, la interfaz se volvió útil.

2. Usando la capa de abstracción

En algún lugar de su aplicación, tiene un método, llamemos al método SecretMethod, solo por diversión, y dentro de este método debe usar la base de datos, porque desea obtener algunos datos.

Ahora tiene una interfaz, que no puede crear directamente (eh, ¿cómo la uso entonces?), Pero tiene una clase MyMySQLDatabase, que puede construirse utilizando la newpalabra clave.

¡GENIAL! Quiero usar una base de datos, así que usaré el MyMySQLDatabase.

Su método podría verse así:

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

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

Esto no está bien. Está creando directamente una clase dentro de este método, y si lo está haciendo dentro del SecretMethod, es seguro asumir que estaría haciendo lo mismo en otros 30 métodos. Si quisiera cambiarlo MyMySQLDatabasea una clase diferente, por ejemplo MyPostgreSQLDatabase, tendría que cambiarlo en sus 30 métodos.

Otro problema es que, si MyMySQLDatabasefalla la creación de , el método nunca terminaría y, por lo tanto, no sería válido.

Comenzamos refactorizando la creación de la MyMySQLDatabasepasándola como un parámetro al método (esto se llama inyección de dependencia).

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

Esto le resuelve el problema, que el MyMySQLDatabaseobjeto nunca podría ser creado. Debido a que SecretMethodespera un MyMySQLDatabaseobjeto válido , si algo sucediera y el objeto nunca se le pasara, el método nunca se ejecutaría. Y eso está totalmente bien.


En algunas aplicaciones esto podría ser suficiente. Puede estar satisfecho, pero refactoricemos para que sea aún mejor.

El propósito de otra refactorización

Puedes ver, ahora mismo SecretMethodusa un MyMySQLDatabaseobjeto. Supongamos que se mudó de MySQL a MSSQL. Realmente no tiene ganas de cambiar toda la lógica dentro de su SecretMethod, un método que llama a BeginTransactiony CommitTransactionmétodos en la databasevariable pasada como parámetro, por lo que crea una nueva clase MyMSSQLDatabase, que también tendrá los métodos BeginTransactiony CommitTransaction.

Luego continúe y cambie la declaración de SecretMethoda lo siguiente.

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

Y debido a que las clases MyMSSQLDatabasey MyMySQLDatabasetienen los mismos métodos, no necesita cambiar nada más y seguirá funcionando.

¡Oh espera!

Tiene una Databaseinterfaz, que MyMySQLDatabaseimplementa, también tiene la MyMSSQLDatabaseclase, que tiene exactamente los mismos métodos que MyMySQLDatabase, quizás el controlador MSSQL también podría implementar la Databaseinterfaz, por lo que debe agregarla a la definición.

public class MyMSSQLDatabase : Database { }

Pero, ¿qué MyMSSQLDatabasepasa si, en el futuro, ya no quiero usar más, porque cambié a PostgreSQL? Tendría que, nuevamente, reemplazar la definición de SecretMethod?

Sí lo harías Y eso no suena bien. Ahora mismo sabemos eso MyMSSQLDatabasey MyMySQLDatabasetenemos los mismos métodos y ambos implementamos la Databaseinterfaz. Así que refactorizas SecretMethodpara que se vea así.

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

Observe cómo SecretMethodya no sabe si está utilizando MySQL, MSSQL o PotgreSQL. Sabe que usa una base de datos, pero no le importa la implementación específica.

Ahora, si desea crear su nuevo controlador de base de datos, para PostgreSQL, por ejemplo, no necesitará cambiarlo SecretMethoden absoluto. Hará un MyPostgreSQLDatabase, hará que implemente la Databaseinterfaz y una vez que haya terminado de codificar el controlador PostgreSQL y funcione, creará su instancia y la inyectará en el SecretMethod.

3. Obtener la implementación deseada de Database

Aún debe decidir, antes de llamar a SecretMethod, qué implementación de la Databaseinterfaz desea (si es MySQL, MSSQL o PostgreSQL). Para esto, puede usar el patrón de diseño 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;
    }
}

La fábrica, como puede ver, sabe qué tipo de base de datos usar desde un archivo de configuración (nuevamente, la Configclase puede ser su propia implementación).

Idealmente, tendrá DatabaseFactorydentro de su contenedor de inyección de dependencia. Su proceso puede verse así.

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

Mira, en qué parte del proceso estás creando un tipo de base de datos específico. No solo eso, no estás creando nada en absoluto. Está llamando a un GetDatabasemétodo en el DatabaseFactoryobjeto almacenado dentro de su contenedor de inyección de dependencia (la _divariable), un método, que le devolverá la instancia correcta de la Databaseinterfaz, según su configuración.

Si, después de 3 semanas de usar PostgreSQL, desea volver a MySQL, abre un único archivo de configuración y cambia el valor del DatabaseDrivercampo de DatabaseEnum.PostgreSQLa DatabaseEnum.MySQL. Y ya terminaste. De repente, el resto de su aplicación utiliza correctamente MySQL nuevamente, cambiando una sola línea.


Si aún no está sorprendido, le recomiendo que se sumerja un poco más en IoC. Cómo puede tomar ciertas decisiones no desde una configuración, sino desde una entrada del usuario. Este enfoque se llama patrón de estrategia y, aunque puede usarse y se usa en aplicaciones empresariales, se usa con mucha más frecuencia cuando se desarrollan juegos de computadora.

Andy
fuente
Me encanta tu respuesta, David. Pero como todas esas respuestas, no se describe cómo se podría poner en práctica. El verdadero problema no es abstraer la capacidad de llamar a diferentes motores de bases de datos, el problema es la sintaxis SQL real. Toma tu DbQueryobjeto, por ejemplo. Suponiendo que ese objeto contenía un miembro para que se ejecutara una cadena de consulta SQL, ¿cómo podría uno hacer eso genérico?
DonBoitnott
1
@DonBoitnott No creo que alguna vez necesites que todo sea genérico. Por lo general, desea introducir la abstracción entre las capas de la aplicación (dominio, servicios, persistencia), también puede presentar la abstracción para los módulos, puede presentar la abstracción en una biblioteca pequeña pero reutilizable y altamente personalizable que está desarrollando para un proyecto más grande, etc. Podrías simplemente abstraer todo a las interfaces, pero eso rara vez es necesario. Es realmente difícil dar una respuesta para todos, porque, lamentablemente, realmente no hay una y proviene de los requisitos.
Andy
2
Entendido. Pero realmente quise decir eso literalmente. Una vez que tenga su clase abstraída, y llegue al punto al que desea llamar, _secret.SecretMethod(database);¿cómo se puede conciliar todo ese trabajo con el hecho de que ahora SecretMethodtodavía tengo que saber con qué DB estoy trabajando para usar el dialecto SQL adecuado? ? Has trabajado muy duro para mantener la mayoría del código ignorante de ese hecho, pero luego a la hora 11, nuevamente debes saber. Estoy en esta situación ahora y estoy tratando de descubrir cómo otros han resuelto este problema.
DonBoitnott
@DonBoitnott No sabía lo que querías decir, ahora lo veo. Podría usar una interfaz en lugar de implementaciones concretas de la DbQueryclase, proporcionar implementaciones de dicha interfaz y usar esa en su lugar, teniendo una fábrica para construir la IDbQueryinstancia. No creo que necesite un tipo genérico para la DatabaseResultclase, siempre puede esperar que los resultados de una base de datos tengan un formato similar. La cuestión aquí es, cuando se trata de bases de datos y SQL sin formato, ya está en un nivel tan bajo en su aplicación (detrás de DAL y repositorios), que no hay necesidad de ...
Andy
... enfoque genérico más.
Andy