다중 데이터베이스 유형을 지원하기 위해 추상 데이터베이스 인터페이스는 어떻게 작성됩니까?


12

MySQL, SQLLite, MSSQL 등과 같은 여러 유형의 데이터베이스와 인터페이스 할 수있는 더 큰 애플리케이션에서 추상 클래스를 설계하기 시작하는 방법은 무엇입니까?

이 디자인 패턴은 무엇이며 어디서 시작합니까?

다음과 같은 메소드가있는 클래스를 작성해야한다고 가정 해 봅시다.

public class Database {
   public DatabaseType databaseType;
   public Database (DatabaseType databaseType){
      this.databaseType = databaseType;
   }

   public void SaveToDatabase(){
       // Save some data to the db
   }
   public void ReadFromDatabase(){
      // Read some data from db
   }
}

//Application
public class Foo {
    public Database db = new Database (DatabaseType.MySQL);
    public void SaveData(){
        db.SaveToDatabase();
    }
}

내가 생각할 수있는 유일한 것은 모든 단일 Database메소드 의 if 문입니다.

public void SaveToDatabase(){
   if(databaseType == DatabaseType.MySQL){

   }
   else if(databaseType == DatabaseType.SQLLite){

   }
}

답변:


11

원하는 것은 응용 프로그램이 사용 하는 인터페이스에 대한 여러 가지 구현 입니다 .

이렇게 :

public interface IDatabase
{
    void SaveToDatabase();
    void ReadFromDatabase();
}

public class MySQLDatabase : IDatabase
{
   public MySQLDatabase ()
   {
      //init stuff
   }

   public void SaveToDatabase(){
       //MySql implementation
   }
   public void ReadFromDatabase(){
      //MySql implementation
   }
}

public class SQLLiteDatabase : IDatabase
{
   public SQLLiteDatabase ()
   {
      //init stuff
   }

   public void SaveToDatabase(){
       //SQLLite implementation
   }
   public void ReadFromDatabase(){
      //SQLLite implementation
   }
}

//Application
public class Foo {
    public IDatabase db = GetDatabase();

    public void SaveData(){
        db.SaveToDatabase();
    }

    private IDatabase GetDatabase()
    {
        if(/*some way to tell if should use MySql*/)
            return new MySQLDatabase();
        else if(/*some way to tell if should use MySql*/)
            return new SQLLiteDatabase();

        throw new Exception("You forgot to configure the database!");
    }
}

IDatabase응용 프로그램에서 런타임에 올바른 구현 을 설정하는 더 좋은 방법은 " 공장 방법 "및 " 종속성 주입 " 과 같은 항목을 살펴보아야 합니다.


25

Caleb의 대답은 그가 올바른 길을 가고 있지만 실제로 잘못되었습니다. 그의 Foo수업은 데이터베이스 외관과 팩토리 역할을합니다. 이것들은 두 가지 책임이며 단일 수업에 참여해서는 안됩니다.


특히 데이터베이스 컨텍스트에서이 질문이 너무 많이 요청되었습니다. 여기에서는 추상화 (인터페이스 사용)를 사용하여 응용 프로그램의 결합을 줄이고 활용 성을 높이는 이점을 철저히 보여줄 것입니다.

더 읽기 전에, Dependency injection 을 아직 모른다면 읽고 이해하는 것이 좋습니다 . 또한 기본적으로 인터페이스의 공용 메소드 뒤에 구현 세부 사항을 숨기는 것이 의미 하는 어댑터 디자인 패턴 을 점검 할 수도 있습니다 .

팩토리 디자인 패턴 과 결합 된 의존성 주입 은 IoC 원칙 의 일부인 전략 디자인 패턴 을 코딩하는 쉬운 방법 입니다.

전화하지 마십시오 . 전화하겠습니다 . (일명 할리우드 원칙 ).


추상화를 사용하여 응용 프로그램 분리

1. 추상화 레이어 만들기

C ++와 같은 언어로 코딩하는 경우 인터페이스 또는 추상 클래스를 작성하고이 인터페이스에 일반 메소드를 추가하십시오. 인터페이스와 추상 클래스는 모두 직접 사용할 수없는 동작을 가지고 있지만 인터페이스의 경우 (인터페이스의 경우) 구현하거나 추상적 인 클래스의 경우 확장해야하므로 코드 자체에서 이미 제안합니다. 인터페이스 또는 추상 클래스가 제공하는 계약을 완전히 채우려면 특정 구현이 필요합니다.

귀하의 (매우 간단한 예) 데이터베이스 인터페이스는 다음과 같습니다 (DatabaseResult 또는 DbQuery 클래스는 각각 데이터베이스 작업을 나타내는 자체 구현이 됨).

public interface Database
{
    DatabaseResult DoQuery(DbQuery query);
    void BeginTransaction();
    void RollbackTransaction();
    void CommitTransaction();
    bool IsInTransaction();
}

이것은 인터페이스이기 때문에 실제로는 아무것도하지 않습니다. 따라서이 인터페이스를 구현하려면 클래스가 필요합니다.

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
    }
}

이제 Database인터페이스 를 구현하는 클래스 가 생겼습니다. 인터페이스가 유용 해졌습니다.

2. 추상화 계층 사용

응용 프로그램 어딘가에 메서드 SecretMethod가 있습니다. 재미를 위해 메서드를 호출합시다. 이 메서드 내에서 일부 데이터를 가져 오기 위해 데이터베이스를 사용해야합니다.

이제 인터페이스를 만들 수 있습니다. 직접 만들 수는 없지만 (어떻게 사용하는지) 키워드를 MyMySQLDatabase사용하여 구성 할 수 있는 class 가 있습니다 new.

큰! 데이터베이스를 사용하고 싶습니다 MyMySQLDatabase.

방법은 다음과 같습니다.

public void SecretMethod()
{
    var database = new MyMySQLDatabase(new CSharpMySQLDriver());

    // you will use the database here, which has the DoQuery,
    // BeginTransaction, RollbackTransaction and CommitTransaction methods
}

이것은 좋지 않습니다. 이 메소드 내에서 직접 클래스를 작성하고 있으며 내부에서 클래스를 수행하는 경우 SecretMethod30 개의 다른 메소드에서 동일한 작업을 수행한다고 가정하는 것이 안전합니다. 당신이 변경을 원하는 경우 MyMySQLDatabase등, 다른 클래스에 MyPostgreSQLDatabase, 당신은 모든 30의 방법에서 변경해야합니다.

또 다른 문제점은 작성 MyMySQLDatabase실패가 발생하면 메소드가 완료되지 않으므로 유효하지 않다는 것입니다.

우리 MyMySQLDatabase는 매개 변수로 메소드에 매개 변수를 전달 하여 생성을 리팩토링하는 것으로 시작합니다 (이를 종속성 주입이라고 함).

public void SecretMethod(MyMySQLDatabase database)
{
    // use the database here
}

이렇게하면 MyMySQLDatabase개체를 만들 수없는 문제가 해결 됩니다. SecretMethod유효한 MyMySQLDatabase객체를 기대 하기 때문에 어떤 일이 발생하고 객체가 객체에 전달되지 않으면 메서드가 실행되지 않습니다. 그리고 그것은 완전히 괜찮습니다.


일부 응용 프로그램에서는 이것으로 충분할 수 있습니다. 만족할 수도 있지만 더 나은 리팩토링을하자.

다른 리팩토링의 목적

보시다시피, 지금 객체를 SecretMethod사용하고 MyMySQLDatabase있습니다. MySQL에서 MSSQL로 이동했다고 가정 해 봅시다. 당신은 정말 당신의 내부에 모든 로직을 변경 기분이 안 SecretMethod하는 호출하는 방법 BeginTransactionCommitTransaction상의 방법을 database새 클래스를 만들 수 있도록 매개 변수로 전달 변수 MyMSSQLDatabase도있을 것이다, BeginTransaction그리고 CommitTransaction방법을.

그런 다음 선언을 SecretMethod다음으로 변경하십시오 .

public void SecretMethod(MyMSSQLDatabase database)
{
    // use the database here
}

그리고 클래스 MyMSSQLDatabaseMyMySQLDatabase동일한 메소드가 있기 때문에 다른 것을 변경할 필요가 없으며 여전히 작동합니다.

아 잠깐만!

구현 하는 Database인터페이스 MyMySQLDatabase가 있습니다. MyMSSQLDatabase클래스 도 있습니다. 클래스 는 클래스와 동일합니다 MyMySQLDatabase. 아마도 MSSQL 드라이버가 Database인터페이스를 구현할 수도 있으므로 정의에 추가합니다.

public class MyMSSQLDatabase : Database { }

그러나 MyMSSQLDatabasePostgreSQL로 전환했기 때문에 앞으로 더 이상 사용하지 않으려면 어떻게해야 합니까? 다시 정의를 바꿔야 SecretMethod합니까?

예, 그렇습니다. 그리고 그것은 제대로 들리지 않습니다. 지금 우리는 것을 알고 MyMSSQLDatabaseMyMySQLDatabase같은 방법이 있고 구현 모두 Database인터페이스를. 그래서 당신 SecretMethod은 이것을 다음과 같이 리팩토링합니다 .

public void SecretMethod(Database database)
{
    // use the database here
}

SecretMethodMySQL, MSSQL 또는 PotgreSQL을 사용하는지 여부를 더 이상 알지 못하는 방법에 주목 하십시오. 데이터베이스를 사용한다는 것을 알고 있지만 특정 구현에 대해서는 신경 쓰지 않습니다.

예를 들어 PostgreSQL과 같은 새 데이터베이스 드라이버를 만들려면 전혀 변경할 필요가 없습니다 SecretMethod. 을 만들고 MyPostgreSQLDatabase, Database인터페이스를 구현 하게하고 , PostgreSQL 드라이버 코딩을 마치고 작동하면 인스턴스를 생성하여에 삽입합니다 SecretMethod.

3. 원하는 구현을 얻기 Database

를 호출하기 전에 원하는 인터페이스 SecretMethod구현 Database(MySQL, MSSQL 또는 PostgreSQL인지 여부)을 결정해야합니다. 이를 위해 팩토리 디자인 패턴을 사용할 수 있습니다.

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

보시다시피 팩토리는 구성 파일에서 사용할 데이터베이스 유형을 알고 있습니다 (다시 말해 Config클래스는 자체 구현 일 수 있음).

이상적으로는 DatabaseFactory의존성 주입 컨테이너 안에 있습니다. 그러면 프로세스가 다음과 같이 보일 수 있습니다.

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

프로세스에서 특정 데이터베이스 유형을 작성하는 방법은 없습니다. 뿐만 아니라, 당신은 전혀 아무것도 만들지 않습니다. 구성에 따라 올바른 인터페이스 인스턴스를 리턴하는 메소드 인 종속성 주입 컨테이너 ( 변수) 내에 저장된 오브젝트 에서 GetDatabase메소드를 호출 합니다.DatabaseFactory_diDatabase

PostgreSQL을 사용한지 3 주 후에 MySQL로 돌아가려면 단일 구성 파일을 열고 DatabaseDriverfield 값을 에서 DatabaseEnum.PostgreSQL로 변경하십시오 DatabaseEnum.MySQL. 그리고 당신은 끝났습니다. 갑자기 나머지 응용 프로그램은 한 줄을 변경하여 MySQL을 다시 올바르게 사용합니다.


여전히 놀랍지 않다면 IoC에 대해 좀 더 자세히 살펴 보는 것이 좋습니다. 구성이 아니라 사용자 입력에서 특정 결정을 내리는 방법. 이러한 접근 방식을 전략 패턴이라고하며 엔터프라이즈 응용 프로그램에서 사용할 수 있고 사용하지만 컴퓨터 게임을 개발할 때 훨씬 더 자주 사용됩니다.


답을 사랑해, 데이빗 그러나 그러한 모든 대답과 마찬가지로 실제로 어떻게 적용 할 수 있는지 설명하는 것은 부족합니다. 실제 문제는 다른 데이터베이스 엔진을 호출하는 기능을 추상화하는 것이 아니라 실제 SQL 구문입니다. DbQuery예를 들어 물건을 가져 가십시오 . 해당 개체에 SQL 쿼리 문자열을 실행할 멤버가 있다고 가정하면 어떻게 일반화 할 수 있습니까?
DonBoitnott

1
@DonBoitnott 나는 당신이 모든 것을 일반화해야한다고 생각하지 않습니다. 일반적으로 응용 프로그램 계층 (도메인, 서비스, persitence) 사이에 추상화를 도입하고 모듈에 대한 추상화를 도입하고 싶거나 더 큰 프로젝트를 위해 개발하는 작지만 재사용 가능하고 고도로 사용자 정의 가능한 라이브러리에 추상화를 도입하고 싶을 수 있습니다. 모든 것을 인터페이스로 추상화 할 수 있지만 거의 필요하지 않습니다. 슬프게도 실제로는 하나도 없으며 요구 사항에서 비롯되기 때문에 모든 것에 대해 하나의 대답을하는 것은 정말 어렵습니다.
Andy

2
이해했다. 그러나 나는 정말로 그것을 그대로 의미했습니다. 일단 당신이 추상 클래스를 가지고 있고, 당신 은 적절한 SQL 언어를 사용하기 위해 내가 현재 사용중인 DB를 알아야 _secret.SecretMethod(database);한다는 사실로 모든 작업을 어떻게 조정하고 SecretMethod싶은지 알게됩니다. ? 그 사실을 모르는 코드의 대부분을 유지하기 위해 열심히 노력했지만 11 시간에 다시 알아야합니다. 나는 지금이 상황에 처해 있으며 다른 사람 이이 문제를 어떻게 해결했는지 알아 내려고 노력 중입니다.
DonBoitnott

@DonBoitnott 나는 당신이 무엇을 의미하는지 몰랐습니다. 지금 그것을 봅니다. DbQuery클래스 의 구체적인 구현 대신 인터페이스를 사용하고, 해당 인터페이스의 구현을 제공하고 대신 인터페이스를 사용하여 IDbQuery인스턴스 를 빌드 할 수 있습니다. DatabaseResult클래스에 일반 유형이 필요하다고 생각하지 않으며 데이터베이스의 결과가 비슷한 방식으로 항상 형식화 될 것으로 기대할 수 있습니다. 데이터베이스와 원시 SQL을 다룰 때 이미 DAL과 리포지토리 뒤에는 응용 프로그램의 수준이 낮기 때문에 필요가 없습니다.
Andy

... 더 이상 일반적인 접근 방식.
Andy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.