A resposta de Caleb, enquanto ele está no caminho certo, está realmente errado. Sua Foo
turma 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 SecretMethod
apenas 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 MyMySQLDatabase
que pode ser construída usando a new
palavra - 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 MyMySQLDatabase
para 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 MyMySQLDatabase
falha, 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 MyMySQLDatabase
objeto nunca pode ser criado. Como o objeto SecretMethod
espera um MyMySQLDatabase
objeto 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 SecretMethod
usa um MyMySQLDatabase
objeto. 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 BeginTransaction
e CommitTransaction
métodos na database
variável passada como parâmetro, para criar uma nova classe MyMSSQLDatabase
, que também terá os métodos BeginTransaction
e CommitTransaction
.
Então vá em frente e altere a declaração de SecretMethod
para o seguinte.
public void SecretMethod(MyMSSQLDatabase database)
{
// use the database here
}
E como as classes MyMSSQLDatabase
e MyMySQLDatabase
têm os mesmos métodos, você não precisa alterar mais nada e ainda funcionará.
Oh espere!
Você tem uma Database
interface, que MyMySQLDatabase
implementa, também possui a MyMSSQLDatabase
classe, que possui exatamente os mesmos métodos que MyMySQLDatabase
, talvez o driver MSSQL também possa implementar a Database
interface, portanto, você a adiciona à definição.
public class MyMSSQLDatabase : Database { }
Mas e se eu, no futuro, não quiser MyMSSQLDatabase
mais 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 MyMSSQLDatabase
e MyMySQLDatabase
têm os mesmos métodos e ambos implementar a Database
interface. Então você refatora SecretMethod
para ficar assim.
public void SecretMethod(Database database)
{
// use the database here
}
Observe como você SecretMethod
nã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 SecretMethod
nada. Você criará MyPostgreSQLDatabase
, implementará a Database
interface 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 Database
interface 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 Config
classe pode ser sua própria implementação).
Idealmente, você terá o DatabaseFactory
interior 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 GetDatabase
método no DatabaseFactory
objeto armazenado no contêiner de injeção de dependência (a _di
variável), um método que retornará a instância correta da Database
interface, 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 DatabaseDriver
campo de DatabaseEnum.PostgreSQL
para 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.
DbQuery
objeto, 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?_secret.SecretMethod(database);
como alguém reconcilia tudo isso com o fato de que agoraSecretMethod
ainda 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.DbQuery
classe, fornecer implementações da referida interface e usá-la, tendo uma fábrica para construir aIDbQuery
instância. Eu não acho que você precisaria de um tipo genérico para aDatabaseResult
classe; 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 ...