Bạn có thể giải thích Nguyên tắc thay thế Liskov bằng một ví dụ C # hay không? [đóng cửa]


92

Bạn có thể giải thích Nguyên tắc thay thế Liskov (Chữ 'L' của SOLID) bằng một ví dụ C # hay bao gồm tất cả các khía cạnh của nguyên tắc một cách đơn giản không? Nếu nó thực sự có thể.


9
Tóm lại, đây là một cách đơn giản hóa để suy nghĩ về nó: Nếu tôi làm theo LSP, tôi có thể thay thế bất kỳ đối tượng nào trong mã của mình bằng đối tượng Mock và không có gì trong mã gọi sẽ cần được điều chỉnh hoặc thay đổi để giải thích cho sự thay thế. LSP là một hỗ trợ cơ bản cho mẫu Test by Mock.
kmote

Có một số ví dụ khác về sự phù hợp và vi phạm trong câu trả lời này
StuartLC

Câu trả lời:


128

(Câu trả lời này đã được viết lại 2013-05-13, hãy đọc phần thảo luận ở phần dưới cùng của ý kiến)

LSP là về việc tuân theo hợp đồng của lớp cơ sở.

Ví dụ, bạn có thể không ném các ngoại lệ mới trong các lớp con vì lớp sử dụng lớp cơ sở sẽ không mong đợi điều đó. Tương tự đối với trường hợp lớp cơ sở ném ra ArgumentNullExceptionnếu một đối số bị thiếu và lớp con cho phép đối số trống, cũng là một vi phạm LSP.

Đây là một ví dụ về cấu trúc lớp vi phạm 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; } }
}

Và mã gọi

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

Như bạn có thể thấy, có hai ví dụ về vịt. Một con vịt hữu cơ và một con vịt điện. Vịt điện chỉ có thể bơi nếu nó được bật. Điều này phá vỡ nguyên tắc LSP vì nó phải được bật để có thể bơi vì IsSwimming(cũng là một phần của hợp đồng) sẽ không được đặt như trong lớp cơ sở.

Tất nhiên bạn có thể giải quyết nó bằng cách làm như thế này

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

Nhưng điều đó sẽ phá vỡ nguyên tắc Mở / Đóng và phải được thực hiện ở mọi nơi (và do đó vẫn tạo ra mã không ổn định).

Giải pháp thích hợp sẽ là tự động bật vịt trong Swimphương pháp này và bằng cách làm như vậy, vịt điện hoạt động chính xác như được xác định bởi IDuckgiao diện

Cập nhật

Ai đó đã thêm nhận xét và xóa nó. Nó có một điểm hợp lệ mà tôi muốn giải quyết:

Giải pháp quay vịt bên trong Swimphương pháp có thể có tác dụng phụ khi làm việc với việc triển khai thực tế ( ElectricDuck). Nhưng điều đó có thể được giải quyết bằng cách sử dụng triển khai giao diện rõ ràng . imho có nhiều khả năng bạn gặp sự cố bằng cách KHÔNG bật nó lên Swimvì có thể nó sẽ bơi khi sử dụng IDuckgiao diện

Cập nhật 2

Diễn đạt lại một số phần để làm rõ hơn.


1
@jgauffin: Ví dụ rất đơn giản và rõ ràng. Nhưng giải pháp mà bạn đề xuất, trước tiên: phá vỡ Nguyên tắc Đóng mở và nó không phù hợp với định nghĩa của Uncle Bob (xem phần kết luận của bài báo của ông) viết: "Nguyên tắc Thay thế Liskov (AKA Design by Contract) là một đặc điểm quan trọng của tất cả các chương trình tuân theo nguyên tắc Đóng mở. " xem: objectmentor.com/resources/articles/lsp.pdf
pencilCake

1
Tôi không thấy cách giải pháp ngắt Mở / Đóng. Đọc lại câu trả lời của tôi nếu bạn đang tham khảo if duck is ElectricDuckphần này. Tôi đã có một cuộc hội thảo về RẮN thứ Năm tuần trước :)
jgauffin

Không thực sự về chủ đề, nhưng bạn có thể vui lòng thay đổi ví dụ của mình để bạn không thực hiện việc đánh máy hai lần không? Rất nhiều nhà phát triển không biết về astừ khóa, điều này thực sự giúp họ không phải kiểm tra nhiều kiểu. Tôi đang nghĩ điều gì đó như sau:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
Siewers

3
@jgauffin - Tôi hơi bối rối trước ví dụ này. Tôi nghĩ Nguyên tắc thay thế Liskov sẽ vẫn có hiệu lực trong trường hợp này vì Duck và ElectricDuck đều bắt nguồn từ IDuck và bạn có thể đặt ElectricDuck hoặc Duck ở bất kỳ nơi nào IDuck được sử dụng. Nếu ElectricDuck phải bật trước khi vịt có thể bơi, đó không phải là trách nhiệm của ElectricDuck hoặc một số mã khởi tạo ElectricDuck và sau đó đặt thuộc tính IsTurnedOn thành true. Nếu điều này vi phạm LSP, có vẻ như LSV sẽ rất khó tuân thủ vì tất cả các giao diện sẽ chứa các logic khác nhau cho các phương thức của nó.
Xaisoft

1
@MystereMan: imho LSP là tất cả về tính đúng đắn của hành vi. Với ví dụ hình chữ nhật / hình vuông, bạn nhận được hiệu ứng phụ của thuộc tính khác đang được thiết lập. Với con vịt, bạn sẽ nhận được tác dụng phụ là nó không bơi. 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

8

LSP một phương pháp tiếp cận thực tế

Ở mọi nơi tôi tìm kiếm các ví dụ C # của LSP, mọi người đã sử dụng các lớp và giao diện tưởng tượng. Đây là cách triển khai thực tế của LSP mà tôi đã triển khai trong một trong các hệ thống của chúng tôi.

Tình huống: Giả sử chúng ta có 3 cơ sở dữ liệu (Khách hàng thế chấp, Khách hàng Tài khoản vãng lai và Khách hàng Tài khoản Tiết kiệm) cung cấp dữ liệu khách hàng và chúng tôi cần thông tin chi tiết về họ của khách hàng. Bây giờ chúng tôi có thể nhận được nhiều hơn 1 thông tin chi tiết về khách hàng từ 3 cơ sở dữ liệu đó dựa trên họ đã cho.

Thực hiện:

TẦNG MÔ HÌNH KINH DOANH:

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

LỚP TRUY CẬP DỮ LIỆU:

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

Giao diện trên được thực hiện bởi lớp trừu tượng

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

Lớp trừu tượng này có một phương thức chung "GetDetails" cho cả 3 cơ sở dữ liệu được mở rộng bởi từng lớp cơ sở dữ liệu như hình dưới đây

THẾ CHẤP TRUY CẬP DỮ LIỆU KHÁCH HÀNG:

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

TRUY CẬP DỮ LIỆU KHÁCH HÀNG TÀI KHOẢN HIỆN TẠI:

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

TIẾT KIỆM TÀI KHOẢN TRUY CẬP DỮ LIỆU KHÁCH HÀNG:

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

Sau khi 3 lớp truy cập dữ liệu này được thiết lập, bây giờ chúng tôi hướng sự chú ý của chúng tôi đến máy khách. Trong lớp Kinh doanh, chúng ta có lớp CustomerServiceManager trả về chi tiết khách hàng cho các khách hàng của nó.

TẦNG KINH DOANH:

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

Tôi đã không hiển thị việc tiêm phụ thuộc để giữ cho nó đơn giản vì bây giờ nó đã trở nên phức tạp.

Bây giờ nếu chúng ta có một cơ sở dữ liệu chi tiết về khách hàng mới, chúng ta có thể thêm một lớp mới mở rộng BaseDataAccess và cung cấp đối tượng cơ sở dữ liệu của nó.

Tất nhiên chúng ta cần các thủ tục được lưu trữ giống hệt nhau trong tất cả các cơ sở dữ liệu tham gia.

Cuối cùng, máy khách cho CustomerServiceManagerlớp sẽ chỉ gọi phương thức GetCustomerDetails, truyền lastName và không quan tâm đến cách thức và nguồn dữ liệu đến từ đâu.

Hy vọng điều này sẽ cung cấp cho bạn một cách tiếp cận thực tế để hiểu LSP.


3
Làm thế nào đây có thể là ví dụ về LSP?
somegeek

1
Tôi cũng không thấy ví dụ về LSP trong đó ... Tại sao nó có quá nhiều lượt ủng hộ?
StaNov

1
@RoshanGhangare IDataAccess có 3 cách triển khai cụ thể có thể được thay thế trong Lớp kinh doanh.
Yawar Murtaza

1
@YawarMurtaza bất cứ điều gì bạn ví dụ mà bạn đã trích dẫn đều là cách triển khai điển hình của mô hình chiến lược đó là nó. Bạn có thể vui lòng làm rõ nơi nó đang vi phạm LSP và cách bạn giải quyết vi phạm LSP đó không
Yogesh

0

Đây là mã để áp dụng Nguyên tắc thay thế 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 tuyên bố: "Các lớp dẫn xuất phải có thể thay thế cho các lớp cơ sở (hoặc giao diện) của chúng" & "Các phương thức sử dụng tham chiếu đến các lớp cơ sở (hoặc giao diện) phải có thể sử dụng các phương thức của các lớp dẫn xuất mà không cần biết về nó hoặc biết chi tiết . "

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.