C # arabiriminde bir önkoşul (LSP) nasıl belirtilir?


11

Diyelim ki şu arayüze sahibiz -

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

Önkoşul, herhangi bir yöntem çalıştırılmadan önce ConnectionString'in ayarlanması / başlatılması gerektiğidir.

Bu önkoşul, IDatabase soyut veya somut bir sınıf olsaydı bir yapıcı aracılığıyla connectionString iletilerek elde edilebilir.

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
}

Alternatif olarak, her yöntem için connectionString parametresi oluşturabiliriz, ancak yalnızca soyut bir sınıf oluşturmaktan daha kötü görünür -

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

Sorular -

  1. Arabirimin içinde bu önkoşulu belirtmenin bir yolu var mı? Bu geçerli bir "sözleşme" olduğunu, bu yüzden bunun için bir dil özelliği veya desen olup olmadığını merak ediyorum (soyut sınıf çözümü daha çok iki tür - bir arayüz ve soyut bir sınıf - oluşturma ihtiyacı yanı sıra bir kesmek imo bu gerekli)
  2. Bu daha teorik bir meraktır - Bu ön koşul aslında LSP bağlamında olduğu gibi bir ön koşul tanımına girer mi?

2
"LSP" ile sizler Liskov ikame ilkesinden mi bahsediyorsunuz? "Ördek ördek gibi ama pil ihtiyacı onun değil bir ördek" ilkesi? Çünkü gördüğüm kadarıyla ISS ve SRP'nin ihlali belki de OCP değil, aslında LSP değil.
Sebastien

2
Sadece bil diye, bütün bu kavram "ConnectionString seti olmalıdır / çalıştırılabilir yöntemlerden herhangi önce intialized" zamansal bağlantı örneğidir blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling ve kaçınılmalıdır, eğer mümkün.
Richiban

Seemann, Abstract Factory'nin gerçekten büyük bir hayranı.
Adrian Iftode

Yanıtlar:


10
  1. Evet. Net 4.0'dan itibaren Microsoft, Kod Sözleşmeleri sağlar . Bunlar formdaki önkoşulları tanımlamak için kullanılabilir Contract.Requires( ConnectionString != null );. Bununla birlikte, bu işlemi bir arayüz için yapmak için, hala IDatabaseContractekli olan bir yardımcı sınıfa ihtiyacınız olacak IDatabaseve ön koşulun, arayüzünüzün tutacağı her bir yöntem için tanımlanması gerekir. Arayüzler için kapsamlı bir örnek için buraya bakınız .

  2. Evet , LSP bir sözleşmenin hem sözdizimsel hem de anlamsal bölümleriyle ilgilenir.


Kod Sözleşmelerini bir arayüzde kullanabileceğinizi düşünmüyordum. Verdiğiniz örnek, bunların sınıflarda kullanıldığını gösterir . Sınıflar bir arabirime uygundur, ancak arabirimin kendisi Kod Sözleşmesi bilgisi içermez (gerçekten bir utanç. Bu, koymak için ideal bir yer olacaktır).
Robert Harvey

1
@RobertHarvey: evet, haklısın. Teknik olarak, elbette ikinci bir sınıfa ihtiyacınız var, ancak bir kez tanımlandığında, sözleşme arayüzün her uygulaması için otomatik olarak çalışır.
Doc Brown

21

Bağlama ve sorgulama iki ayrı konudur. Bu nedenle, iki ayrı arabirime sahip olmalıdırlar.

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

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

Bu hem IDatabasekullanıldığında bağlanmasını sağlar hem de istemcinin ihtiyaç duymadığı arabirime bağlı olmamasını sağlar.


"Bu türler aracılığıyla önkoşulları uygulama modeli" hakkında daha açık olabilir
Caleth

@Caleth: Bu bir "önkoşulları uygulama genel modeli" değildir. Bu, bağlantının her şeyden önce gerçekleşmesini sağlamak için bu özel gereksinim için bir çözümdür. Diğer ön koşullar farklı çözümlere ihtiyaç duyacaktır (cevabımda bahsettiğim gibi). Bu gereksinim için eklemek istiyorum, açıkça Euphoric'in benim önerisini tercih ederim, çünkü çok daha basit ve herhangi bir ek üçüncü taraf bileşenine ihtiyaç duymuyor.
Doc Brown

Bu belirli requrement şey önce olur başka bir şey kabul görmektedir. Ben de cevabınızın bu soruya daha iyi uyduğunu düşünüyorum , ancak bu cevap geliştirilebilir
Caleth

1
Bu cevap konuyu tamamen kaçırıyor. IDatabaseArayüz veri tabanında bir bağlantı kurma ve daha sonra rasgele sorguları yürütme edebilen bir nesneyi tanımlamaktadır. Bu ise veritabanı ve kodunun geri kalanı arasında bir sınır olarak görev nesne. Bu nedenle, bu nesnenin sorguların davranışını etkileyebilecek durumu (bir işlem gibi) koruması gerekir . Onları aynı sınıfa koymak çok pratiktir.
jpmc26

4
@ jpmc26 Durum IDatabase uygulayan sınıf içinde korunabileceğinden, itirazlarınızın hiçbiri mantıklı değil. Ayrıca, onu oluşturan üst sınıfa da başvurabilir ve böylece tüm veritabanı durumuna erişebilir.
Euphoric

5

Bir adım geriye gidelim ve buradaki büyük resme bakalım.

Nedir IDatabasebireyin sorumluluk?

Birkaç farklı işlemi vardır:

  • Bir bağlantı dizesini ayrıştırma
  • Veritabanıyla bağlantı açma (harici bir sistem)
  • Veritabanına mesaj gönderme; mesajlar veritabanına durumunu değiştirmek için komut verir
  • Veritabanından yanıtlar alın ve bunları arayanın kullanabileceği bir formata dönüştürün
  • Bağlantıyı kapatın

Bu listeye baktığınızda, "Bu SRP'yi ihlal etmiyor mu?" Diye düşünüyor olabilirsiniz. Ama sanmıyorum. Tüm işlemler tek ve uyumlu bir kavramın parçasıdır: veritabanına durumsal bir bağlantıyı yönetme (harici bir sistem) . Bağlantıyı kurar, bağlantının mevcut durumunu izler (özellikle diğer bağlantılarda yapılan işlemlerle ilgili olarak), bağlantının geçerli durumunu ne zaman gerçekleştireceğini vb. Gösterir. Bu anlamda bir API görevi görür çoğu arayanın umursamayacağı birçok uygulama ayrıntısını gizler. Örneğin, HTTP, soketler, borular, özel TCP, HTTPS kullanıyor mu? Telefon kodu umursamıyor; sadece mesaj göndermek ve yanıt almak istiyor. Bu kapsülleme için iyi bir örnektir.

Emin miyiz? Bu operasyonların bazılarını ayıramadık mı? Belki, ama faydası yok. Onları bölmeye çalışırsanız, bağlantıyı açık tutan ve / veya mevcut durumun ne olduğunu yöneten merkezi bir nesneye ihtiyacınız olacaktır. Diğer tüm işlemler aynı duruma güçlü bir şekilde bağlanır ve bunları ayırmaya çalışırsanız, yine de bağlantı nesnesine tekrar temsilci atacaklardır. Bu işlemler doğal ve mantıksal olarak devlete bağlıdır ve bunları ayırmanın bir yolu yoktur. Ayırma yapabildiğimiz zaman harika, ama bu durumda aslında yapamayız. En azından DB ile konuşmak için çok farklı, vatansız bir protokol olmadan ve bu aslında ACID uyumluluğu gibi çok önemli sorunları çok daha zor hale getirir. Ayrıca, bu işlemleri bağlantıdan ayırmaya çalışırken, arayanların umursamadığı protokolle ilgili ayrıntıları ortaya çıkarmak zorunda kalacaksınız, çünkü bir çeşit "keyfi" mesaj göndermenin bir yoluna ihtiyacınız olacak veritabanına.

Durum bilgisi olan bir protokolle uğraştığımızın son alternatifinizi (bağlantı dizesini parametre olarak geçirme) oldukça katı bir şekilde dışladığını unutmayın.

Gerçekten ayarlamak için bağlantı dizesine ihtiyacımız var mı?

Evet. Sen olamaz açmak Eğer bir bağlantı dizesi kadar bağlantı ve bağlantı açmak kadar protokolü ile bir şey yapamaz. Bu yüzden , bir tane olmadan bir bağlantı nesnesine sahip olmak anlamsızdır .

Bağlantı dizesini gerektirme sorununu nasıl çözeriz?

Çözmeye çalıştığımız sorun, nesnenin her zaman kullanılabilir bir durumda olmasını istiyoruz. OO dillerinde devleti yönetmek için ne tür bir varlık kullanılır? Nesneler , arayüzler değil. Arayüzlerin yönetilecek durumu yoktur. Çözmeye çalıştığınız sorun bir devlet yönetimi sorunu olduğundan, burada bir arayüz gerçekten uygun değil. Soyut bir sınıf çok daha doğaldır. Yani bir kurucu ile soyut bir sınıf kullanın.

Bağlantı açılmadan önce de işe yaramadığından, kurucu sırasında bağlantıyı gerçekten açmayı da düşünebilirsiniz . protected OpenBir bağlantı açma işlemi veritabanına özgü olabileceğinden, bu soyut bir yöntem gerektirir . ConnectionStringBağlantıyı açtıktan sonra bağlantı dizesini değiştirmek anlamsız olacağından , özelliğin yalnızca bu durumda okunması da iyi bir fikirdir . (Dürüst olmak gerekirse, ben yine de onu okutmak istiyorum. Farklı bir dize ile bağlantı istiyorsanız, başka bir nesne yapın.)

Arayüze ihtiyacımız var mı?

Bağlantı üzerinden gönderebileceğiniz mevcut iletileri ve geri alabileceğiniz yanıt türlerini belirten bir arabirim yararlı olabilir. Bu, bu işlemleri yürüten ancak bir bağlantı açma mantığına bağlı olmayan kod yazmamızı sağlayacaktır. Ama asıl nokta: bağlantıyı yönetmek, "Hangi mesajları gönderebilirim ve veritabanına / mesajdan hangi mesajları geri alabilirim?" Arayüzünün bir parçası değildir, bu nedenle bağlantı dizesi bunun bir parçası bile olmamalıdır arayüz.

Bu rotaya gidersek, kodumuz şöyle görünebilir:

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
}

Düşürücü, katılmama nedenlerini açıklarsa takdir eder.
jpmc26

Anlaşıldı, yeniden: downvoter. Bu doğru çözüm. Bağlantı dizesi yapıcıda somut / soyut sınıfa sağlanmalıdır. Bir bağlantının açılması / kapatılmasının dağınık işi, bu nesneyi kullanan kodun bir endişesi değildir ve sınıfın kendi içinde kalmalıdır. OpenYöntemin olması gerektiğini privateve Connectionbağlantıyı oluşturan ve bağlanan korumalı bir özelliği ortaya koymanız gerektiğini iddia ediyorum . Veya korumalı bir OpenConnectionyöntemi ortaya çıkarın .
Greg Burghardt

Bu çözüm oldukça zarif ve çok iyi bir tasarım. Ancak tasarım kararlarının ardındaki bazı gerekçelerin yanlış olduğunu düşünüyorum. Temelde SRP ile ilgili ilk birkaç paragrafta. "IDatabase'in sorumluluğu nedir?" Bölümünde açıklandığı gibi SRP'yi ihlal ediyor. SRP için görülen sorumluluklar sadece bir sınıfın yaptığı veya yönettiği şeyler değildir. Ayrıca "aktörler" veya "değişim nedenleri". "Ben veritabanından yanıt almak ve onları arayanın kullanabileceği bir biçime dönüştürmek" SRP ihlal ettiğini düşünüyorum "değiştirmek için bir bağlantı dizesini ayrıştırmak" çok farklı bir neden vardır.
Sebastien

Yine de bunu onaylıyorum.
Sebastien

1
Ve BTW, SOLID müjde değildir. Bir çözüm tasarlarken akılda tutulması çok önemlidir. Ancak NEDEN bunu yaptığınızı biliyorsanız, onları ihlal edebilirsiniz, çözümünüzü NASIL etkileyecek ve eğer sorun çıkarırsa yeniden düzenleme ile işleri nasıl düzelteceksiniz. Bu yüzden yukarıda belirtilen çözüm SRP'yi ihlal etse bile henüz en iyisi olduğunu düşünüyorum.
Sebastien

0

Burada bir arayüze sahip olmanın nedenini gerçekten görmüyorum. Veritabanı sınıfınız SQL'e özgüdür ve gerçekten düzgün açılmayan bir bağlantıyı sorgulamadığınızdan emin olmanız için kullanışlı / güvenli bir yol sağlar. Yine de bir arayüzde ısrar ediyorsanız, bunu nasıl yapacağım.

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

Kullanım şöyle görünebilir:

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