태그 보관물: liskov-substitution

liskov-substitution

C #의 인터페이스에서 전제 조건 (LSP)을 지정하는 방법은 무엇입니까? IDatabase가 추상 클래스이거나 구체적인 클래스 인

다음과 같은 인터페이스가 있다고 가정 해 봅시다.

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

전제 조건은 메소드를 실행하기 전에 ConnectionString을 설정 / 초기화해야한다는 것입니다.

이 전제 조건은 IDatabase가 추상 클래스이거나 구체적인 클래스 인 경우 생성자를 통해 connectionString을 전달하여 어느 정도 달성 ​​할 수 있습니다.

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
}

또는 각 메소드에 대한 매개 변수 인 connectionString을 작성할 수 있지만 추상 클래스를 작성하는 것보다 나빠 보입니다.

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

질문-

  1. 인터페이스 자체 내에서이 전제 조건을 지정하는 방법이 있습니까? 유효한 “계약”이므로 언어 ​​기능이나 패턴이 있는지 궁금합니다. 추상 클래스 솔루션은 매번 두 가지 유형 (인터페이스와 추상 클래스)을 작성해야 할 필요가 있습니다. 이것은 필요합니다)
  2. 이것은 이론적 인 호기심에 가깝습니다.이 전제 조건이 실제로 LSP의 맥락에서와 같이 전제 조건의 정의에 속합니까?


답변

  1. 예. .Net 4.0 이상에서 Microsoft는 코드 계약을 제공합니다 . 이것은 양식에서 전제 조건을 정의하는 데 사용될 수 있습니다 Contract.Requires( ConnectionString != null );. 그러나 인터페이스에서이 작업을 수행하려면에 IDatabaseContract연결된 클래스가 계속 필요하며 IDatabase인터페이스의 모든 개별 메소드에 대해 사전 조건을 정의해야합니다. 인터페이스에 대한 광범위한 예는 여기참조하십시오 .

  2. , LSP는 계약의 구문 및 의미 부분을 모두 처리합니다.


답변

연결과 쿼리는 별개의 두 가지 관심사입니다. 따라서 두 개의 별도 인터페이스가 있어야합니다.

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

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

이것은 둘 다 IDatabase사용될 때 연결되고 클라이언트가 필요없는 인터페이스에 의존하지 않도록합니다.


답변

한 걸음 물러서서 더 큰 그림을 보자.

IDatabase책임 은 무엇입니까 ?

몇 가지 다른 작업이 있습니다.

  • 연결 문자열 구문 분석
  • 데이터베이스 (외부 시스템)와의 연결을 엽니 다
  • 데이터베이스에 메시지를 보냅니다. 메시지는 데이터베이스에 상태를 변경하도록 명령합니다.
  • 데이터베이스에서 응답을 수신하여 발신자가 사용할 수있는 형식으로 변환
  • 연결을 닫습니다

이 목록을 보면 “이것이 SRP를 위반하지 않습니까?” 그러나 나는 그렇게 생각하지 않습니다. 모든 작업은 단일 통합 개념의 일부입니다. 즉 , 데이터베이스 (외부 시스템)에 대한 상태 저장 연결을 관리합니다 . 연결을 설정하고 (특히 다른 연결에서 수행 된 작업과 관련하여) 연결의 현재 상태를 추적하고 연결의 현재 상태를 커밋 할시기를 알립니다. 이러한 의미에서 API 역할을합니다. 대부분의 발신자가 신경 쓰지 않는 많은 구현 세부 사항을 숨 깁니다. 예를 들어 HTTP, 소켓, 파이프, 사용자 지정 TCP, HTTPS를 사용합니까? 호출 코드는 중요하지 않습니다. 단지 메시지를 보내고 응답을 받기를 원합니다. 이것은 캡슐화의 좋은 예입니다.

확실합니까? 이러한 작업 중 일부를 분리 할 수 ​​없습니까? 어쩌면 이점은 없습니다. 그것들을 분리하려고하면 연결을 계속 유지하거나 현재 상태를 관리하는 중앙 객체가 여전히 필요합니다. 다른 모든 작업은 동일한 상태에 강력하게 연결되어 있으며 분리하려고하면 어쨌든 연결 개체로 다시 위임됩니다. 이러한 작업은 자연스럽고 논리적으로 상태 와 연결되어 있으며 분리 할 방법이 없습니다. 디커플링은 우리가 할 수있을 때 훌륭하지만,이 경우 실제로 . 적어도 DB와 통신하는 매우 다른 상태 비 저장 프로토콜이 없으면 ACID 준수와 같은 매우 중요한 문제가 실제로 훨씬 더 어려워집니다. 또한 이러한 작업을 연결에서 분리하려고 시도하는 동안 일종의 “임의”메시지를 보내는 방법이 필요하므로 호출자가 신경 쓰지 않는 프로토콜에 대한 세부 정보를 노출해야합니다. 데이터베이스에.

우리가 상태 저장 프로토콜을 다루고 있다는 사실은 마지막 대안 (연결 문자열을 매개 변수로 전달)을 거의 확실하게 배제합니다.

연결 문자열을 설정해야합니까?

예. 당신은 할 수없는 이 연결 문자열을 때까지 연결하고 연결을 열 때까지 당신이 프로토콜을 사용하여 아무것도 할 수 없습니다. 그건 그래서 무의미 하나없이 연결 개체를 가지고.

연결 문자열이 필요한 문제를 어떻게 해결합니까?

우리가 해결하려는 문제는 객체가 항상 사용 가능한 상태가되기를 원한다는 것입니다. OO 언어로 상태를 관리하는 데 어떤 종류의 엔티티가 사용됩니까? 인터페이스가 아닌 객체 . 인터페이스는 관리 할 상태가 없습니다. 해결하려는 문제는 상태 관리 문제이므로 인터페이스가 실제로 적합하지 않습니다. 추상 클래스는 훨씬 더 자연 스럽습니다. 따라서 생성자와 함께 추상 클래스를 사용하십시오.

연결을 열기 전에는 쓸모가 없기 때문에 생성자 동안 실제로 연결을 여는 것도 고려할 수 있습니다 . protected Open연결을 여는 프로세스는 데이터베이스마다 다를 수 있으므로 추상적 인 방법 이 필요합니다 . ConnectionString연결이 열린 후 연결 문자열을 변경하는 것은 의미가 없으므로이 경우 속성을 읽기 전용 으로 설정하는 것이 좋습니다 . (정직하게, 나는 어쨌든 읽기 전용으로 만들 것입니다. 다른 문자열로 연결하려면 다른 객체를 만드십시오.)

우리는 전혀 인터페이스가 필요합니까?

연결을 통해 보낼 수있는 사용 가능한 메시지와 다시받을 수있는 응답 유형을 지정하는 인터페이스가 유용 할 수 있습니다. 이를 통해 이러한 작업을 실행하는 코드를 작성할 수 있지만 연결을 여는 논리와는 관련이 없습니다. 그러나 요점은 연결 관리가 “어떤 메시지를 보낼 수 있고 어떤 메시지를 데이터베이스와주고받을 수 있는가?”라는 인터페이스의 일부가 아니기 때문에 연결 문자열이 그 일부가되어서는 안된다는 것입니다. 상호 작용.

이 경로를 사용하면 코드가 다음과 같이 보일 수 있습니다.

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
}


답변

나는 여기에 인터페이스가있는 이유를 정말로 보지 못합니다. 데이터베이스 클래스는 SQL에 따라 다르며, 제대로 열리지 않은 연결에서 쿼리하지 않도록하는 편리하고 안전한 방법을 제공합니다. 당신이 인터페이스를 고집한다면, 여기 내가 어떻게 할 것입니다.

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

사용법은 다음과 같습니다.

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


답변