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.
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?