Bagaimana cara menentukan prasyarat (LSP) di antarmuka dalam C #?

11

Katakanlah kita memiliki antarmuka berikut -

interface IDatabase { 
    string ConnectionString{get;set;}
    void ExecuteNoQuery(string sql);
    void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

Prasyaratnya adalah ConnectionString harus diatur / diinternisasi sebelum salah satu metode dapat dijalankan.

Prasyarat ini dapat agak dicapai dengan melewatkan koneksiString melalui konstruktor jika IDatabase adalah kelas abstrak atau konkret -

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
}

Atau, kita dapat membuat connectionString parameter untuk setiap metode, tetapi terlihat lebih buruk daripada hanya membuat kelas abstrak -

interface IDatabase { 
    void ExecuteNoQuery(string connectionString, string sql);
    void ExecuteNoQuery(string connectionString, string[] sql);
    //Various other methods all with the connectionString parameter
}

Pertanyaan -

  1. Apakah ada cara untuk menentukan prasyarat ini di dalam antarmuka itu sendiri? Ini adalah "kontrak" yang sah jadi saya bertanya-tanya apakah ada fitur bahasa atau pola untuk ini (solusi kelas abstrak lebih merupakan peretasan imo selain kebutuhan membuat dua jenis - antarmuka dan kelas abstrak - setiap kali ini dibutuhkan)
  2. Ini lebih merupakan keingintahuan teoritis - Apakah prasyarat ini benar-benar jatuh ke dalam definisi prasyarat seperti dalam konteks LSP?
Achilles
sumber
2
Dengan "LSP" kalian berbicara tentang prinsip substitusi Liskov? Prinsip "jika dukunya seperti bebek tetapi membutuhkan baterai, bukan prinsipnya"? Karena seperti yang saya lihat itu lebih merupakan pelanggaran terhadap ISP dan SRP bahkan mungkin OCP tetapi tidak benar-benar LSP.
Sebastien
2
Asal tahu saja, seluruh konsep "ConnectionString ini harus ditetapkan / diinternisasi sebelum salah satu metode dapat dijalankan" adalah contoh blog temporal coupling.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling dan harus dihindari, jika bisa jadi.
Richiban
Seemann benar-benar penggemar berat Pabrik Abstrak.
Adrian Iftode

Jawaban:

10
  1. Iya. Dari .Net 4.0 ke atas, Microsoft menyediakan Kontrak Kode . Ini dapat digunakan untuk mendefinisikan prasyarat dalam formulir Contract.Requires( ConnectionString != null );. Namun, untuk membuat ini berfungsi untuk sebuah antarmuka, Anda masih memerlukan kelas pembantu IDatabaseContract, yang menjadi melekat IDatabase, dan prasyarat perlu ditentukan untuk setiap metode individu antarmuka Anda di mana ia akan dipegang. Lihat di sini untuk contoh luas untuk antarmuka.

  2. Ya , LSP berkaitan dengan bagian sintaksis dan semantik dari suatu kontrak.

Doc Brown
sumber
Saya tidak berpikir Anda bisa menggunakan Kontrak Kode dalam sebuah antarmuka. Contoh yang Anda berikan menunjukkan bahwa mereka digunakan di kelas. Kelas-kelas memang sesuai dengan antarmuka, tetapi antarmuka itu sendiri tidak mengandung informasi Kontrak Kode (memalukan, sungguh. Itu akan menjadi tempat yang ideal untuk meletakkannya).
Robert Harvey
1
@RobertHarvey: ya, Anda benar. Secara teknis, Anda memerlukan kelas kedua, tentu saja, tetapi begitu ditentukan, kontrak bekerja secara otomatis untuk setiap implementasi antarmuka.
Doc Brown
21

Menghubungkan dan menanyakan adalah dua masalah terpisah. Dengan demikian, mereka harus memiliki dua antarmuka yang terpisah.

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

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

Ini memastikan bahwa keduanya IDatabaseakan terhubung saat digunakan dan membuat klien tidak bergantung pada antarmuka yang tidak diperlukan.

Euforia
sumber
Bisa lebih eksplisit tentang "ini adalah pola menegakkan prasyarat melalui tipe"
Caleth
@Caleth: ini bukan "pola umum menegakkan prasyarat". Ini adalah solusi untuk persyaratan khusus ini untuk memastikan koneksi terjadi sebelum hal lain. Prasyarat lain akan membutuhkan solusi yang berbeda (seperti yang saya sebutkan dalam jawaban saya). Saya ingin menambahkan untuk persyaratan ini, saya jelas lebih suka saran Euphoric daripada saran saya, karena jauh lebih sederhana dan tidak memerlukan komponen pihak ketiga tambahan.
Doc Brown
Persyaratan khusus bahwa sesuatu terjadi sebelum sesuatu yang lain dapat diterapkan secara luas. Saya juga berpikir jawaban Anda lebih cocok dengan pertanyaan ini , tetapi jawaban ini dapat ditingkatkan
Caleth
1
Jawaban ini benar-benar melenceng. The IDatabaseantarmuka mendefinisikan sebuah objek yang mampu membangun koneksi ke database dan kemudian mengeksekusi query sewenang-wenang. Itu adalah objek yang bertindak sebagai batas antara database dan sisa kode. Karena itu, objek ini harus mempertahankan status (seperti transaksi) yang dapat memengaruhi perilaku kueri. Menempatkan mereka di kelas yang sama sangat praktis.
jpmc26
4
@ jpmc26 Tidak satu pun dari keberatan Anda yang masuk akal, karena status dapat dipertahankan dalam kelas yang mengimplementasikan IDatabase. Itu juga bisa mereferensikan kelas induk yang membuatnya, sehingga mendapatkan akses ke seluruh kondisi basis data.
Euforia
5

Mari kita mundur dan melihat gambaran yang lebih besar di sini.

Apa IDatabasetanggung jawabnya?

Ini memiliki beberapa operasi berbeda:

  • Parsing string koneksi
  • Buka koneksi dengan database (sistem eksternal)
  • Kirim pesan ke basis data; pesan memerintahkan database untuk mengubah statusnya
  • Menerima tanggapan dari database dan mengubahnya menjadi format yang dapat digunakan penelepon
  • Tutup koneksi

Melihat daftar ini, Anda mungkin berpikir, "Bukankah ini melanggar SRP?" Tapi saya rasa tidak. Semua operasi adalah bagian dari konsep kohesif tunggal: mengelola koneksi stateful ke database (sistem eksternal) . Itu membuat koneksi, itu melacak keadaan koneksi saat ini (dalam kaitannya dengan operasi yang dilakukan pada koneksi lain, khususnya), itu menandakan kapan melakukan keadaan koneksi saat ini, dll. Dalam hal ini, ia bertindak sebagai API yang menyembunyikan banyak detail implementasi yang tidak dipedulikan oleh sebagian besar penelepon. Misalnya, apakah menggunakan HTTP, soket, pipa, TCP kustom, HTTPS? Kode panggilan tidak peduli; ia hanya ingin mengirim pesan dan mendapat tanggapan. Ini adalah contoh enkapsulasi yang bagus.

Apakah kita yakin Tidak bisakah kita memisahkan beberapa operasi ini? Mungkin, tetapi tidak ada manfaatnya. Jika Anda mencoba untuk membaginya, Anda masih akan membutuhkan objek pusat yang membuat koneksi tetap terbuka dan / atau mengelola keadaan saat ini. Semua operasi lainnya sangat digabungkan ke negara yang sama, dan jika Anda mencoba untuk memisahkan mereka, mereka hanya akan berakhir mendelegasikan kembali ke objek koneksi. Operasi-operasi ini secara alami dan logis digabungkan ke negara, dan tidak ada cara untuk memisahkannya. Decoupling sangat bagus ketika kita bisa melakukannya, tetapi dalam kasus ini, kita sebenarnya tidak bisa. Setidaknya bukan tanpa protokol stateless yang sangat berbeda untuk berbicara dengan DB, dan itu akan membuat masalah yang sangat penting seperti kepatuhan ACID menjadi lebih sulit. Juga, dalam proses mencoba memisahkan operasi-operasi ini dari koneksi, Anda akan dipaksa untuk mengekspos detail tentang protokol yang tidak dipedulikan penelepon, karena Anda akan memerlukan cara mengirim semacam pesan "sewenang-wenang" ke database.

Perhatikan bahwa fakta yang kita hadapi dengan protokol stateful cukup mengesampingkan alternatif terakhir Anda (melewati string koneksi sebagai parameter).

Apakah kita benar-benar membutuhkan string koneksi untuk diatur?

Iya. Anda tidak dapat membuka koneksi sampai Anda memiliki string koneksi, dan Anda tidak dapat melakukan apa pun dengan protokol sampai Anda membuka koneksi. Jadi tidak ada gunanya memiliki objek koneksi tanpa satu.

Bagaimana kita memecahkan masalah yang membutuhkan string koneksi?

Masalah yang kita coba selesaikan adalah bahwa kita ingin objek berada dalam kondisi yang dapat digunakan setiap saat. Entitas apa yang digunakan untuk mengelola status dalam bahasa OO? Objek , bukan antarmuka. Antarmuka tidak memiliki negara untuk dikelola. Karena masalah yang Anda coba selesaikan adalah masalah manajemen negara, antarmuka tidak benar-benar sesuai di sini. Kelas abstrak jauh lebih alami. Jadi gunakan kelas abstrak dengan konstruktor.

Anda mungkin juga ingin mempertimbangkan untuk benar-benar membuka koneksi selama konstruktor, karena koneksi juga tidak berguna sebelum dibuka. Itu akan membutuhkan protected Openmetode abstrak karena proses membuka koneksi mungkin spesifik database. Ini juga merupakan ide yang baik untuk membuat ConnectionStringproperti hanya membaca dalam kasus ini, karena mengubah string koneksi setelah koneksi terbuka tidak akan berarti. (Jujur, saya hanya akan membuatnya membaca. Jika Anda ingin koneksi dengan string yang berbeda, buat objek lain.)

Apakah kita memerlukan antarmuka sama sekali?

Antarmuka yang menentukan pesan yang tersedia yang dapat Anda kirim melalui koneksi dan jenis respons yang bisa Anda dapatkan kembali bisa berguna. Ini akan memungkinkan kita untuk menulis kode yang mengeksekusi operasi ini tetapi tidak digabungkan dengan logika membuka koneksi. Tapi itu intinya: mengelola koneksi bukan bagian dari antarmuka, "Pesan apa yang bisa saya kirim dan pesan apa yang bisa saya dapatkan kembali ke / dari database?", Jadi string koneksi seharusnya tidak menjadi bagian dari itu antarmuka.

Jika kita melewati rute ini, kode kita mungkin terlihat seperti ini:

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
}
jpmc26
sumber
Akan menghargai jika downvoter akan menjelaskan alasan mereka tidak setuju.
jpmc26
Setuju, kembali: downvoter. Ini solusi yang tepat. String koneksi harus disediakan dalam konstruktor ke kelas konkret / abstrak. Bisnis berantakan membuka / menutup koneksi bukan masalah kode menggunakan objek ini, dan harus tetap internal ke kelas itu sendiri. Saya berpendapat bahwa Openmetode ini seharusnya privatedan Anda harus mengekspos Connectionproperti yang dilindungi yang menciptakan koneksi dan koneksi. Atau memaparkan OpenConnectionmetode yang dilindungi .
Greg Burghardt
Solusi ini cukup elegan dan desain yang sangat baik. Tapi saya pikir beberapa alasan di balik keputusan desain salah. Terutama dalam beberapa paragraf pertama tentang SRP. Itu melanggar SRP bahkan seperti yang dijelaskan dalam "Apa tanggung jawab IDatabase?". Tanggung jawab yang terlihat untuk SRP bukan hanya hal-hal yang dilakukan atau dikelola kelas. Ini juga "aktor" atau "alasan untuk berubah". Dan saya pikir itu melanggar SRP karena "Terima respons dari database dan mengubahnya menjadi format yang bisa digunakan penelepon" memiliki alasan yang sangat berbeda untuk berubah daripada "Parse a connection string".
Sebastien
Saya tetap mengungguli ini.
Sebastien
1
Dan BTW, SOLID bukanlah Injil. Tentu mereka sangat penting untuk diingat ketika merancang solusi. Tetapi Anda BISA melanggar mereka jika Anda tahu MENGAPA Anda melakukannya, BAGAIMANA itu akan mempengaruhi solusi Anda dan BAGAIMANA memperbaiki hal-hal dengan refactoring jika itu membuat Anda mendapat masalah. Jadi saya pikir bahkan jika solusi yang disebutkan di atas melanggar SRP itu adalah yang terbaik.
Sebastien
0

Saya benar-benar tidak melihat alasan memiliki antarmuka sama sekali di sini. Kelas basis data Anda adalah khusus SQL, dan benar-benar hanya memberi Anda cara yang nyaman / aman untuk memastikan Anda tidak menanyakan koneksi yang tidak dibuka dengan benar. Jika Anda bersikeras pada antarmuka, inilah cara saya akan melakukannya.

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

Penggunaannya mungkin terlihat seperti ini:

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