Soyut veritabanı arabirimleri birden çok veritabanı türünü desteklemek için nasıl yazılır?


12

Daha büyük uygulamalarında MySQL, SQLLite, MSSQL, vb.

Bu tasarım desenine ne denir ve tam olarak nerede başlar?

Diyelim ki aşağıdaki yöntemlere sahip bir sınıf yazmanız gerekiyor

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

Aklıma gelen tek şey her Databaseyöntemde bir if ifadesidir

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

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

   }
}

Yanıtlar:


11

İstediğiniz, uygulamanızın kullandığı arabirim için birden çok uygulama .

şöyle:

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

IDatabaseUygulamanızda çalışma zamanında doğru uygulamayı kurmanın daha iyi bir yolu olarak , " Fabrika Yöntemi " ve " Dependancy Injection " gibi şeylere bakmalısınız .


25

Caleb'in cevabı, doğru yolda iken aslında yanlış. Onun Foosınıfı bir veritabanı cephesi ve fabrika gibi hem davranır. Bunlar iki sorumluluktur ve tek bir sınıfa konulmamalıdır.


Bu soru, özellikle veritabanı bağlamında, birçok kez sorulmuştur. Burada, uygulamanızı daha az eşleştirilmiş ve daha çok yönlü hale getirmek için soyutlama (arayüzler kullanarak) kullanmanın faydasını iyice göstermeye çalışacağım.

Daha fazla okumadan önce, henüz bilmiyorsanız, Bağımlılık enjeksiyonunu okumanızı ve temel bir anlayışa sahip olmanızı öneririm . Ayrıca , arayüzün genel yöntemlerinin arkasındaki uygulama ayrıntılarını gizlemenin anlamı olan Adaptör tasarım desenini de kontrol etmek isteyebilirsiniz .

Bağımlılık enjeksiyonu, Fabrika tasarım deseni ile birleştiğinde , temel taştır ve IoC prensibinin bir parçası olan Strateji tasarım desenini kodlamanın kolay bir yoludur .

Bizi arama, seni arayacağız . (AKA Hollywood prensibi ).


Bir uygulamayı soyutlama kullanarak ayırma

1. soyutlama katmanı Yapma

C ++ gibi bir dilde kodlama yapıyorsanız bir arabirim (veya soyut sınıf) oluşturursunuz ve bu arabirime genel yöntemler eklersiniz. Hem arabirimler hem de soyut sınıflar, bunları doğrudan kullanamayacağınız davranışına sahip olduklarından, ancak bunları uygulamanız (arabirim durumunda) veya genişletmeniz (soyut sınıf durumunda), kodun zaten önerdiğini, arabirim veya soyut sınıf tarafından verilen sözleşmeyi tamamlamak için özel uygulamalara sahip olmaları gerekir.

(Çok basit bir örnek) veritabanı arayüzünüz aşağıdaki gibi görünebilir (sırasıyla DatabaseResult veya DbQuery sınıfları, veritabanı işlemlerini temsil eden kendi uygulamalarınız olacaktır):

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

Bu bir arayüz olduğundan, kendisi gerçekten hiçbir şey yapmaz. Yani bu arayüzü uygulamak için bir sınıfa ihtiyacınız var.

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

Şimdi uygulayan bir sınıfınız var Database, arayüz artık kullanışlı oldu.

2. Soyutlama katmanını kullanma

Uygulamanızın bir yerinde, bir yönteminiz var SecretMethod, sadece eğlence için yöntemi çağıralım ve bu yöntemin içinde veritabanını kullanmanız gerekiyor, çünkü bazı verileri almak istiyorsunuz.

Şimdi doğrudan oluşturamadığınız bir arayüzünüz var (uh, o zaman nasıl kullanırım), ancak anahtar kelime MyMySQLDatabasekullanılarak oluşturulabilecek bir sınıfınız var new.

HARİKA! Bir veritabanı kullanmak istiyorum, bu yüzden kullanacağım MyMySQLDatabase.

Metodunuz şöyle görünebilir:

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

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

Bu iyi değil. Doğrudan bu yöntemin içinde bir sınıf oluşturuyorsunuz ve bunu içinde SecretMethodyapıyorsanız, diğer 30 yöntemde de aynısını yapacağınızı varsaymak güvenlidir. Eğer MyMySQLDatabasefarklı bir sınıfa MyPostgreSQLDatabasegeçmek isterseniz , örneğin 30 metodunuzun tümünde bunu değiştirmeniz gerekir.

Başka bir sorun, MyMySQLDatabasebaşarısız oluşturma , yöntem asla bitmez ve bu nedenle geçersiz olacaktır.

Biz MyMySQLDatabaseyöntemini bir parametre olarak geçirerek (bu bağımlılık enjeksiyon denir) oluşturulması yeniden düzenleyerek başlar .

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

Bu, MyMySQLDatabasenesnenin asla yaratılamayacağı sorunu çözer . Çünkü SecretMethodgeçerli bir beklediğini MyMySQLDatabasenesneyi bir şey oldu ve nesne kendisine geçirilen asla olsaydı, yöntem çalıştırmak asla. Ve bu tamamen iyi.


Bazı uygulamalarda bu yeterli olabilir. Memnun olabilirsiniz, ama daha da iyi olmak için yeniden düzenleyelim.

Başka bir yeniden düzenlemenin amacı

Gördüğünüz gibi, şu anda SecretMethodbir MyMySQLDatabasenesne kullanıyor . Diyelim ki MySQL'den MSSQL'e geçtiniz. Gerçekten, içindeki tüm mantığı SecretMethod, parametre olarak geçirilen değişken üzerinde a BeginTransactionve CommitTransactionyöntemleri çağıran bir yöntem değiştirmek gibi hissetmezsiniz database, böylece ve yöntemlerine MyMSSQLDatabasesahip olacak yeni bir sınıf oluşturursunuz .BeginTransactionCommitTransaction

Sonra devam edip beyanını SecretMethodaşağıdaki şekilde değiştirirsiniz.

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

Ve sınıflar MyMSSQLDatabaseve MyMySQLDatabaseaynı yöntemlere sahip olduğundan, başka bir şeyi değiştirmenize gerek yoktur ve yine de çalışır.

Bekle!

Uygulayan bir Databasearayüze MyMySQLDatabasesahipsiniz, MyMSSQLDatabasesınıfla da tamamen aynı yöntemlere sahipsiniz MyMySQLDatabase, belki de MSSQL sürücüsü Databasearayüzü de uygulayabilir , böylece tanıma eklersiniz.

public class MyMSSQLDatabase : Database { }

Ama gelecekte MyMSSQLDatabasePostgreSQL'e geçtiğim için artık kullanmak istemezsem ne olur ? Tekrar tanımını değiştirmek zorunda kalırdım SecretMethod?

Evet, istersiniz. Ve bu doğru gelmiyor. Şu anda biliyoruz, bunu MyMSSQLDatabaseve MyMySQLDatabaseaynı yöntemlere sahip ve her ikisi de Databasearayüzü uyguluyor . Yani böyle SecretMethodgörünmek için refactor .

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

Bildirim, nasıl SecretMethodartık MySQL, MSSQL veya PotgreSQL kullanıp kullanmadığınızı, bilir. Bir veritabanı kullandığını bilir, ancak belirli uygulamaları umursamaz.

Şimdi yeni veritabanı sürücünüzü oluşturmak istiyorsanız, örneğin PostgreSQL için, hiç değiştirmenize gerek kalmayacak SecretMethod. Bir yapacak MyPostgreSQLDatabase, Databasearayüzü uygulayacak ve PostgreSQL sürücüsünü kodladıktan ve çalıştıktan sonra, örneğini oluşturacak ve içine enjekte edeceksiniz SecretMethod.

3. İstenilen uygulamaların elde edilmesi Database

Aramaya başlamadan önce SecretMethod, hangi Databasearayüzün uygulanmasını istediğinize karar vermelisiniz (MySQL, MSSQL veya PostgreSQL olsun). Bunun için fabrika tasarım desenini kullanabilirsiniz.

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

Gördüğünüz gibi fabrika, bir yapılandırma dosyasından hangi veritabanı türünün kullanılacağını bilir (yine Configsınıf kendi uygulamanız olabilir).

İdeal olarak, DatabaseFactorybağımlılık enjeksiyon kabınızın içine sahip olacaksınız . İşleminiz bu şekilde görünebilir.

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

Bakın, süreçte hiçbir yerde belirli bir veritabanı türü oluşturmuyorsunuz. Sadece bu da değil, hiçbir şey yaratmıyorsunuz. Bağımlılık enjeksiyon kabınızın ( değişken) içinde depolanan nesne üzerinde, yapılandırmanıza bağlı olarak size doğru arabirim örneğini döndürecek bir GetDatabaseyöntem çağırıyorsunuz .DatabaseFactory_diDatabase

PostgreSQL kullandıktan 3 hafta sonra MySQL'e geri dönmek isterseniz, tek bir yapılandırma dosyası açar ve DatabaseDriveralanın değerini olarak olarak DatabaseEnum.PostgreSQLdeğiştirirsiniz DatabaseEnum.MySQL. Ve işiniz bitti. Aniden uygulamanızın geri kalanı MySQL'i tek bir satırı değiştirerek tekrar doğru kullanır.


Hala şaşkın değilseniz, IoC'ye biraz daha dalmanızı tavsiye ederim. Bir yapılandırmadan değil, bir kullanıcı girdisinden nasıl belirli kararlar alabileceğiniz. Bu yaklaşıma strateji modeli denir ve kurumsal uygulamalarda kullanılabilir ve kullanılmasına rağmen, bilgisayar oyunları geliştirilirken çok daha sık kullanılır.


Cevabınızı seviyorum David. Ancak tüm bu yanıtlar gibi, kişinin bunu nasıl uygulamaya koyabileceği açıklanmamaktadır. Asıl sorun, farklı veritabanı motorlarına çağrı yapma yeteneğini soyutlamıyor, sorun gerçek SQL sözdizimidir. Örneğin, DbQuerynesnenizi ele alalım . Bu nesnenin yürütülecek bir SQL sorgu dizesi için bir üye içerdiği varsayılarak, bu jenerik nasıl yapılabilir?
DonBoitnott

1
@DonBoitnott Genel olmak için her şeye ihtiyacınız olacağını düşünmüyorum. Genellikle uygulama katmanları (etki alanı, hizmetler, sebat) arasında soyutlama yapmak istersiniz, ayrıca modüller için soyutlama yapmak isteyebilirsiniz, daha büyük bir proje için geliştirdiğiniz küçük ama yeniden kullanılabilir ve son derece özelleştirilebilir bir kütüphaneye soyutlama getirmek isteyebilirsiniz. Her şeyi arayüzlere soyutlayabilirsiniz, ancak bu nadiren gereklidir. Her şey için bir cevap vermek gerçekten zor, çünkü ne yazık ki, gerçekten bir tane yok ve gereksinimlerden geliyor.
Andy

2
Anladım. Ama gerçekten bunu kastetmiştim. Bir kez soyutlanmış sınıfınız var ve _secret.SecretMethod(database);nasıl aramak istediğiniz noktaya ulaşırsınız, hepsi bu mutabakatı nasıl yapar, şimdi SecretMethoduygun SQL lehçesini kullanmak için hangi DB ile çalıştığımı bilmek zorunda. ? Kodun çoğunu bu gerçeği bilmemek için çok çalıştınız, ancak daha sonra 11. saatte tekrar bilmelisiniz. Şimdi bu durumdayım ve başkalarının bu sorunu nasıl çözdüğünü anlamaya çalışıyorum.
DonBoitnott

@DonBoitnott Ne demek istediğini bilmiyordum, şimdi görüyorum. DbQuerySınıfın somut uygulamaları yerine bir arabirim kullanabilir , söz konusu arabirimin uygulamalarını sağlayabilir ve bunun yerine, IDbQueryörneği oluşturmak için bir fabrikaya sahip olabilirsiniz . DatabaseResultSınıf için genel bir tip gerekir sanmıyorum , her zaman benzer bir şekilde biçimlendirilmiş bir veritabanından sonuçları bekleyebilirsiniz. Buradaki şey, veritabanları ve ham SQL ile uğraşırken, uygulamanızda (DAL ve Depoların arkasında) zaten çok düşük bir seviyedesiniz, buna gerek yok ...
Andy

... artık genel bir yaklaşım.
Andy
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.