La respuesta de Caleb, mientras está en el camino correcto, es realmente incorrecta. Su Foo
clase 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 new
palabra 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 MyMySQLDatabase
a una clase diferente, por ejemplo MyPostgreSQLDatabase
, tendría que cambiarlo en sus 30 métodos.
Otro problema es que, si MyMySQLDatabase
falla 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 MyMySQLDatabase
pasá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 MyMySQLDatabase
objeto nunca podría ser creado. Debido a que SecretMethod
espera un MyMySQLDatabase
objeto 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 SecretMethod
usa un MyMySQLDatabase
objeto. 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 BeginTransaction
y CommitTransaction
métodos en la database
variable pasada como parámetro, por lo que crea una nueva clase MyMSSQLDatabase
, que también tendrá los métodos BeginTransaction
y CommitTransaction
.
Luego continúe y cambie la declaración de SecretMethod
a lo siguiente.
public void SecretMethod(MyMSSQLDatabase database)
{
// use the database here
}
Y debido a que las clases MyMSSQLDatabase
y MyMySQLDatabase
tienen los mismos métodos, no necesita cambiar nada más y seguirá funcionando.
¡Oh espera!
Tiene una Database
interfaz, que MyMySQLDatabase
implementa, también tiene la MyMSSQLDatabase
clase, que tiene exactamente los mismos métodos que MyMySQLDatabase
, quizás el controlador MSSQL también podría implementar la Database
interfaz, por lo que debe agregarla a la definición.
public class MyMSSQLDatabase : Database { }
Pero, ¿qué MyMSSQLDatabase
pasa 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 MyMSSQLDatabase
y MyMySQLDatabase
tenemos los mismos métodos y ambos implementamos la Database
interfaz. Así que refactorizas SecretMethod
para que se vea así.
public void SecretMethod(Database database)
{
// use the database here
}
Observe cómo SecretMethod
ya 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 SecretMethod
en absoluto. Hará un MyPostgreSQLDatabase
, hará que implemente la Database
interfaz 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 Database
interfaz 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 Config
clase puede ser su propia implementación).
Idealmente, tendrá DatabaseFactory
dentro 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 GetDatabase
método en el DatabaseFactory
objeto almacenado dentro de su contenedor de inyección de dependencia (la _di
variable), un método, que le devolverá la instancia correcta de la Database
interfaz, 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 DatabaseDriver
campo de DatabaseEnum.PostgreSQL
a 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.
DbQuery
objeto, 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?_secret.SecretMethod(database);
¿cómo se puede conciliar todo ese trabajo con el hecho de que ahoraSecretMethod
todaví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.DbQuery
clase, proporcionar implementaciones de dicha interfaz y usar esa en su lugar, teniendo una fábrica para construir laIDbQuery
instancia. No creo que necesite un tipo genérico para laDatabaseResult
clase, 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 ...