Bagaimana antarmuka basis data abstrak ditulis untuk mendukung berbagai jenis basis data?


12

Bagaimana seseorang mulai merancang kelas abstrak dalam aplikasi mereka yang lebih besar yang dapat berinteraksi dengan beberapa jenis database, seperti MySQL, SQLLite, MSSQL, dll?

Apa yang disebut pola desain ini dan di mana tepatnya dimulai?

Katakanlah Anda perlu menulis kelas yang memiliki metode berikut

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

Satu-satunya hal yang dapat saya pikirkan adalah pernyataan if dalam setiap Databasemetode

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

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

   }
}

Jawaban:


11

Yang Anda inginkan adalah beberapa implementasi untuk antarmuka yang digunakan aplikasi Anda.

seperti itu:

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

Sejauh cara yang lebih baik untuk mengatur IDatabaseimplementasi yang benar pada waktu berjalan dalam aplikasi Anda, Anda harus melihat ke dalam hal-hal seperti " Metode Pabrik ", dan " Ketergantungan Injeksi ".


25

Jawaban Caleb, ketika dia berada di jalur yang benar, sebenarnya salah. Nya Fookelas bertindak baik sebagai fasad database dan pabrik. Itu adalah dua tanggung jawab dan tidak boleh dimasukkan ke dalam satu kelas.


Pertanyaan ini, terutama dalam konteks basis data, sudah terlalu sering ditanyakan. Di sini saya akan mencoba untuk menunjukkan kepada Anda secara menyeluruh manfaat menggunakan abstraksi (menggunakan antarmuka) untuk membuat aplikasi Anda lebih sedikit digabungkan dan lebih fleksibel.

Sebelum membaca lebih lanjut, saya sarankan Anda membaca dan mendapatkan pemahaman dasar tentang injeksi Ketergantungan , jika Anda belum mengetahuinya. Anda mungkin juga ingin memeriksa pola desain Adaptor , yang pada dasarnya adalah apa yang menyembunyikan detail implementasi di balik metode publik antarmuka artinya.

Ketergantungan injeksi, ditambah dengan pola desain Pabrik , adalah batu fondasi dan cara mudah untuk kode pola desain Strategi , yang merupakan bagian dari prinsip IoC .

Jangan hubungi kami, kami akan menghubungi Anda . (AKA prinsip Hollywood ).


Memisahkan aplikasi menggunakan abstraksi

1. Membuat lapisan abstraksi

Anda membuat antarmuka - atau kelas abstrak, jika Anda membuat kode dalam bahasa seperti C ++ - dan menambahkan metode generik ke antarmuka ini. Karena kedua antarmuka dan kelas abstrak memiliki perilaku Anda tidak dapat menggunakannya secara langsung, tetapi Anda harus mengimplementasikan (dalam kasus antarmuka) atau memperluas (dalam kasus kelas abstrak) mereka, kode itu sendiri sudah menyarankan, Anda akan perlu memiliki implementasi spesifik untuk memenuhi kontrak yang diberikan oleh antarmuka atau kelas abstrak.

Antarmuka basis data (contoh sangat sederhana) Anda mungkin terlihat seperti ini (kelas DatabaseResult atau DbQuery masing-masing akan menjadi implementasi Anda sendiri yang mewakili operasi basis data):

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

Karena ini adalah antarmuka, itu sendiri tidak benar-benar melakukan apa pun. Jadi, Anda memerlukan kelas untuk mengimplementasikan antarmuka ini.

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

Sekarang Anda memiliki kelas yang mengimplementasikan Database, antarmuka menjadi bermanfaat.

2. Menggunakan lapisan abstraksi

Di suatu tempat di aplikasi Anda, Anda memiliki sebuah metode, sebut saja metode itu SecretMethod, hanya untuk bersenang-senang, dan di dalam metode ini Anda harus menggunakan database, karena Anda ingin mengambil beberapa data.

Sekarang Anda memiliki antarmuka, yang tidak dapat Anda buat secara langsung (eh, bagaimana saya menggunakannya), tetapi Anda memiliki kelas MyMySQLDatabase, yang dapat dibangun menggunakan newkata kunci.

BAGUS! Saya ingin menggunakan database, jadi saya akan menggunakan MyMySQLDatabase.

Metode Anda mungkin terlihat seperti ini:

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

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

Ini tidak bagus. Anda secara langsung membuat kelas di dalam metode ini, dan jika Anda melakukannya di dalam SecretMethod, aman untuk menganggap Anda akan melakukan hal yang sama di 30 metode lainnya. Jika Anda ingin mengubah MyMySQLDatabaseke kelas yang berbeda, seperti MyPostgreSQLDatabase, Anda harus mengubahnya di semua 30 metode Anda.

Masalah lain adalah, jika pembuatan MyMySQLDatabasegagal, metode tidak akan pernah selesai dan karenanya tidak valid.

Kita mulai dengan refactoring pembuatan MyMySQLDatabasedengan melewatkannya sebagai parameter ke metode (ini disebut injeksi ketergantungan).

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

Ini menyelesaikan masalah Anda, bahwa MyMySQLDatabaseobjek tidak pernah dapat dibuat. Karena SecretMethodmengharapkan MyMySQLDatabaseobjek yang valid , jika sesuatu terjadi dan objek tidak akan pernah diteruskan ke sana, metode tidak akan pernah berjalan. Dan itu sama sekali tidak masalah.


Dalam beberapa aplikasi ini mungkin cukup. Anda mungkin puas, tetapi mari kita refactor untuk menjadi lebih baik.

Tujuan dari refactoring lain

Anda bisa lihat, sekarang ini SecretMethodmenggunakan MyMySQLDatabaseobjek. Mari kita asumsikan Anda pindah dari MySQL ke MSSQL. Anda tidak benar-benar ingin mengubah semua logika di dalam Anda SecretMethod, metode yang memanggil BeginTransactiondan CommitTransactionmetode pada databasevariabel yang dilewatkan sebagai parameter, sehingga Anda membuat kelas baru MyMSSQLDatabase, yang juga akan memiliki BeginTransactiondan CommitTransactionmetode.

Kemudian Anda pergi ke depan dan mengubah deklarasi SecretMethodmenjadi yang berikut.

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

Dan karena kelas MyMSSQLDatabasedan MyMySQLDatabasememiliki metode yang sama, Anda tidak perlu mengubah apa pun dan itu akan tetap berfungsi.

Oh tunggu!

Anda memiliki Databaseantarmuka, yang MyMySQLDatabaseimplementasinya, Anda juga memiliki MyMSSQLDatabasekelas, yang memiliki metode yang sama persis MyMySQLDatabase, mungkin driver MSSQL juga dapat mengimplementasikan Databaseantarmuka, sehingga Anda menambahkannya ke definisi.

public class MyMSSQLDatabase : Database { }

Tetapi bagaimana jika saya, di masa depan, tidak ingin menggunakan MyMSSQLDatabaselagi, karena saya beralih ke PostgreSQL? Saya harus, sekali lagi, mengganti definisi SecretMethod?

Ya, tentu saja. Dan itu kedengarannya tidak benar. Sekarang kita tahu, itu MyMSSQLDatabasedan MyMySQLDatabasememiliki metode yang sama dan keduanya mengimplementasikan Databaseantarmuka. Jadi Anda refactor SecretMethodagar terlihat seperti ini.

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

Perhatikan, bagaimana SecretMethodtidak lagi tahu, apakah Anda menggunakan MySQL, MSSQL atau PotgreSQL. Ia tahu itu menggunakan database, tetapi tidak peduli dengan implementasi spesifik.

Sekarang jika Anda ingin membuat driver database baru Anda, untuk PostgreSQL misalnya, Anda tidak perlu mengubah SecretMethodsama sekali. Anda akan membuat MyPostgreSQLDatabase, membuatnya mengimplementasikan Databaseantarmuka dan setelah Anda selesai mengkode driver PostgreSQL dan berfungsi, Anda akan membuat instance dan menyuntikkannya ke dalam SecretMethod.

3. Memperoleh implementasi yang diinginkan dari Database

Anda masih harus memutuskan, sebelum memanggil SecretMethod, implementasi Databaseantarmuka yang Anda inginkan (apakah itu MySQL, MSSQL atau PostgreSQL). Untuk ini, Anda dapat menggunakan pola desain pabrik.

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

Pabrik, seperti yang Anda lihat, tahu tipe database mana yang akan digunakan dari file konfigurasi (sekali lagi, Configkelas mungkin merupakan implementasi Anda sendiri).

Idealnya, Anda akan memiliki bagian DatabaseFactorydalam wadah injeksi ketergantungan Anda. Proses Anda kemudian akan terlihat seperti ini.

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

Lihat, bagaimana tidak dalam proses Anda membuat tipe database tertentu. Tidak hanya itu, Anda tidak menciptakan apa pun. Anda memanggil GetDatabasemetode pada DatabaseFactoryobjek yang disimpan di dalam wadah injeksi ketergantungan Anda ( _divariabel), sebuah metode, yang akan mengembalikan Anda instance Databaseantarmuka yang benar, berdasarkan pada konfigurasi Anda.

Jika, setelah 3 minggu menggunakan PostgreSQL, Anda ingin kembali ke MySQL, Anda membuka file konfigurasi tunggal dan mengubah nilai DatabaseDriverbidang dari DatabaseEnum.PostgreSQLmenjadi DatabaseEnum.MySQL. Dan kamu sudah selesai. Tiba-tiba sisa aplikasi Anda dengan benar menggunakan MySQL lagi, dengan mengubah satu baris.


Jika Anda masih tidak kagum, saya sarankan Anda untuk menyelam lebih dalam IoC. Bagaimana Anda dapat membuat keputusan tertentu bukan dari konfigurasi, tetapi dari input pengguna. Pendekatan ini disebut pola strategi dan meskipun dapat dan digunakan dalam aplikasi perusahaan, ini jauh lebih sering digunakan ketika mengembangkan game komputer.


Cintai jawaban Anda, David. Tetapi seperti semua jawaban seperti itu, itu kurang menggambarkan bagaimana seseorang dapat mempraktikkannya. Masalah sebenarnya bukan mengabstraksi kemampuan untuk memanggil ke mesin database yang berbeda, masalahnya adalah sintaks SQL yang sebenarnya. Ambil DbQueryobjek Anda , misalnya. Dengan asumsi objek itu berisi anggota untuk string kueri SQL yang akan dieksekusi, bagaimana mungkin seseorang membuat generik itu?
DonBoitnott

1
@ DonBoitnott Saya tidak berpikir Anda akan perlu semuanya menjadi generik. Anda biasanya ingin memperkenalkan abstraksi di antara lapisan aplikasi (domain, layanan, kegigihan), Anda mungkin juga ingin memperkenalkan abstraksi untuk modul, Anda mungkin ingin memperkenalkan abstraksi ke perpustakaan kecil tapi dapat digunakan kembali dan sangat dapat disesuaikan yang sedang Anda kembangkan untuk proyek yang lebih besar, dll. Anda bisa saja mengabstraksikan semuanya ke antarmuka, tetapi itu jarang diperlukan. Sangat sulit untuk memberikan jawaban satu-untuk-segalanya, karena, sayangnya, sebenarnya tidak ada satu dan itu berasal dari persyaratan.
Andy

2
Dimengerti Tetapi saya benar-benar memaksudkan itu secara harfiah. Setelah Anda memiliki kelas abstrak Anda, dan Anda sampai pada titik di mana Anda ingin memanggil _secret.SecretMethod(database);bagaimana seseorang mendamaikan semua itu bekerja dengan fakta bahwa sekarang saya SecretMethodmasih harus tahu apa DB saya bekerja dengan untuk menggunakan dialek SQL yang tepat ? Anda telah bekerja sangat keras untuk menjaga sebagian besar kode tidak mengetahui fakta itu, tetapi kemudian pada jam ke-11, Anda lagi harus tahu. Saya berada dalam situasi ini sekarang dan mencoba mencari tahu bagaimana orang lain telah memecahkan masalah ini.
DonBoitnott

@ DonBoitnott Saya tidak tahu apa yang Anda maksud, saya melihatnya sekarang. Anda bisa menggunakan antarmuka alih-alih implementasi konkret dari DbQuerykelas, menyediakan implementasi dari antarmuka tersebut dan menggunakannya, memiliki pabrik untuk membuat IDbQueryinstance. Saya tidak berpikir Anda akan membutuhkan tipe generik untuk DatabaseResultkelas, Anda selalu dapat mengharapkan hasil dari database diformat dengan cara yang sama. Masalahnya di sini adalah, ketika berhadapan dengan database dan SQL mentah, Anda sudah berada pada level rendah pada aplikasi Anda (di belakang DAL dan Repositori), sehingga tidak perlu untuk ...
Andy

... pendekatan generik lagi.
Andy
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.