Como especificar uma pré-condição (LSP) em uma interface em C #?

11

Digamos que temos a seguinte interface -

interface IDatabase { 
    string ConnectionString{get;set;}
    void ExecuteNoQuery(string sql);
    void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

A pré-condição é que ConnectionString deve ser definido / inicializado antes que qualquer um dos métodos possa ser executado.

Essa pré-condição pode ser um pouco alcançada passando uma connectionString por meio de um construtor se o IDatabase fosse uma classe abstrata ou concreta -

abstract class Database { 
    public string ConnectionString{get;set;}
    public Database(string connectionString){ ConnectionString = connectionString;}

    public void ExecuteNoQuery(string sql);
    public void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

Como alternativa, podemos criar um parâmetro connectionString para cada método, mas parece pior do que apenas criar uma classe abstrata -

interface IDatabase { 
    void ExecuteNoQuery(string connectionString, string sql);
    void ExecuteNoQuery(string connectionString, string[] sql);
    //Various other methods all with the connectionString parameter
}

Questões -

  1. Existe uma maneira de especificar essa pré-condição na própria interface? É um "contrato" válido, então estou me perguntando se existe um recurso ou padrão de linguagem para isso (a solução de classe abstrata é mais uma imitação de hack, além da necessidade de criar dois tipos - uma interface e uma classe abstrata - sempre isso é necessário)
  2. Isso é mais uma curiosidade teórica - essa pré-condição realmente se enquadra na definição de uma pré-condição como no contexto do LSP?
Aquiles
fonte
2
Por "LSP" vocês estão falando sobre o princípio de substituição de Liskov? O princípio "se é charlatão como um pato, mas precisa de baterias, não é um pato"? Porque, a meu ver, é mais uma violação do ISP e do SRP, talvez até do OCP, mas não do LSP.
Sebastien
2
Para que você saiba, todo esse conceito de "ConnectionString deve ser definido / inicializado antes que qualquer um dos métodos possa ser executado" é um exemplo de acoplamento temporal blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling e deve ser evitado, se possível.
Richiban
Seemann é realmente um grande fã da Abstract Factory.
Adrian Iftode

Respostas:

10
  1. Sim. A partir do .Net 4.0, a Microsoft fornece contratos de código . Eles podem ser usados ​​para definir pré-condições no formulário Contract.Requires( ConnectionString != null );. No entanto, para fazer isso funcionar para uma interface, você ainda precisará de uma classe auxiliar IDatabaseContract, a qual é anexada IDatabase, e a pré-condição precisa ser definida para cada método individual da sua interface onde ela deve se manter. Veja aqui um exemplo abrangente de interfaces.

  2. Sim , o LSP lida com as partes sintática e semântica de um contrato.

Doc Brown
fonte
Não achei que você pudesse usar contratos de código em uma interface. O exemplo que você fornece mostra que eles estão sendo usados ​​nas classes. As classes estão em conformidade com uma interface, mas a interface em si não contém informações de Contrato de Código (uma pena, na verdade. Esse seria o local ideal para colocá-lo).
Robert Harvey
1
@RobertHarvey: sim, você está certo. Tecnicamente, você precisa de uma segunda classe, é claro, mas uma vez definido, o contrato funcionará automaticamente para todas as implementações da interface.
Doc Brown
21

Conectar e consultar são duas preocupações distintas. Como tal, eles devem ter duas interfaces separadas.

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

interface IDatabase
{
    public void ExecuteNoQuery(string sql);
    public void ExecuteNoQuery(string[] sql);
}

Isso garante que o dispositivo IDatabaseserá conectado quando usado e faz com que o cliente não dependa da interface de que não precisa.

Eufórico
fonte
Poderia ser mais explícito sobre "este é um padrão de fazer cumprir os pré-requisitos através de tipos"
Caleth
@ Caleth: este não é um "padrão geral de imposição de pré-condições". Esta é uma solução para esse requisito específico de garantir que a conexão ocorra antes de qualquer outra coisa. Outras pré-condições precisarão de soluções diferentes (como a que mencionei na minha resposta). Eu gostaria de acrescentar a esse requisito, claramente preferiria a sugestão do Euphoric à minha, porque é muito mais simples e não precisa de nenhum componente adicional de terceiros.
Doc Brown
A exigência específica de que algo acontece antes de algo é amplamente aplicável. Eu também acho que sua resposta melhor se adapta a esta pergunta , mas esta resposta pode ser melhorada
Caleth
1
Esta resposta está completamente errada. A IDatabaseinterface define um objeto capaz de estabelecer uma conexão com um banco de dados e, em seguida, executar consultas arbitrárias. Ele é o objeto que atua como a fronteira entre o banco de dados e o resto do código. Como tal, esse objeto deve manter o estado (como uma transação) que pode afetar o comportamento das consultas. Colocá-los na mesma classe é muito prático.
Jpmc26
4
@ jpmc26 Nenhuma de suas objeções faz sentido, pois o estado pode ser mantido dentro da classe que implementa o IDatabase. Também pode fazer referência à classe pai que a criou, obtendo acesso a todo o estado do banco de dados.
Euphoric
5

Vamos dar um passo atrás e ver a foto maior aqui.

Qual é IDatabasea responsabilidade?

Possui algumas operações diferentes:

  • Analisar uma cadeia de conexão
  • Abrir uma conexão com um banco de dados (um sistema externo)
  • Envie mensagens para o banco de dados; as mensagens comandam o banco de dados para alterar seu estado
  • Receba respostas do banco de dados e transforme-as em um formato que o chamador possa usar
  • Feche a conexão

Olhando para esta lista, você pode estar pensando: "Isso não viola o SRP?" Mas acho que não. Todas as operações fazem parte de um conceito único e coeso: gerenciar uma conexão estável com o banco de dados (um sistema externo) . Estabelece a conexão, mantém o controle do estado atual da conexão (em relação às operações realizadas em outras conexões, em particular), sinaliza quando confirmar o estado atual da conexão, etc. Nesse sentido, atua como uma API que oculta muitos detalhes de implementação com os quais a maioria dos chamadores não se importa. Por exemplo, ele usa HTTP, soquetes, tubulações, TCP personalizado, HTTPS? Código de chamada não se importa; ele só quer enviar mensagens e obter respostas. Este é um bom exemplo de encapsulamento.

Temos certeza? Não poderíamos dividir algumas dessas operações? Talvez, mas não há benefício. Se você tentar dividi-los, ainda precisará de um objeto central que mantenha a conexão aberta e / ou gerencie qual é o estado atual. Todas as outras operações estão fortemente acopladas ao mesmo estado e, se você tentar separá-las, elas acabarão delegando de volta ao objeto de conexão de qualquer maneira. Essas operações são natural e logicamente acopladas ao estado, e não há como separá-las. A dissociação é excelente quando podemos fazê-lo, mas, neste caso, na verdade não podemos. Pelo menos não sem um protocolo muito diferente e sem estado para conversar com o banco de dados, e isso realmente dificultaria muito os problemas muito importantes, como a conformidade com ACID. Além disso, no processo de tentar desacoplar essas operações da conexão, você será forçado a expor detalhes sobre o protocolo de que os chamadores não se importam, pois você precisará de uma maneira de enviar algum tipo de mensagem "arbitrária" para o banco de dados.

Observe que o fato de estarmos lidando com um protocolo estável exclui bastante sua última alternativa (passar a cadeia de conexão como um parâmetro).

Realmente precisamos que a cadeia de conexão seja definida?

Sim. Você não pode abrir a conexão até ter uma string de conexão e não pode fazer nada com o protocolo até abrir a conexão. Portanto, é inútil ter um objeto de conexão sem um.

Como resolvemos o problema de exigir a cadeia de conexão?

O problema que estamos tentando resolver é que queremos que o objeto esteja em um estado utilizável o tempo todo. Que tipo de entidade é usada para gerenciar o estado nos idiomas OO? Objetos , não interfaces. As interfaces não têm estado para gerenciar. Como o problema que você está tentando resolver é um problema de gerenciamento de estado, uma interface não é realmente apropriada aqui. Uma classe abstrata é muito mais natural. Portanto, use uma classe abstrata com um construtor.

Você também pode considerar abrir a conexão durante o construtor, já que a conexão também é inútil antes de ser aberta. Isso exigiria um protected Openmétodo abstrato , pois o processo de abertura de uma conexão pode ser específico do banco de dados. Também seria uma boa idéia fazer com que a ConnectionStringpropriedade fosse lida apenas nesse caso, pois alterar a cadeia de conexão após a conexão ser aberta não faria sentido. (Honestamente, eu faria apenas leitura de qualquer maneira. Se você quiser uma conexão com uma string diferente, crie outro objeto.)

Precisamos de uma interface?

Uma interface que especifica as mensagens disponíveis que você pode enviar pela conexão e os tipos de respostas que você pode receber de volta podem ser úteis. Isso nos permitiria escrever código que executa essas operações, mas não está acoplado à lógica de abrir uma conexão. Mas esse é o ponto: gerenciar a conexão não faz parte da interface de "Que mensagens posso enviar e quais mensagens posso voltar para / do banco de dados?", Portanto, a cadeia de conexão nem deve fazer parte disso. interface.

Se seguirmos essa rota, nosso código poderá ser algo assim:

interface IDatabase {
    void ExecuteNoQuery(string sql);
    void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

abstract class ConnectionStringDatabase : IDatabase { 

    public string ConnectionString { get; }

    public Database(string connectionString) {
        this.ConnectionString = connectionString;
        this.Open();
    }

    protected abstract void Open();

    public abstract void ExecuteNoQuery(string sql);
    public abstract void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}
jpmc26
fonte
Apreciaria se o downvoter explicasse sua razão de discordar.
precisa saber é
Concordado, re: downvoter. Esta é a solução correta. A cadeia de conexão deve ser fornecida no construtor para a classe concreta / abstrata. O negócio bagunçado de abrir / fechar uma conexão não é uma preocupação do código que usa esse objeto e deve permanecer interno à própria classe. Eu argumentaria que o Openmétodo deveria ser privatee você deveria expor uma Connectionpropriedade protegida que cria a conexão e se conecta. Ou exponha um OpenConnectionmétodo protegido .
Greg Burghardt
Esta solução é bastante elegante e muito bem projetada. Mas acho que alguns dos motivos por trás das decisões de design estão errados. Principalmente nos primeiros parágrafos sobre o SRP. Ele viola o SRP, mesmo como explicado em "Qual é a responsabilidade do IDatabase?". As responsabilidades vistas pelo SRP não são apenas coisas que uma classe faz ou gerencia. É também "atores" ou "razões para mudar". E acho que viola o SRP porque "Receba respostas do banco de dados e as transforme em um formato que o chamador possa usar" tem um motivo muito diferente para mudar do que "Analisar uma cadeia de conexão".
Sebastien
Ainda voto isso.
Sebastien
1
E, BTW, o SOLID não é o evangelho. Claro que eles são muito importantes para se ter em mente ao projetar uma solução. Mas você pode violá-los se você sabe por que faz isso, como isso afetará sua solução e como consertar as coisas com a refatoração, se isso causar problemas. Portanto, acho que mesmo que a solução mencionada acima viole o SRP, é a melhor ainda.
Sebastien
0

Realmente não vejo o motivo de ter uma interface aqui. Sua classe de banco de dados é específica do SQL e realmente oferece uma maneira conveniente / segura de garantir que você não esteja consultando uma conexão que não foi aberta corretamente. Se você insistir em uma interface, aqui está como eu faria isso.

public interface IDatabase : IDisposable
{
    string ConnectionString { get; }
    void ExecuteNoQuery(string sql);
    void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

public class SqlDatabase : IDatabase
{
    public string ConnectionString { get; }
    SqlConnection sqlConnection;
    SqlTransaction sqlTransaction; // optional

    public SqlDatabase(string connectionStr)
    {
        if (String.IsNullOrEmpty(connectionStr)) throw new ArgumentException("connectionStr empty");
        ConnectionString = connectionStr;
        instantiateSqlProps();
    }

    private void instantiateSqlProps()
    {
        sqlConnection.Open();
        sqlTransaction = sqlConnection.BeginTransaction();
    }

    public void ExecuteNoQuery(string sql) { /*run query*/ }
    public void ExecuteNoQuery(string[] sql) { /*run query*/ }

    public void Dispose()
    {
        sqlTransaction.Commit();
        sqlConnection.Dispose();
    }

    public void Commit()
    {
        Dispose();
        instantiateSqlProps();
    }
}

O uso pode ser assim:

using (IDatabase dbase = new SqlDatabase("Data Source = servername; Initial Catalog = MyDb; Integrated Security = True"))
{
    dbase.ExecuteNoQuery("delete from dbo.Invoices");
    dbase.ExecuteNoQuery("delete from dbo.Customers");
}
Graham
fonte