C #의 인터페이스에서 전제 조건 (LSP)을 지정하는 방법은 무엇입니까?


11

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

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의 맥락에서와 같이 전제 조건의 정의에 속합니까?

2
"LSP"로 여러분은 Liskov 대체 원리에 대해 이야기하고 있습니까? "오리처럼 ck하지만 오리가 아닌 배터리가 필요한 경우"원칙? 내가 알기로는 ISP와 SRP에 대한 위반이 OCP 일 수도 있지만 실제로 LSP는 아닙니다.
Sebastien

2
아시다시피, "ConnectionString은 메소드를 실행하기 전에 설정 / 초기화해야합니다"라는 전체 개념은 시간 결합 블로그 의 한 예이며 , 다음과 같은 경우 피해야합니다. 가능한.
Richiban

Seemann은 실제로 Abstract Factory의 큰 팬입니다.
Adrian Iftode

답변:


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

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


인터페이스에서 코드 계약을 사용할 수 있다고 생각하지 않았습니다. 제공하는 예제는 클래스 에서 사용되는 것을 보여줍니다 . 클래스는 인터페이스를 따르지만 인터페이스 자체에는 코드 계약 정보가 포함되어 있지 않습니다.
Robert Harvey

1
@RobertHarvey : 그렇습니다. 기술적으로, 물론 두 번째 클래스가 필요하지만 일단 정의되면 계약이 인터페이스의 모든 구현에 대해 자동으로 작동합니다.
Doc Brown

21

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

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

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

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


"이것은 유형을 통해 전제 조건을 시행하는 패턴"에 대해 더 명확 할 수 있습니다.
Caleth

@Caleth : 이것은 "전제 조건을 시행하는 일반적인 패턴"이 아닙니다. 이는 연결이 다른 것보다 먼저 이루어 지도록하는 특정 요구 사항에 대한 솔루션입니다. 다른 전제 조건에는 다른 솔루션이 필요합니다 (내 대답에서 언급 한 것과 같은). 이 요구 사항을 추가하고 싶습니다. Euphoric의 제안은 내 제안보다 훨씬 간단하고 추가 타사 구성 요소가 필요하지 않기 때문에 분명히 선호합니다.
Doc Brown

하는 특정의 requrement 뭔가 하기 전에 일이 뭔가 다른 널리 적용 할 수있다. 또한 귀하의 답변 이이 질문에 더 잘 맞는다고 생각 하지만이 답변을 개선 할 수 있습니다
Caleth

1
이 답변은 요점을 완전히 놓친 것입니다. IDatabase인터페이스가 데이터베이스와의 연결을 설정 한 다음, 임의의 질의를 실행할 수있는 객체를 정의한다. 그것은 이다 데이터베이스와 코드의 나머지 부분 사이의 경계 역할을하는 객체입니다. 따라서이 개체는 쿼리 동작에 영향을 줄 수있는 상태 (예 : 트랜잭션) 를 유지 해야합니다. 같은 수업에 참여시키는 것은 매우 실용적입니다.
jpmc26

4
@ jpmc26 IDatabase를 구현하는 클래스 내에서 상태를 유지할 수 있기 때문에 귀하의 반대 의견은 없습니다. 또한 생성 한 부모 클래스를 참조하여 전체 데이터베이스 상태에 액세스 할 수 있습니다.
Euphoric

5

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

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
}

downvoter가 동의하지 않는 이유를 설명해 주시면 감사하겠습니다.
jpmc26

동의 : 다시 : downvoter. 이것이 올바른 해결책입니다. 연결 문자열은 생성자에서 concrete / abstract 클래스에 제공되어야합니다. 연결을 열고 닫는 지저분한 업무는이 객체를 사용하는 코드의 문제가 아니며 클래스 자체 내부에 있어야합니다. 나는 그 Open방법이 있어야 하고 연결을 만들고 연결 private하는 보호 된 Connection속성을 노출 해야 한다고 주장 합니다. 또는 보호 된 OpenConnection방법을 노출하십시오 .
Greg Burghardt

이 솔루션은 매우 우아하고 매우 잘 설계되었습니다. 그러나 디자인 결정의 원인 중 일부는 잘못되었다고 생각합니다. 주로 SRP에 관한 처음 몇 단락에서. "IDatabase의 책임은 무엇입니까?"에 설명 된대로 SRP를 위반합니다. SRP에 대한 책임은 수업이 수행하거나 관리하는 것이 아닙니다. 또한 "배우"또는 "변경 사유"입니다. 그리고 "데이터베이스에서 응답을 수신하고 호출자가 사용할 수있는 형식으로 변환합니다"는 "연결 문자열 구문 분석"과는 다른 이유가 있기 때문에 SRP를 위반한다고 생각합니다.
Sebastien

여전히 나는 이것을 찬성했다.
Sebastien

1
그리고 BTW, SOLID는 복음이 아닙니다. 물론 솔루션을 설계 할 때 명심해야합니다. 그러나 왜 그렇게하는지 알고 있다면 솔루션에 영향을 미치며 리팩토링으로 문제를 해결하는 방법을 수정하는 방법을 알고 있다면 위반할 수 있습니다. 따라서 위에서 언급 한 솔루션이 SRP를 위반하더라도 이것이 최고의 솔루션이라고 생각합니다.
Sebastien

0

나는 여기에 인터페이스가있는 이유를 정말로 보지 못합니다. 데이터베이스 클래스는 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");
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.