Bisakah Anda menjelaskan Prinsip Substitusi Liskov dengan contoh C # yang baik? [Tutup]

93

Dapatkah Anda menjelaskan Prinsip Substitusi Liskov ('L' dari SOLID) dengan contoh C # yang mencakup semua aspek prinsip dengan cara yang disederhanakan? Jika memang mungkin.

pencilCake
sumber
9
Berikut adalah cara berpikir yang disederhanakan tentangnya secara singkat: Jika saya mengikuti LSP, saya dapat mengganti objek apa pun dalam kode saya dengan objek Mock, dan tidak ada apa pun dalam kode panggilan yang perlu disesuaikan atau diubah untuk memperhitungkan substitusi. LSP adalah dukungan fundamental untuk pola Test by Mock.
kmote
Ada beberapa contoh kesesuaian dan pelanggaran dalam jawaban ini
StuartLC

Jawaban:

128

(Jawaban ini sudah ditulis ulang 2013-05-13, baca pembahasannya di bawah komentar)

LSP adalah tentang mengikuti kontrak kelas dasar.

Anda dapat misalnya tidak membuang pengecualian baru di sub kelas karena yang menggunakan kelas dasar tidak akan mengharapkannya. Hal yang sama berlaku jika kelas dasar melempar ArgumentNullExceptionjika ada argumen yang hilang dan sub kelas memungkinkan argumen menjadi null, juga pelanggaran LSP.

Berikut adalah contoh struktur kelas yang melanggar LSP:

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

Dan kode panggilannya

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

Seperti yang Anda lihat, ada dua contoh bebek. Satu bebek organik dan satu bebek listrik. Bebek listrik hanya bisa berenang jika dihidupkan. Ini melanggar prinsip LSP karena harus dinyalakan untuk bisa berenang karena IsSwimming(yang juga merupakan bagian dari kontrak) tidak akan diatur seperti di kelas dasar.

Anda tentu saja dapat menyelesaikannya dengan melakukan hal seperti ini

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

Tapi itu akan merusak prinsip Open / Closed dan harus diterapkan di mana-mana (dan karenanya masih menghasilkan kode yang tidak stabil).

Solusi yang tepat adalah menyalakan bebek secara otomatis dalam Swimmetode tersebut dan dengan demikian membuat bebek listrik berperilaku persis seperti yang ditentukan oleh IDuckantarmuka

Memperbarui

Seseorang menambahkan komentar dan menghapusnya. Itu memiliki poin valid yang ingin saya sampaikan:

Solusi dengan menyalakan bebek di dalam Swimmetode dapat memiliki efek samping saat bekerja dengan implementasi aktual ( ElectricDuck). Tapi itu bisa diatasi dengan menggunakan implementasi antarmuka eksplisit . imho kemungkinan besar Anda mendapatkan masalah dengan TIDAK menyalakannya Swimkarena diharapkan itu akan berenang saat menggunakan IDuckantarmuka

Perbarui 2

Diulang beberapa bagian agar lebih jelas.

jgauffin.dll
sumber
1
@jgauffin: Contoh sederhana dan jelas. Tetapi solusi yang Anda usulkan, pertama: melanggar Prinsip Tertutup Terbuka dan tidak sesuai dengan definisi Paman Bob (lihat bagian kesimpulan dari artikelnya) yang menulis: "Prinsip Substitusi Liskov (Desain AKA dengan Kontrak) adalah fitur penting dari semua program yang sesuai dengan prinsip Terbuka-Tertutup. " lihat: objectmentor.com/resources/articles/lsp.pdf
pencilCake
1
Saya tidak melihat bagaimana solusinya rusak Terbuka / Tertutup. Baca jawaban saya lagi jika Anda mengacu pada if duck is ElectricDuckbagian itu. Saya memiliki seminar tentang SOLID Kamis lalu :)
jgauffin
Tidak benar-benar pada topik, tetapi bisakah Anda mengubah contoh Anda sehingga Anda tidak melakukan pengecekan tipe dua kali? Banyak pengembang tidak mengetahui askata kunci, yang sebenarnya menyelamatkan mereka dari banyak pemeriksaan jenis. Saya sedang memikirkan sesuatu seperti berikut:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
Siewers
3
@jgauffin - Saya agak bingung dengan contohnya. Saya pikir Prinsip Pergantian Liskov akan tetap berlaku dalam kasus ini karena Duck dan ElectricDuck keduanya berasal dari IDuck dan Anda dapat meletakkan ElectricDuck atau Duck di mana pun IDuck digunakan. Jika ElectricDuck harus menyalakan sebelum bebek bisa berenang, bukankah itu tanggung jawab ElectricDuck atau kode yang membuat ElectricDuck dan kemudian menyetel properti IsTurnedOn ke true. Jika ini melanggar LSP, tampaknya LSV akan sangat sulit untuk dipatuhi karena semua antarmuka akan berisi logika yang berbeda untuk metodenya.
Xaisoft
1
@MystereMan: imho LSP adalah tentang kebenaran perilaku. Dengan contoh persegi panjang / persegi Anda mendapatkan efek samping dari properti lain yang sedang disetel. Dengan bebek Anda mendapatkan efek samping dari tidak berenang. 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 merupakan Pendekatan Praktis

Di mana-mana saya mencari contoh C # LSP, orang telah menggunakan kelas dan antarmuka imajiner. Berikut adalah implementasi praktis LSP yang saya terapkan di salah satu sistem kami.

Skenario: Misalkan kita memiliki 3 database (Nasabah Hipotek, Nasabah Giro dan Nasabah Tabungan) yang menyediakan data nasabah dan kita membutuhkan detail nasabah untuk diberikan nama belakang nasabah. Sekarang kami mungkin mendapatkan lebih dari 1 detail pelanggan dari 3 database tersebut terhadap nama belakang yang diberikan.

Penerapan:

LAPISAN MODEL BISNIS:

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

LAYER AKSES DATA:

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

Antarmuka di atas diimplementasikan oleh kelas abstrak

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

Kelas abstrak ini memiliki metode umum "GetDetails" untuk ketiga database yang diperluas oleh masing-masing kelas database seperti yang ditunjukkan di bawah ini

AKSES DATA PELANGGAN MORTGAGE:

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

AKSES DATA PELANGGAN AKUN SAAT INI:

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

AKSES DATA PELANGGAN REKENING TABUNGAN:

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

Setelah 3 kelas akses data ini ditetapkan, sekarang kami menarik perhatian kami ke klien. Di lapisan Bisnis kami memiliki kelas CustomerServiceManager yang mengembalikan detail pelanggan ke kliennya.

LAPISAN BISNIS:

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

Saya belum menunjukkan injeksi ketergantungan agar tetap sederhana karena sekarang sudah semakin rumit.

Sekarang jika kita memiliki database detail pelanggan baru, kita bisa menambahkan kelas baru yang memperluas BaseDataAccess dan menyediakan objek database-nya.

Tentu saja kita membutuhkan prosedur tersimpan yang identik di semua database yang berpartisipasi.

Terakhir, klien untuk CustomerServiceManagerkelas hanya akan memanggil metode GetCustomerDetails, meneruskan lastName dan tidak peduli tentang bagaimana dan dari mana data itu berasal.

Semoga ini akan memberi Anda pendekatan praktis untuk memahami LSP.

Yawar Murtaza
sumber
3
Bagaimana ini bisa menjadi contoh LSP?
somegeek
1
Saya juga tidak melihat contoh LSP dalam hal itu ... Mengapa ada begitu banyak suara positif?
StaNov
1
@RoshanGhangare IDataAccess memiliki 3 implementasi konkret yang dapat disubstitusikan di Business Layer.
Yawar Murtaza
1
@YawarMurtaza apa pun contoh yang Anda kutip adalah penerapan tipikal pola strategi itu saja. Bisakah Anda jelaskan di mana ia melanggar LSP dan bagaimana Anda menyelesaikan pelanggaran LSP itu
Yogesh
@Yogesh - Anda dapat menukar implementasi IDataAccess dengan salah satu kelas konkritnya dan itu tidak akan mempengaruhi kode klien - intinya LSP. Ya, ada tumpang tindih dalam pola desain tertentu. Kedua, jawaban di atas hanya untuk menunjukkan bagaimana LSP diimplementasikan dalam sistem produksi untuk aplikasi perbankan. Maksud saya bukan untuk menunjukkan bagaimana LSP dapat rusak dan bagaimana memperbaikinya - itu akan menjadi tutorial pelatihan dan Anda dapat menemukan 100 di antaranya di web.
Yawar Murtaza
0

Berikut kode untuk menerapkan Prinsip Pengganti Liskov.

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 menyatakan: "Kelas turunan harus dapat diganti dengan kelas dasar (atau antarmuka)" & "Metode yang menggunakan referensi ke kelas dasar (atau antarmuka) harus dapat menggunakan metode kelas turunan tanpa mengetahuinya atau mengetahui detailnya . "

mark333 ... 333 ... 333
sumber