Jawaban Caleb, ketika dia berada di jalur yang benar, sebenarnya salah. Nya Foo
kelas 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 new
kata 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 MyMySQLDatabase
ke kelas yang berbeda, seperti MyPostgreSQLDatabase
, Anda harus mengubahnya di semua 30 metode Anda.
Masalah lain adalah, jika pembuatan MyMySQLDatabase
gagal, metode tidak akan pernah selesai dan karenanya tidak valid.
Kita mulai dengan refactoring pembuatan MyMySQLDatabase
dengan melewatkannya sebagai parameter ke metode (ini disebut injeksi ketergantungan).
public void SecretMethod(MyMySQLDatabase database)
{
// use the database here
}
Ini menyelesaikan masalah Anda, bahwa MyMySQLDatabase
objek tidak pernah dapat dibuat. Karena SecretMethod
mengharapkan MyMySQLDatabase
objek 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 SecretMethod
menggunakan MyMySQLDatabase
objek. Mari kita asumsikan Anda pindah dari MySQL ke MSSQL. Anda tidak benar-benar ingin mengubah semua logika di dalam Anda SecretMethod
, metode yang memanggil BeginTransaction
dan CommitTransaction
metode pada database
variabel yang dilewatkan sebagai parameter, sehingga Anda membuat kelas baru MyMSSQLDatabase
, yang juga akan memiliki BeginTransaction
dan CommitTransaction
metode.
Kemudian Anda pergi ke depan dan mengubah deklarasi SecretMethod
menjadi yang berikut.
public void SecretMethod(MyMSSQLDatabase database)
{
// use the database here
}
Dan karena kelas MyMSSQLDatabase
dan MyMySQLDatabase
memiliki metode yang sama, Anda tidak perlu mengubah apa pun dan itu akan tetap berfungsi.
Oh tunggu!
Anda memiliki Database
antarmuka, yang MyMySQLDatabase
implementasinya, Anda juga memiliki MyMSSQLDatabase
kelas, yang memiliki metode yang sama persis MyMySQLDatabase
, mungkin driver MSSQL juga dapat mengimplementasikan Database
antarmuka, sehingga Anda menambahkannya ke definisi.
public class MyMSSQLDatabase : Database { }
Tetapi bagaimana jika saya, di masa depan, tidak ingin menggunakan MyMSSQLDatabase
lagi, karena saya beralih ke PostgreSQL? Saya harus, sekali lagi, mengganti definisi SecretMethod
?
Ya, tentu saja. Dan itu kedengarannya tidak benar. Sekarang kita tahu, itu MyMSSQLDatabase
dan MyMySQLDatabase
memiliki metode yang sama dan keduanya mengimplementasikan Database
antarmuka. Jadi Anda refactor SecretMethod
agar terlihat seperti ini.
public void SecretMethod(Database database)
{
// use the database here
}
Perhatikan, bagaimana SecretMethod
tidak 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 SecretMethod
sama sekali. Anda akan membuat MyPostgreSQLDatabase
, membuatnya mengimplementasikan Database
antarmuka 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 Database
antarmuka 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, Config
kelas mungkin merupakan implementasi Anda sendiri).
Idealnya, Anda akan memiliki bagian DatabaseFactory
dalam 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 GetDatabase
metode pada DatabaseFactory
objek yang disimpan di dalam wadah injeksi ketergantungan Anda ( _di
variabel), sebuah metode, yang akan mengembalikan Anda instance Database
antarmuka 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 DatabaseDriver
bidang dari DatabaseEnum.PostgreSQL
menjadi 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.
DbQuery
objek Anda , misalnya. Dengan asumsi objek itu berisi anggota untuk string kueri SQL yang akan dieksekusi, bagaimana mungkin seseorang membuat generik itu?_secret.SecretMethod(database);
bagaimana seseorang mendamaikan semua itu bekerja dengan fakta bahwa sekarang sayaSecretMethod
masih 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.DbQuery
kelas, menyediakan implementasi dari antarmuka tersebut dan menggunakannya, memiliki pabrik untuk membuatIDbQuery
instance. Saya tidak berpikir Anda akan membutuhkan tipe generik untukDatabaseResult
kelas, 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 ...