¿Cómo especificar una condición previa (LSP) en una interfaz en C #?

11

Digamos que tenemos la siguiente interfaz:

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

La condición previa es que ConnectionString debe establecerse / inicializarse antes de poder ejecutar cualquiera de los métodos.

Esta condición previa se puede lograr de alguna manera al pasar un connectionString a través de un constructor si IDatabase fuera una clase abstracta u 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
}

Alternativamente, podemos crear connectionString un parámetro para cada método, pero se ve peor que simplemente crear una clase abstracta:

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

Preguntas

  1. ¿Hay alguna manera de especificar esta condición previa dentro de la interfaz misma? Es un "contrato" válido, así que me pregunto si hay una característica o patrón de lenguaje para esto (la solución de clase abstracta es más un truco, además de la necesidad de crear dos tipos - una interfaz y una clase abstracta - cada vez esto es necesario)
  2. Esto es más una curiosidad teórica: ¿esta condición previa cae realmente en la definición de una condición previa como en el contexto de LSP?
Aquiles
fuente
2
¿Por "LSP" ustedes están hablando del principio de sustitución de Liskov? ¿El principio de "si es un graznido como un pato pero necesita baterías no es un pato"? Porque, como lo veo, es más una violación del ISP y el SRP, tal vez incluso el OCP, pero no el LSP.
Sebastien
2
Para que lo sepas, todo este concepto de "ConnectionString debe establecerse / inicializarse antes de que se pueda ejecutar cualquiera de los métodos" es un ejemplo de acoplamiento temporal blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling y debe evitarse, si posible.
Richiban
Seemann es realmente un gran admirador de Abstract Factory.
Adrian Iftode

Respuestas:

10
  1. Si. Desde .Net 4.0 en adelante, Microsoft proporciona contratos de código . Estos se pueden utilizar para definir condiciones previas en el formulario Contract.Requires( ConnectionString != null );. Sin embargo, para que esto funcione para una interfaz, aún necesitará una clase auxiliar IDatabaseContract, que se adjuntará IDatabase, y la condición previa debe definirse para cada método individual de su interfaz donde se mantendrá. Vea aquí un amplio ejemplo de interfaces.

  2. , el LSP trata con partes sintácticas y semánticas de un contrato.

Doc Brown
fuente
No pensé que pudieras usar Code Contracts en una interfaz. El ejemplo que proporciona muestra que se usan en clases. Las clases se ajustan a una interfaz, pero la interfaz en sí no contiene información de Contrato de Código (una pena, realmente. Ese sería el lugar ideal para ponerla).
Robert Harvey
1
@RobertHarvey: sí, tienes razón. Técnicamente, necesita una segunda clase, por supuesto, pero una vez definido, el contrato funciona automáticamente para cada implementación de la interfaz.
Doc Brown
21

Conectarse y consultar son dos preocupaciones separadas. Como tal, deben tener dos interfaces separadas.

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

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

Esto garantiza que IDatabasese conectará cuando se use y hace que el cliente no dependa de la interfaz que no necesita.

Eufórico
fuente
Podría ser más explícito sobre "este es un patrón de imposición de las condiciones previas a través de los tipos"
Caleth
@Caleth: este no es un "patrón general de precondiciones de cumplimiento" Esta es una solución para este requisito específico de garantizar que la conexión se realice antes que nada. Otras condiciones previas necesitarán diferentes soluciones (como la que mencioné en mi respuesta). Me gustaría agregar para este requisito, claramente preferiría la sugerencia de Euphoric sobre la mía, porque es mucho más simple y no necesita ningún componente adicional de terceros.
Doc Brown
El requisito específico de que algo suceda antes que otra cosa es ampliamente aplicable. También creo que su respuesta se ajusta mejor a esta pregunta , pero esta respuesta se puede mejorar
Caleth
1
Esta respuesta pierde completamente el punto. La IDatabaseinterfaz define un objeto capaz de establecer una conexión a una base de datos y luego ejecutar consultas arbitrarias. Que es el objeto que actúa como el límite entre la base de datos y el resto del código. Como tal, este objeto tiene que mantener un estado (como una transacción) que pueda afectar el comportamiento de las consultas. Ponerlos en la misma clase es muy práctico.
jpmc26
44
@ jpmc26 Ninguna de sus objeciones tiene sentido, ya que el estado se puede mantener dentro de la clase que implementa IDatabase. También puede hacer referencia a la clase principal que la creó, obteniendo así acceso a todo el estado de la base de datos.
Eufórico
5

Retrocedamos un paso y miremos la imagen más grande aquí.

¿Cuál es IDatabasela responsabilidad?

Tiene algunas operaciones diferentes:

  • Analizar una cadena de conexión
  • Abrir una conexión con una base de datos (un sistema externo)
  • Enviar mensajes a la base de datos; los mensajes ordenan a la base de datos que altere su estado
  • Reciba respuestas de la base de datos y transfórmelas en un formato que la persona que llama pueda usar
  • Cerrar la conexión

Al mirar esta lista, podría estar pensando, "¿No viola esto SRP?" Pero no creo que lo haga. Todas las operaciones son parte de un concepto único y coherente: administrar una conexión con estado a la base de datos (un sistema externo) . Establece la conexión, realiza un seguimiento del estado actual de la conexión (en relación con las operaciones realizadas en otras conexiones, en particular), señala cuándo confirmar el estado actual de la conexión, etc. En este sentido, actúa como una API que oculta muchos detalles de implementación que a la mayoría de las personas que llaman no les importa Por ejemplo, ¿utiliza HTTP, sockets, tuberías, TCP personalizado, HTTPS? Al código de llamada no le importa; solo quiere enviar mensajes y obtener respuestas. Este es un buen ejemplo de encapsulación.

¿Estamos seguros? ¿No podríamos dividir algunas de estas operaciones? Quizás, pero no hay beneficio. Si intenta dividirlos, aún necesitará un objeto central que mantenga la conexión abierta y / o administre cuál es el estado actual. Todas las demás operaciones están fuertemente acopladas al mismo estado, y si intenta separarlas, terminarán delegando de nuevo en el objeto de conexión de todos modos. Estas operaciones están acopladas natural y lógicamente al estado, y no hay forma de separarlas. El desacoplamiento es excelente cuando podemos hacerlo, pero en este caso, en realidad no podemos. Al menos no sin un protocolo sin estado muy diferente para hablar con el DB, y eso en realidad haría que problemas muy importantes como el cumplimiento de ACID sean mucho más difíciles. Además, en el proceso de intentar desacoplar estas operaciones de la conexión, se verá obligado a exponer detalles sobre el protocolo que a las personas que llaman no les importa, ya que necesitará una forma de enviar algún tipo de mensaje "arbitrario" a la base de datos.

Tenga en cuenta que el hecho de que estemos tratando con un protocolo con estado descarta bastante sólidamente su última alternativa (pasar la cadena de conexión como parámetro).

¿Realmente necesitamos establecer una cadena de conexión?

Si. No puede abrir la conexión hasta que tenga una cadena de conexión, y no puede hacer nada con el protocolo hasta que abra la conexión. Por lo tanto, no tiene sentido tener un objeto de conexión sin uno.

¿Cómo resolvemos el problema de requerir la cadena de conexión?

El problema que estamos tratando de resolver es que queremos que el objeto esté en un estado utilizable en todo momento. ¿Qué tipo de entidad se usa para administrar el estado en los idiomas OO? Objetos , no interfaces. Las interfaces no tienen estado para administrar. Debido a que el problema que está tratando de resolver es un problema de administración de estado, una interfaz no es realmente apropiada aquí. Una clase abstracta es mucho más natural. Entonces usa una clase abstracta con un constructor.

También puede considerar abrir realmente la conexión durante el constructor, ya que la conexión también es inútil antes de abrirse. Eso requeriría un protected Openmétodo abstracto ya que el proceso de abrir una conexión puede ser específico de la base de datos. También sería una buena idea hacer que la ConnectionStringpropiedad sea de solo lectura en este caso, ya que cambiar la cadena de conexión después de que la conexión esté abierta no tendría sentido. (Honestamente, lo haría leer de todos modos. Si quieres una conexión con una cadena diferente, crea otro objeto).

¿Necesitamos alguna interfaz?

Puede ser útil una interfaz que especifique los mensajes disponibles que puede enviar a través de la conexión y los tipos de respuestas que puede recibir. Esto nos permitiría escribir código que ejecute estas operaciones pero que no esté acoplado a la lógica de abrir una conexión. Pero ese es el punto: administrar la conexión no es parte de la interfaz de "¿Qué mensajes puedo enviar y qué mensajes puedo volver a / de la base de datos?", Por lo que la cadena de conexión ni siquiera debería ser parte de eso interfaz.

Si seguimos esta ruta, nuestro código podría verse así:

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
fuente
Agradecería que el votante explicara su razón para estar en desacuerdo.
jpmc26
De acuerdo, re: downvoter. Esta es la solución correcta. La cadena de conexión debe proporcionarse en el constructor a la clase concreta / abstracta. El desordenado negocio de abrir / cerrar una conexión no es una preocupación del código que usa este objeto, y debe permanecer interno a la clase misma. Yo diría que el Openmétodo debería ser privatey usted debería exponer una Connectionpropiedad protegida que crea la conexión y se conecta. O exponer un OpenConnectionmétodo protegido .
Greg Burghardt
Esta solución es bastante elegante y muy bien diseñada. Pero creo que parte del razonamiento detrás de las decisiones de diseño es incorrecto. Principalmente en los primeros párrafos sobre el SRP. Viola el SRP incluso como se explica en "¿Cuál es la responsabilidad de IDatabase?". Las responsabilidades como se ve para el SRP no son solo cosas que una clase hace o gestiona. También son "actores" o "razones para cambiar". Y creo que viola el SRP porque "Recibir respuestas de la base de datos y transformarlas en un formato que la persona que llama pueda usar" tiene una razón muy diferente para cambiar que "Analizar una cadena de conexión".
Sebastien
Aún así voté esto.
Sebastien
1
Y por cierto, SOLIDOS no son el evangelio. Seguro que son muy importantes a tener en cuenta al diseñar una solución. Pero PUEDES violarlos si sabes POR QUÉ lo haces, CÓMO afectará tu solución y CÓMO arreglar las cosas con la refactorización si te mete en problemas. Por lo tanto, creo que incluso si la solución mencionada anteriormente viola el SRP, es la mejor hasta ahora.
Sebastien
0

Realmente no veo la razón para tener una interfaz aquí. Su clase de base de datos es específica de SQL, y realmente le brinda una manera conveniente / segura de asegurarse de que no está consultando en una conexión que no se abre correctamente. Sin embargo, si insiste en una interfaz, así es como lo haría.

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

El uso podría verse así:

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
fuente