Liskov İkame İlkesini iyi bir C # örneği ile açıklayabilir misiniz? [kapalı]


94

Liskov İkame İlkesini (SOLID'in 'L'si), ilkenin tüm yönlerini basitleştirilmiş bir şekilde kapsayan iyi bir C # örneği ile açıklayabilir misiniz? Gerçekten mümkünse.


9
İşte kısaca bunun hakkında basitleştirilmiş bir düşünme yolu: LSP'yi takip edersem, kodumdaki herhangi bir nesneyi bir Mock nesnesiyle değiştirebilirim ve çağıran koddaki hiçbir şeyin değiştirmeyi hesaba katmak için ayarlanması veya değiştirilmesi gerekmez. LSP, Test by Mock kalıbı için temel bir destektir.
2014

Yanıtlar:


128

(Bu cevap 2013-05-13 yeniden yazılmıştır, yorumların altındaki tartışmayı okuyun)

LSP, temel sınıfın sözleşmesini takip etmekle ilgilidir.

Örneğin, temel sınıfı kullananların bunu beklemeyeceği için alt sınıflarda yeni istisnalar atamazsınız. ArgumentNullExceptionBir bağımsız değişken eksikse ve alt sınıf, bağımsız değişkenin boş olmasına izin veriyorsa, aynı zamanda bir LSP ihlali de temel sınıf atarsa geçerlidir.

LSP'yi ihlal eden bir sınıf yapısı örneği:

public interface IDuck
{
   void Swim();
   // contract says that IsSwimming should be true if Swim has been called.
   bool IsSwimming { get; }
}

public class OrganicDuck : IDuck
{
   public void Swim()
   {
      //do something to swim
   }

   bool IsSwimming { get { /* return if the duck is swimming */ } }
}

public class ElectricDuck : IDuck
{
   bool _isSwimming;

   public void Swim()
   {
      if (!IsTurnedOn)
        return;

      _isSwimming = true;
      //swim logic            
   }

   bool IsSwimming { get { return _isSwimming; } }
}

Ve arama kodu

void MakeDuckSwim(IDuck duck)
{
    duck.Swim();
}

Gördüğünüz gibi iki ördek örneği var. Bir organik ördek ve bir elektrikli ördek. Elektrikli ördek ancak açıksa yüzebilir. Bu, IsSwimming(aynı zamanda sözleşmenin bir parçası olan) temel sınıfta olduğu gibi ayarlanmayacağından yüzebilmek için açılması gerektiğinden LSP ilkesini ihlal eder.

Elbette böyle bir şey yaparak çözebilirsiniz.

void MakeDuckSwim(IDuck duck)
{
    if (duck is ElectricDuck)
        ((ElectricDuck)duck).TurnOn();
    duck.Swim();
}

Ancak bu, Açık / Kapalı ilkesini ihlal eder ve her yerde uygulanması gerekir (ve bu nedenle hala kararsız kod üretir).

Uygun çözüm, Swimyöntemde ördeği otomatik olarak açmak ve bunu yaparak elektrikli ördeğin tam olarak IDuckarayüz tarafından tanımlandığı gibi davranmasını sağlamak olacaktır.

Güncelleme

Birisi bir yorum ekledi ve onu kaldırdı. Ele almak istediğim geçerli bir nokta vardı:

SwimYöntemin içindeki ördeği açmanın çözümü , gerçek uygulama ( ElectricDuck) ile çalışırken yan etkilere neden olabilir . Ancak bu, açık bir arayüz uygulaması kullanılarak çözülebilir . imho Swim, IDuckarayüzü kullanırken yüzmesi beklendiğinden, onu AÇMADAN sorun yaşamanız daha olasıdır.

Güncelleme 2

Daha net hale getirmek için bazı kısımları yeniden basıldı.


1
@jgauffin: Örnek basit ve anlaşılır. Ama önerdiğiniz çözüm, öncelikle: Açık-Kapalı Prensibini çiğniyor ve Bob Amca'nın şöyle yazan tanımına uymuyor (makalesinin sonuç kısmına bakın): "Liskov İkame Prensibi (AKA Kontratla Tasarım) önemli bir özelliktir Açık-Kapalı ilkesine uyan tüm programlardan. " bkz: objectmentor.com/resources/articles/lsp.pdf
pencilCake

1
Çözümün Açık / Kapalı'yı nasıl kırdığını anlamıyorum. Kısmına atıfta bulunuyorsanız cevabımı tekrar okuyun if duck is ElectricDuck. Ben :) KATI geçen Perşembe hakkında bir seminer vardı
jgauffin

Aslında konu üzerinde değil, ama lütfen örnek kontrolünü iki kez yapmamak için değiştirebilir misin? Pek çok geliştirici asanahtar kelimenin farkında değildir , bu da onları birçok tür kontrolünden kurtarır. Aşağıdakine benzer bir şey düşünüyorum:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
Siewers

3
@jgauffin - Örnek beni biraz karıştırdı. Liskov İkame İlkesinin bu durumda yine de geçerli olacağını düşündüm çünkü Duck ve ElectricDuck, IDuck'tan türetildi ve IDuck'ın kullanıldığı her yere bir ElectricDuck veya Duck koyabilirsiniz. ElectricDuck'ın ördek yüzmeden önce açılması gerekiyorsa, bu ElectricDuck'ın veya ElectricDuck'ı başlatan ve ardından IsTurnedOn özelliğini true olarak ayarlayan bazı kodların sorumluluğu değildir. Bu LSP'yi ihlal ederse, tüm arayüzler kendi yöntemleri için farklı mantık içereceğinden LSV'ye uymak çok zor olacaktır.
Xaisoft

1
@MystereMan: imho LSP tamamen davranışsal doğrulukla ilgilidir. Dikdörtgen / kare örneğiyle, ayarlanan diğer özelliğin yan etkisini elde edersiniz. Ördekle yüzmemenin yan etkisini elde edersiniz. LSP:if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).
jgauffin

9

LSP Pratik Bir Yaklaşım

LSP'nin C # örneklerini aradığım her yerde, insanlar hayali sınıfları ve arayüzleri kullandılar. İşte sistemlerimizden birinde uyguladığım LSP'nin pratik uygulaması.

Senaryo: Müşteri verilerini sağlayan 3 veritabanımız (Mortgage Müşterileri, Cari Hesaplar Müşterileri ve Tasarruf Hesabı Müşterileri) olduğunu ve verilen müşterinin soyadı için müşteri ayrıntılarına ihtiyacımız olduğunu varsayalım. Şimdi bu 3 veritabanından soyadına göre 1'den fazla müşteri ayrıntısı alabiliriz.

Uygulama:

İŞ MODELİ KATMANI:

public class Customer
{
    // customer detail properties...
}

VERİ ERİŞİM KATMANI:

public interface IDataAccess
{
    Customer GetDetails(string lastName);
}

Yukarıdaki arayüz soyut sınıf tarafından uygulanmaktadır

public abstract class BaseDataAccess : IDataAccess
{
    /// <summary> Enterprise library data block Database object. </summary>
    public Database Database;


    public Customer GetDetails(string lastName)
    {
        // use the database object to call the stored procedure to retrieve the customer details
    }
}

Bu soyut sınıf, aşağıda gösterildiği gibi veritabanı sınıflarının her biri tarafından genişletilen 3 veritabanının tümü için ortak bir "GetDetails" yöntemine sahiptir.

MORTGAGE MÜŞTERİ VERİ ERİŞİMİ:

public class MortgageCustomerDataAccess : BaseDataAccess
{
    public MortgageCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetMortgageCustomerDatabase();
    }
}

CARİ HESAP MÜŞTERİ VERİ ERİŞİMİ:

public class CurrentAccountCustomerDataAccess : BaseDataAccess
{
    public CurrentAccountCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetCurrentAccountCustomerDatabase();
    }
}

TASARRUF HESABI MÜŞTERİ VERİ ERİŞİMİ:

public class SavingsAccountCustomerDataAccess : BaseDataAccess
{
    public SavingsAccountCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetSavingsAccountCustomerDatabase();
    }
}

Bu 3 veri erişim sınıfı belirlendikten sonra, şimdi dikkatimizi müşteriye çekiyoruz. İş katmanında, müşteri ayrıntılarını müşterilerine döndüren CustomerServiceManager sınıfımız var.

İŞ KATMANI:

public class CustomerServiceManager : ICustomerServiceManager, BaseServiceManager
{
   public IEnumerable<Customer> GetCustomerDetails(string lastName)
   {
        IEnumerable<IDataAccess> dataAccess = new List<IDataAccess>()
        {
            new MortgageCustomerDataAccess(new DatabaseFactory()), 
            new CurrentAccountCustomerDataAccess(new DatabaseFactory()),
            new SavingsAccountCustomerDataAccess(new DatabaseFactory())
        };

        IList<Customer> customers = new List<Customer>();

       foreach (IDataAccess nextDataAccess in dataAccess)
       {
            Customer customerDetail = nextDataAccess.GetDetails(lastName);
            customers.Add(customerDetail);
       }

        return customers;
   }
}

Şimdi zaten karmaşık hale geldiğinden, basit tutmak için bağımlılık enjeksiyonunu göstermedim.

Şimdi, yeni bir müşteri detay veritabanımız varsa, BaseDataAccess'i genişleten ve veritabanı nesnesini sağlayan yeni bir sınıf ekleyebiliriz.

Elbette, katılan tüm veritabanlarında aynı depolanan prosedürlere ihtiyacımız var.

Son olarak, CustomerServiceManagersınıf istemcisi yalnızca GetCustomerDetails yöntemini çağırır, lastName'i iletir ve verilerin nasıl ve nereden geldiği ile ilgilenmemelidir.

Umarım bu size LSP'yi anlamak için pratik bir yaklaşım sunar.


3
Bu nasıl bir LSP örneği olabilir?
somegeek

1
Bunda da LSP örneğini göremiyorum ... Neden bu kadar çok olumlu oy var?
StaNov

1
@RoshanGhangare IDataAccess, İş Katmanında ikame edilebilecek 3 somut uygulamaya sahiptir.
Yawar Murtaza

1
@YawarMurtaza örnek verdiğiniz örnek ne olursa olsun, strateji modelinin tipik uygulamasıdır. O LSP'yi kırılıyor ve LSP ihlalinin çözmek nasıl nerede net Lütfen artık
Yogesh

@Yogesh - IDataAccess uygulamasını somut sınıflarından herhangi biriyle takas edebilirsiniz ve bu istemci kodunu etkilemeyecektir - kısaca LSP budur. Evet, belirli tasarım modellerinde örtüşmeler var. İkinci olarak, yukarıdaki cevap yalnızca LSP'nin bir bankacılık uygulaması için bir üretim sisteminde nasıl uygulandığını göstermektir. Niyetim, LSP'nin nasıl kırılabileceğini ve nasıl düzeltileceğini göstermekti - bu bir eğitim öğreticisi olurdu ve bunlardan 100'ünü web'de bulabilirsiniz.
Yawar Murtaza

0

İşte Liskov İkame İlkesini uygulama kodu.

public abstract class Fruit
{
    public abstract string GetColor();
}

public class Orange : Fruit
{
    public override string GetColor()
    {
        return "Orange Color";
    }
}

public class Apple : Fruit
{
    public override string GetColor()
    {
        return "Red color";
    }
}

class Program
{
    static void Main(string[] args)
    {
        Fruit fruit = new Orange();

        Console.WriteLine(fruit.GetColor());

        fruit = new Apple();

        Console.WriteLine(fruit.GetColor());
    }
}

LSV şunu belirtir: "Türetilmiş sınıflar, temel sınıfları (veya arayüzleri) için ikame edilebilir olmalıdır" & "Temel sınıflara (veya arayüzlere) referanslar kullanan yöntemler, bunları bilmeden veya ayrıntıları bilmeden türetilmiş sınıfların yöntemlerini kullanabilmelidir. . "

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.