คุณสามารถอธิบาย Liskov Substitution Principle (The 'L' of SOLID) ด้วยตัวอย่าง C # ที่ดีซึ่งครอบคลุมทุกแง่มุมของหลักการในรูปแบบที่เรียบง่ายได้หรือไม่? ถ้าเป็นไปได้จริงๆ.
คุณสามารถอธิบาย Liskov Substitution Principle (The 'L' of SOLID) ด้วยตัวอย่าง C # ที่ดีซึ่งครอบคลุมทุกแง่มุมของหลักการในรูปแบบที่เรียบง่ายได้หรือไม่? ถ้าเป็นไปได้จริงๆ.
คำตอบ:
(คำตอบนี้ถูกเขียนใหม่เมื่อ 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
ปรับแต่งบางส่วนใหม่เพื่อให้ชัดเจนยิ่งขึ้น
if duck is ElectricDuck
ส่วนนี้ ฉันมีงานสัมมนาเกี่ยวกับ SOLID เมื่อวันพฤหัสบดีที่ผ่านมา :)
as
คีย์เวิร์ดซึ่งช่วยประหยัดได้จากการตรวจสอบประเภทจำนวนมาก ฉันกำลังคิดบางอย่างดังนี้if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
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).
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
นี่คือรหัสสำหรับการใช้ 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: "คลาสที่ได้รับควรสามารถใช้แทนคลาสพื้นฐาน (หรืออินเทอร์เฟซ) ได้" & "วิธีการที่ใช้การอ้างอิงไปยังคลาสพื้นฐาน (หรืออินเทอร์เฟซ) ต้องสามารถใช้วิธีการของคลาสที่ได้รับโดยที่ไม่รู้เรื่องหรือไม่ทราบรายละเอียด .”