คุณสามารถอธิบาย Liskov Substitution Principle ด้วยตัวอย่าง C # ที่ดีได้หรือไม่? [ปิด]


93

คุณสามารถอธิบาย Liskov Substitution Principle (The 'L' of SOLID) ด้วยตัวอย่าง C # ที่ดีซึ่งครอบคลุมทุกแง่มุมของหลักการในรูปแบบที่เรียบง่ายได้หรือไม่? ถ้าเป็นไปได้จริงๆ.


9
ต่อไปนี้เป็นวิธีคิดที่ง่ายขึ้นโดยสรุป: หากฉันติดตาม LSP ฉันสามารถแทนที่วัตถุใด ๆ ในรหัสของฉันด้วยวัตถุจำลองและไม่มีสิ่งใดในรหัสการโทรที่จะต้องได้รับการปรับหรือเปลี่ยนเป็นบัญชีสำหรับการทดแทน LSP เป็นการสนับสนุนขั้นพื้นฐานสำหรับรูปแบบ Test by Mock
kmote

มีตัวอย่างเพิ่มเติมเกี่ยวกับความสอดคล้องและการละเมิดในคำตอบนี้
StuartLC

คำตอบ:


128

(คำตอบนี้ถูกเขียนใหม่เมื่อ 2013-05-13 อ่านการสนทนาด้านล่างของความคิดเห็น)

LSP เป็นเรื่องเกี่ยวกับการทำตามสัญญาของคลาสพื้นฐาน

ตัวอย่างเช่นคุณไม่สามารถโยนข้อยกเว้นใหม่ในคลาสย่อยได้เนื่องจากคลาสพื้นฐานจะไม่คาดหวังเช่นนั้น เช่นเดียวกันถ้าคลาสพื้นฐานพ่นArgumentNullExceptionหากอาร์กิวเมนต์ขาดหายไปและคลาสย่อยอนุญาตให้อาร์กิวเมนต์เป็นโมฆะนอกจากนี้ยังเป็นการละเมิด LSP

นี่คือตัวอย่างโครงสร้างคลาสที่ละเมิด 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; } }
}

และรหัสโทร

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

อย่างที่คุณเห็นมีสองตัวอย่างของเป็ด เป็ดอินทรีย์หนึ่งตัวและเป็ดไฟฟ้าหนึ่งตัว เป็ดไฟฟ้าจะว่ายน้ำได้ก็ต่อเมื่อเปิดเครื่องเท่านั้น สิ่งนี้ผิดหลักการ LSP เนื่องจากต้องเปิดเพื่อให้สามารถว่ายน้ำได้IsSwimming (ซึ่งเป็นส่วนหนึ่งของสัญญา) จะไม่ถูกตั้งค่าไว้ในคลาสพื้นฐาน

แน่นอนคุณสามารถแก้ได้โดยทำสิ่งนี้

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

แต่นั่นจะทำลายหลักการเปิด / ปิดและต้องนำไปใช้ทุกที่ (และยังคงสร้างรหัสที่ไม่เสถียร)

วิธีแก้ปัญหาที่เหมาะสมคือการเปิดเป็ดโดยอัตโนมัติในSwimวิธีการและการทำเช่นนั้นทำให้เป็ดไฟฟ้าทำงานตามที่กำหนดโดยIDuckอินเทอร์เฟซ

อัปเดต

มีคนเพิ่มความคิดเห็นและลบออก มีประเด็นที่ถูกต้องที่ฉันต้องการกล่าวถึง:

วิธีแก้ปัญหาด้วยการเปิดเป็ดในSwimวิธีนี้อาจมีผลข้างเคียงเมื่อทำงานกับการใช้งานจริง ( ElectricDuck) แต่ที่จะสามารถแก้ไขได้โดยการใช้ใช้อินเตอร์เฟซที่ชัดเจน มีโอกาสมากกว่าที่คุณจะประสบปัญหาโดยการไม่เปิดเครื่องSwimเนื่องจากคาดว่ามันจะว่ายน้ำเมื่อใช้IDuckอินเทอร์เฟซ

อัปเดต 2

ปรับแต่งบางส่วนใหม่เพื่อให้ชัดเจนยิ่งขึ้น


1
@jgauffin: ตัวอย่างง่ายและชัดเจน แต่วิธีแก้ปัญหาที่คุณเสนอประการแรก: ทำลายหลักการแบบเปิด - ปิดและไม่เข้ากับคำจำกัดความของลุงบ๊อบ (ดูส่วนสรุปของบทความของเขา) ซึ่งเขียนว่า: "หลักการทดแทน Liskov (AKA Design by Contract) เป็นคุณลักษณะที่สำคัญ ของโปรแกรมทั้งหมดที่เป็นไปตามหลักการเปิด - ปิด " ดู: objectmentor.com/resources/articles/lsp.pdf
pencilCake

1
ฉันไม่เห็นวิธีการแก้ปัญหาเปิด / ปิด อ่านคำตอบของฉันอีกครั้งหากคุณกำลังอ้างถึงif duck is ElectricDuckส่วนนี้ ฉันมีงานสัมมนาเกี่ยวกับ SOLID เมื่อวันพฤหัสบดีที่ผ่านมา :)
jgauffin

ไม่ได้อยู่ในหัวข้อ แต่คุณช่วยเปลี่ยนตัวอย่างของคุณเพื่อที่จะไม่ทำการตรวจสอบประเภทซ้ำสองครั้งได้หรือไม่? นักพัฒนาจำนวนมากไม่ทราบถึงasคีย์เวิร์ดซึ่งช่วยประหยัดได้จากการตรวจสอบประเภทจำนวนมาก ฉันกำลังคิดบางอย่างดังนี้if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
Siewers

3
@jgauffin - ฉันสับสนเล็กน้อยกับตัวอย่าง ฉันคิดว่าหลักการเปลี่ยนตัว Liskov จะยังคงใช้ได้ในกรณีนี้เนื่องจากทั้ง Duck และ ElectricDuck ได้มาจาก IDuck และคุณสามารถใส่ ElectricDuck หรือ Duck ได้ทุกที่ที่ใช้ IDuck หาก ElectricDuck ต้องเปิดก่อนที่เป็ดจะว่ายน้ำได้นั่นไม่ใช่ความรับผิดชอบของ ElectricDuck หรือโค้ดบางตัวที่สร้างอินสแตนซ์ ElectricDuck แล้วตั้งค่าคุณสมบัติ IsTurnedOn เป็นจริง หากสิ่งนี้ละเมิด LSP ดูเหมือนว่า LSV จะยากมากที่จะปฏิบัติตามเนื่องจากอินเทอร์เฟซทั้งหมดจะมีตรรกะที่แตกต่างกันสำหรับวิธีการดังกล่าว
Xaisoft

1
@MystereMan: imho LSP เป็นข้อมูลเกี่ยวกับความถูกต้องของพฤติกรรม ด้วยตัวอย่างสี่เหลี่ยมผืนผ้า / สี่เหลี่ยมคุณจะได้รับผลข้างเคียงของคุณสมบัติอื่นที่กำลังตั้งค่า ด้วยเป็ดคุณจะได้รับผลข้างเคียงจากการว่ายน้ำไม่เป็น 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 แนวทางปฏิบัติ

ทุกที่ที่ฉันมองหาตัวอย่าง C # ของ LSP มีคนใช้คลาสและอินเทอร์เฟซในจินตนาการ นี่คือการนำ LSP ไปใช้งานจริงที่ฉันนำมาใช้ในระบบของเรา

สถานการณ์สมมติ: สมมติว่าเรามีฐานข้อมูล 3 แห่ง (ลูกค้าสินเชื่อที่อยู่อาศัยลูกค้าบัญชีกระแสรายวันและลูกค้าบัญชีออมทรัพย์) ที่ให้ข้อมูลลูกค้าและเราต้องการรายละเอียดของลูกค้าสำหรับนามสกุลของลูกค้าที่ระบุ ตอนนี้เราอาจได้รับรายละเอียดลูกค้ามากกว่า 1 รายจากฐานข้อมูลทั้ง 3 ฐานข้อมูลเทียบกับนามสกุลที่ระบุ

การนำไปใช้:

ชั้นวางโมเดลธุรกิจ:

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

ชั้นการเข้าถึงข้อมูล:

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

อินเทอร์เฟซด้านบนถูกนำไปใช้โดยคลาสนามธรรม

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

คลาสนามธรรมนี้มีเมธอดทั่วไป "GetDetails" สำหรับฐานข้อมูลทั้ง 3 ซึ่งขยายโดยแต่ละคลาสฐานข้อมูลดังที่แสดงด้านล่าง

การเข้าถึงข้อมูลลูกค้า MORTGAGE:

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

การเข้าถึงข้อมูลลูกค้าในบัญชีปัจจุบัน:

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

การบันทึกการเข้าถึงข้อมูลของลูกค้า:

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

เมื่อตั้งค่าคลาสการเข้าถึงข้อมูลทั้ง 3 นี้แล้วตอนนี้เราจะดึงความสนใจของเราไปที่ไคลเอนต์ ในชั้นธุรกิจเรามีคลาส CustomerServiceManager ที่ส่งคืนรายละเอียดของลูกค้าให้กับลูกค้า

ชั้นธุรกิจ:

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

ฉันไม่ได้แสดงการฉีดพึ่งพาเพื่อให้มันง่ายขึ้นเพราะตอนนี้มันเริ่มซับซ้อนแล้ว

ตอนนี้ถ้าเรามีฐานข้อมูลรายละเอียดลูกค้าใหม่เราสามารถเพิ่มคลาสใหม่ที่ขยาย BaseDataAccess และจัดเตรียมวัตถุฐานข้อมูลได้

แน่นอนว่าเราต้องการขั้นตอนการจัดเก็บที่เหมือนกันในฐานข้อมูลที่เข้าร่วมทั้งหมด

สุดท้ายไคลเอ็นต์สำหรับCustomerServiceManagerคลาสจะเรียกใช้เมธอด GetCustomerDetails ส่งผ่าน lastName และไม่ควรสนใจว่าข้อมูลมาจากไหนและอย่างไร

หวังว่านี่จะเป็นแนวทางปฏิบัติในการทำความเข้าใจ LSP


3
นี่เป็นตัวอย่างของ LSP ได้อย่างไร?
somegeek

1
ฉันไม่เห็นตัวอย่าง LSP ในนั้น ... เหตุใดจึงมีการโหวตเพิ่มจำนวนมาก
StaNov

1
@RoshanGhangare IDataAccess มีการใช้งานที่เป็นรูปธรรม 3 แบบซึ่งสามารถทดแทนได้ใน Business Layer
Yawar Murtaza

1
@YawarMurtaza สิ่งที่คุณยกตัวอย่างมาคือการนำรูปแบบกลยุทธ์ไปใช้โดยทั่วไปนั่นแหล่ะ คุณช่วยบอกให้ชัดเจนได้ไหมว่ามันทำลาย LSP ตรงไหนและคุณแก้ปัญหาการละเมิด LSP ได้อย่างไร
Yogesh

@Yogesh - คุณสามารถสลับการใช้งาน IDataAccess กับคลาสคอนกรีตใด ๆ และจะไม่มีผลกับรหัสไคลเอนต์นั่นคือสิ่งที่ LSP สรุป ใช่มีการซ้อนทับกันในรูปแบบการออกแบบบางอย่าง ประการที่สองคำตอบข้างต้นเป็นเพียงการแสดงให้เห็นว่า LSP ถูกนำไปใช้ในระบบการผลิตสำหรับแอปพลิเคชันธนาคารอย่างไร ความตั้งใจของฉันไม่ได้ต้องการแสดงให้เห็นว่า LSP สามารถเสียได้อย่างไรและจะแก้ไขได้อย่างไรนั่นจะเป็นแบบฝึกหัดการฝึกอบรมและคุณสามารถค้นหา 100 รายการได้จากเว็บ
Yawar Murtaza

0

นี่คือรหัสสำหรับการใช้ Liskov Substitute Principle

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: "คลาสที่ได้รับควรสามารถใช้แทนคลาสพื้นฐาน (หรืออินเทอร์เฟซ) ได้" & "วิธีการที่ใช้การอ้างอิงไปยังคลาสพื้นฐาน (หรืออินเทอร์เฟซ) ต้องสามารถใช้วิธีการของคลาสที่ได้รับโดยที่ไม่รู้เรื่องหรือไม่ทราบรายละเอียด .”

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.