Một ví dụ về Nguyên tắc thay thế Liskov là gì?


908

Tôi đã nghe nói rằng Nguyên tắc thay thế Liskov (LSP) là một nguyên tắc cơ bản của thiết kế hướng đối tượng. Nó là gì và một số ví dụ về việc sử dụng nó là gì?


Thêm ví dụ về việc tuân thủ và vi phạm LSP tại đây
StuartLC

1
Câu hỏi này có vô số câu trả lời hay và quá rộng .
Raedwald

Câu trả lời:


892

Một ví dụ tuyệt vời minh họa LSP (được đưa ra bởi chú Bob trong một podcast tôi nghe gần đây) là đôi khi một thứ gì đó nghe có vẻ đúng trong ngôn ngữ tự nhiên không hoạt động hoàn toàn bằng mã.

Trong toán học, a Squarelà a Rectangle. Quả thực đó là một chuyên môn của một hình chữ nhật. "Là một" làm cho bạn muốn mô hình hóa điều này với sự kế thừa. Tuy nhiên, nếu trong mã bạn đã tạo Squarera từ Rectangleđó, thì Squarenên sử dụng bất cứ nơi nào bạn mong đợi a Rectangle. Điều này làm cho một số hành vi lạ.

Hãy tưởng tượng bạn đã có SetWidthSetHeightcác phương thức trên Rectanglelớp cơ sở của bạn ; Điều này có vẻ hoàn toàn hợp lý. Tuy nhiên, nếu Rectangletham chiếu của bạn chỉ đến a Square, thì SetWidthSetHeightkhông có ý nghĩa gì vì đặt cái này sẽ thay đổi cái kia để phù hợp với nó. Trong trường hợp này Squarethất bại trong Thử nghiệm thay thế Liskov Rectanglevà sự trừu tượng của việc Squarethừa kế từ đó Rectanglelà một điều xấu.

nhập mô tả hình ảnh ở đây

Bạn nên xem các Nguyên tắc RẮN nguyên tắc vô giá khác .


19
@ m-sharp Điều gì xảy ra nếu đó là một Hình chữ nhật bất biến sao cho thay vì SetWidth và SetHeight, chúng ta có các phương thức GetWidth và GetHeight thay thế?
Pacerier

139
Đạo đức của câu chuyện: mô hình hóa các lớp học của bạn dựa trên các hành vi không dựa trên các thuộc tính; mô hình hóa dữ liệu của bạn dựa trên các thuộc tính và không dựa trên các hành vi. Nếu nó cư xử như một con vịt, nó chắc chắn là một con chim.
Sklivvz

193
Chà, hình vuông rõ ràng là một loại hình chữ nhật trong thế giới thực. Việc chúng ta có thể mô hình hóa điều này trong mã của chúng ta hay không tùy thuộc vào thông số kỹ thuật. Điều mà LSP chỉ ra là hành vi kiểu con phải phù hợp với hành vi loại cơ sở như được định nghĩa trong đặc tả loại cơ sở. Nếu thông số loại cơ sở hình chữ nhật nói rằng chiều cao và chiều rộng có thể được đặt độc lập, thì LSP nói rằng hình vuông không thể là một kiểu con của hình chữ nhật. Nếu thông số hình chữ nhật nói rằng hình chữ nhật là bất biến, thì hình vuông có thể là một kiểu con của hình chữ nhật. Đó là tất cả về các kiểu con duy trì hành vi được chỉ định cho loại cơ sở.
SteveT

63
@Pacerier không có vấn đề gì nếu nó bất biến. Vấn đề thực sự ở đây là chúng ta không mô hình hóa các hình chữ nhật, mà là "hình chữ nhật có thể thay đổi được", tức là hình chữ nhật có chiều rộng hoặc chiều cao có thể được sửa đổi sau khi tạo (và chúng ta vẫn coi nó là cùng một đối tượng). Nếu chúng ta nhìn vào lớp hình chữ nhật theo cách này, rõ ràng hình vuông không phải là "hình chữ nhật có thể định hình lại", bởi vì hình vuông không thể được định hình lại và vẫn là hình vuông (nói chung). Về mặt toán học, chúng ta không nhìn thấy vấn đề bởi vì tính đột biến thậm chí không có ý nghĩa trong bối cảnh toán học.
asmeker

14
Tôi có một câu hỏi về nguyên tắc. Tại sao sẽ là vấn đề nếu Square.setWidth(int width)được thực hiện như thế này : this.width = width; this.height = width;? Trong trường hợp này, đảm bảo rằng chiều rộng bằng chiều cao.
MC Hoàng đế

488

Nguyên tắc thay thế Liskov (LSP, ) là một khái niệm trong Lập trình hướng đối tượng cho biết:

Các hàm sử dụng các con trỏ hoặc tham chiếu đến các lớp cơ sở phải có thể sử dụng các đối tượng của các lớp dẫn xuất mà không biết nó.

Trọng tâm của LSP là về các giao diện và hợp đồng cũng như cách quyết định khi nào nên mở rộng một lớp so với sử dụng một chiến lược khác như thành phần để đạt được mục tiêu của bạn.

Cách hiệu quả nhất mà tôi đã thấy để minh họa điểm này là trong Head First OOA & D . Họ trình bày một kịch bản mà bạn là nhà phát triển trong một dự án để xây dựng một khuôn khổ cho các trò chơi chiến lược.

Họ trình bày một lớp đại diện cho một bảng trông như thế này:

Sơ đồ lớp

Tất cả các phương thức lấy tọa độ X và Y làm tham số để xác định vị trí ô trong mảng hai chiều của Tiles. Điều này sẽ cho phép một nhà phát triển trò chơi quản lý các đơn vị trong bảng trong suốt quá trình của trò chơi.

Cuốn sách tiếp tục thay đổi các yêu cầu để nói rằng khung trò chơi cũng phải hỗ trợ các bảng trò chơi 3D để phù hợp với các trò chơi có chuyến bay. Vì vậy, một ThreeDBoardlớp học được giới thiệu mà mở rộng Board.

Thoạt nhìn đây có vẻ là một quyết định tốt. Boardcung cấp cả HeightWidthtài sản và ThreeDBoardcung cấp các trục Z.

Nơi nó tan vỡ là khi bạn nhìn vào tất cả các thành viên khác được thừa hưởng từ đó Board. Các phương pháp để AddUnit, GetTile, GetUnitsvà như vậy, tất cả mất cả X và các thông số Y trong Boardlớp nhưng ThreeDBoardcần một tham số Z là tốt.

Vì vậy, bạn phải thực hiện lại các phương thức đó với tham số Z. Tham số Z không có ngữ cảnh cho Boardlớp và các phương thức được kế thừa từ Boardlớp mất ý nghĩa của chúng. Một đơn vị mã cố gắng sử dụng ThreeDBoardlớp làm lớp cơ sở của nó Boardsẽ rất may mắn.

Có lẽ chúng ta nên tìm một cách tiếp cận khác. Thay vì mở rộng Board, ThreeDBoardnên bao gồm các Boardđối tượng. Một Boardđối tượng trên một đơn vị của trục Z.

Điều này cho phép chúng tôi sử dụng các nguyên tắc hướng đối tượng tốt như đóng gói và tái sử dụng và không vi phạm LSP.


10
Xem thêm Vấn đề Circle-Ellipse trên Wikipedia để biết ví dụ tương tự nhưng đơn giản hơn.
Brian

Yêu cầu từ @NotMySelf: "Tôi nghĩ ví dụ đơn giản là chứng minh rằng việc kế thừa từ bảng không có ý nghĩa gì trong bối cảnh ThreeDBoard và tất cả các chữ ký phương thức đều vô nghĩa với trục Z.".
Contango

1
Vì vậy, nếu chúng ta thêm một phương thức khác vào một lớp Con nhưng tất cả các chức năng của Parent vẫn có ý nghĩa trong lớp Con thì nó có phá vỡ LSP không? Vì một mặt, chúng tôi đã sửa đổi giao diện để sử dụng Con một chút, mặt khác, nếu chúng tôi đưa Con trở thành Cha mẹ, mã dự kiến ​​Cha mẹ sẽ hoạt động tốt.
Nickolay Kondratyev

5
Đây là một ví dụ chống Liskov. Liskov làm cho chúng ta lấy được hình chữ nhật từ Quảng trường. Nhiều tham số-lớp từ lớp ít tham số. Và bạn đã chỉ ra rằng nó là xấu. Đây thực sự là một trò đùa tốt khi được đánh dấu là một câu trả lời và đã được nâng cấp 200 lần một câu trả lời chống liskov cho câu hỏi liskov. Nguyên tắc Liskov là một ngụy biện thực sự?
Gangnus

3
Tôi đã thấy thừa kế làm việc sai cách. Đây là một ví dụ. Lớp cơ sở phải là 3DBoard và Board lớp dẫn xuất. Bảng vẫn có trục Z là Max (Z) = Min (Z) = 1
Paulustrious

169

Sự thay thế là một nguyên tắc trong lập trình hướng đối tượng nói rằng, trong một chương trình máy tính, nếu S là một kiểu con của T, thì các đối tượng thuộc loại T có thể được thay thế bằng các đối tượng loại S

hãy làm một ví dụ đơn giản trong Java:

Ví dụ xấu

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

Con vịt có thể bay vì nó là một con chim, nhưng điều này thì sao:

public class Ostrich extends Bird{}

Đà điểu là một con chim, nhưng nó không thể bay, lớp đà điểu là một kiểu con của lớp Chim, nhưng nó không thể sử dụng phương pháp bay, điều đó có nghĩa là chúng ta đang phá vỡ nguyên tắc LSP.

Ví dụ tốt

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 

3
Ví dụ đẹp, nhưng bạn sẽ làm gì nếu khách hàng có Bird bird. Bạn phải truyền đối tượng lên FlyingBirds để sử dụng bay, điều này không tốt phải không?
Tâm trạng

17
Không. Nếu khách hàng có Bird bird, điều đó có nghĩa là nó không thể sử dụng fly(). Đó là nó. Vượt qua một Duckkhông thay đổi thực tế này. Nếu khách hàng có FlyingBirds bird, thì ngay cả khi nó được thông qua, Ducknó vẫn luôn hoạt động theo cùng một cách.
Steve Chamaillard

9
Điều này cũng không phải là một ví dụ tốt cho Phân đoạn giao diện?
Saharsh

Ví dụ tuyệt vời Thanks Man
Abdelhadi Abdo

6
Còn về cách sử dụng Giao diện 'Flyable' (không thể nghĩ ra tên nào hay hơn). Bằng cách này, chúng ta không dấn thân vào hệ thống phân cấp cứng nhắc này .. Trừ khi chúng ta biết thực sự cần nó.
Thứ ba

132

LSP quan tâm bất biến.

Ví dụ cổ điển được đưa ra bởi khai báo mã giả sau đây (triển khai được bỏ qua):

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

Bây giờ chúng tôi có một vấn đề mặc dù giao diện phù hợp. Lý do là chúng tôi đã vi phạm bất biến xuất phát từ định nghĩa toán học của hình vuông và hình chữ nhật. Cách thức hoạt động của getters và setters, Rectanglenên đáp ứng các bất biến sau:

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

Tuy nhiên, bất biến này phải bị vi phạm bởi việc thực hiện đúng Square, do đó nó không phải là sự thay thế hợp lệ Rectangle.


35
Và do đó, khó khăn trong việc sử dụng "OO" để mô hình hóa bất cứ điều gì chúng ta có thể muốn thực hiện mô hình.
DrPizza

9
@DrPizza: Hoàn toàn đúng. Tuy nhiên, hai điều. Thứ nhất, các mối quan hệ như vậy vẫn có thể được mô hình hóa trong OOP, mặc dù không đầy đủ hoặc sử dụng các đường vòng phức tạp hơn (chọn bất kỳ phù hợp với vấn đề của bạn). Thứ hai, không có sự thay thế nào tốt hơn. Các ánh xạ / modell khác có cùng các vấn đề tương tự. ;-)
Konrad Rudolph

7
@NickW Trong một số trường hợp (nhưng không phải ở trên), bạn có thể đảo ngược chuỗi thừa kế - nói một cách logic, điểm 2D là điểm 3D, trong đó chiều thứ ba bị bỏ qua (hoặc 0 - tất cả các điểm nằm trên cùng một mặt phẳng trong Không gian 3D). Nhưng điều này tất nhiên là không thực tế. Nói chung, đây là một trong những trường hợp thừa kế không thực sự có ích và không có mối quan hệ tự nhiên nào tồn tại giữa các thực thể. Mô hình chúng một cách riêng biệt (ít nhất là tôi không biết cách nào tốt hơn).
Konrad Rudolph

7
OOP có nghĩa là mô hình hóa các hành vi và không phải dữ liệu. Các lớp học của bạn vi phạm đóng gói ngay cả trước khi vi phạm LSP.
Sklivvz

2
@AustinWBryan Yep; Tôi đã làm việc trong lĩnh vực này càng lâu, tôi càng có xu hướng sử dụng tính kế thừa cho các giao diện và các lớp cơ sở trừu tượng và thành phần cho phần còn lại. Đôi khi nó làm việc nhiều hơn một chút (gõ khôn ngoan) nhưng nó tránh được cả đống vấn đề, và được các lập trình viên có kinh nghiệm khác nhắc lại.
Konrad Rudolph

77

Robert Martin có một bài viết xuất sắc về Nguyên tắc thay thế Liskov . Nó thảo luận về những cách tinh tế và không tinh tế trong đó nguyên tắc có thể bị vi phạm.

Một số phần có liên quan của bài báo (lưu ý rằng ví dụ thứ hai được cô đọng nhiều):

Một ví dụ đơn giản về vi phạm LSP

Một trong những vi phạm rõ ràng nhất của nguyên tắc này là việc sử dụng Thông tin loại thời gian chạy (RTTI) của C ++ để chọn một chức năng dựa trên loại đối tượng. I E:

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

Rõ ràng DrawShapechức năng được hình thành xấu. Nó phải biết về mọi đạo hàm có thể có của Shapelớp và nó phải được thay đổi bất cứ khi nào các đạo hàm mới Shapeđược tạo ra. Thật vậy, nhiều người xem cấu trúc của chức năng này như là sự kết hợp với Thiết kế hướng đối tượng.

Hình vuông và hình chữ nhật, một vi phạm tinh vi hơn.

Tuy nhiên, có những cách khác, tinh vi hơn nhiều, vi phạm LSP. Hãy xem xét một ứng dụng sử dụng Rectanglelớp như được mô tả dưới đây:

class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};

[...] Hãy tưởng tượng rằng một ngày nào đó người dùng yêu cầu khả năng thao tác hình vuông ngoài hình chữ nhật. [...]

Rõ ràng, một hình vuông là một hình chữ nhật cho tất cả các mục đích và mục đích bình thường. Vì mối quan hệ ISA giữ, nên mô hình hóa Square lớp là bắt nguồn từ đó Rectangle. [...]

Squaresẽ kế thừa SetWidthvà các SetHeightchức năng. Các hàm này hoàn toàn không phù hợp với a Square, vì chiều rộng và chiều cao của hình vuông là giống hệt nhau. Đây phải là một đầu mối quan trọng rằng có một vấn đề với thiết kế. Tuy nhiên, có một cách để vượt qua vấn đề. Chúng tôi có thể ghi đè SetWidthSetHeight[...]

Nhưng hãy xem xét các chức năng sau:

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

Nếu chúng ta chuyển tham chiếu đến một Squaređối tượng vào hàm này, Squaređối tượng sẽ bị hỏng vì chiều cao sẽ không bị thay đổi. Đây là một vi phạm rõ ràng về LSP. Hàm này không hoạt động cho các dẫn xuất của các đối số của nó.

[...]


14
Tuy muộn nhưng tôi nghĩ đây là một câu trích dẫn thú vị trong bài báo đó: Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one. Nếu điều kiện trước của lớp trẻ mạnh hơn điều kiện trước của lớp cha mẹ, bạn không thể thay thế một đứa trẻ cho cha mẹ mà không vi phạm điều kiện trước. Do đó LSP.
2023861

@ user2023861 Bạn hoàn toàn đúng. Tôi sẽ viết một câu trả lời dựa trên điều này.
inf3rno

40

LSP là cần thiết khi một số mã nghĩ rằng nó đang gọi các phương thức của một loại Tvà có thể vô tình gọi các phương thức của một loại S, trong đó S extends T(tức là Sthừa kế, xuất phát từ hoặc là một kiểu con của siêu kiểu T).

Ví dụ, điều này xảy ra khi một hàm có tham số đầu vào loại T, được gọi (nghĩa là được gọi) với giá trị đối số của loại S. Hoặc, trong đó một định danh loại T, được gán một giá trị của loại S.

val id : T = new S() // id thinks it's a T, but is a S

LSP yêu cầu các kỳ vọng (tức là bất biến) cho các phương thức loại T(ví dụ Rectangle), không bị vi phạm khi các phương thức loại S(ví dụ Square) được gọi thay thế.

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

Ngay cả một loại với các trường bất biến vẫn có các bất biến, ví dụ: các setters hình chữ nhật bất biến mong muốn các kích thước được sửa đổi độc lập, nhưng các setters Square bất biến vi phạm kỳ vọng này.

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP yêu cầu mỗi phương thức của kiểu con Sphải có (các) tham số đầu vào chống chỉ định và đầu ra covariant.

Contravariant có nghĩa là phương sai trái với hướng của thừa kế, tức là kiểu Si, của từng tham số đầu vào của từng phương thức của kiểu con S, phải giống hoặc siêu kiểu Ticủa tham số đầu vào tương ứng của phương thức tương ứng của siêu kiểu T.

Hiệp phương sai có nghĩa là phương sai theo cùng một hướng của thừa kế, tức là kiểu So, đầu ra của mỗi phương thức của kiểu con S, phải giống hoặc một kiểu con của kiểu Tođầu ra tương ứng của phương thức siêu kiểu tương ứng T.

Điều này là do nếu người gọi nghĩ rằng nó có một kiểu T, nghĩ rằng nó đang gọi một phương thức T, thì nó cung cấp (các) đối số của kiểu Tivà gán đầu ra cho kiểu đó To. Khi nó thực sự gọi phương thức tương ứng S, thì mỗi Tiđối số đầu vào được gán cho một Sitham số đầu vào và Sođầu ra được gán cho kiểu To. Do đó, nếu Sikhông phải là conttvariant wrt Ti, thì một phân nhóm Xiconwhwhich sẽ không phải là một kiểu con của Sinhómcocould được gán cho Ti.

Ngoài ra, đối với các ngôn ngữ (ví dụ Scala hoặc Ceylon) có chú thích phương sai vị trí định nghĩa trên các tham số đa hình loại (ví dụ: tổng quát), đồng hướng hoặc đối lập của chú thích phương sai cho từng tham số loại Tphải ngược hoặc cùng hướng tương ứng với mọi tham số đầu vào hoặc đầu ra (của mọi phương thức T) có loại tham số loại.

Ngoài ra, đối với mỗi tham số đầu vào hoặc đầu ra có loại chức năng, hướng phương sai cần thiết được đảo ngược. Quy tắc này được áp dụng đệ quy.


Subtyping là thích hợp trong đó các bất biến có thể được liệt kê.

Có nhiều nghiên cứu đang diễn ra về cách mô hình hóa các bất biến, để chúng được thực thi bởi trình biên dịch.

Typestate (xem trang 3) tuyên bố và thực thi các bất biến trạng thái trực giao để gõ. Ngoài ra, bất biến có thể được thực thi bằng cách chuyển đổi các xác nhận thành các loại . Ví dụ: để xác nhận rằng một tệp đang mở trước khi đóng nó, thì File.open () có thể trả về loại OpenFile, chứa phương thức close () không có trong Tệp. Một API tic-tac-toe có thể là một ví dụ về sử dụng gõ để thực thi bất biến tại thời gian biên dịch. Hệ thống loại thậm chí có thể là Turing-Complete, ví dụ Scala . Các ngôn ngữ và định lý được gõ phụ thuộc chính thức hóa các mô hình gõ bậc cao hơn.

Do nhu cầu về ngữ nghĩa để trừu tượng hóa mở rộng , tôi hy vọng rằng việc sử dụng gõ để mô hình bất biến, tức là ngữ nghĩa biểu thị bậc cao thống nhất, vượt trội hơn so với typestate. "Mở rộng" có nghĩa là thành phần không giới hạn, được cho phép của sự phát triển mô đun, không phối hợp. Bởi vì dường như đối với tôi, đó là phản đề của sự hợp nhất và do đó mức độ tự do, để có hai mô hình phụ thuộc lẫn nhau (ví dụ như kiểu và Kiểu chữ) để thể hiện ngữ nghĩa được chia sẻ, không thể thống nhất với nhau về thành phần mở rộng . Ví dụ, phần mở rộng giống như Bài toán biểu thức đã được thống nhất trong phân nhóm, nạp chồng hàm và các miền gõ tham số.

Quan điểm lý thuyết của tôi là để kiến thức tồn tại (xem phần Tập trung hóa là mù và không phù hợp), sẽ không bao giờ có một mô hình chung có thể thực thi 100% tất cả các bất biến có thể có trong ngôn ngữ máy tính hoàn chỉnh Turing. Để kiến ​​thức tồn tại, nhiều khả năng bất ngờ tồn tại, tức là rối loạn và entropy phải luôn luôn tăng lên. Đây là lực entropic. Để chứng minh tất cả các tính toán có thể có của một phần mở rộng tiềm năng, là tính toán một ưu tiên tất cả các phần mở rộng có thể.

Đây là lý do tại sao Định lý dừng tồn tại, tức là không thể xác định được liệu mọi chương trình có thể có trong ngôn ngữ lập trình hoàn chỉnh Turing có chấm dứt hay không. Có thể chứng minh rằng một số chương trình cụ thể chấm dứt (một trong đó tất cả các khả năng đã được xác định và tính toán). Nhưng không thể chứng minh rằng tất cả các phần mở rộng có thể có của chương trình đó sẽ chấm dứt, trừ khi các khả năng mở rộng của chương trình đó không hoàn thành Turing (ví dụ: thông qua việc gõ phụ thuộc). Vì yêu cầu cơ bản cho tính đầy đủ của Turing là đệ quy không giới hạn , nên trực giác hiểu được các định lý không hoàn chỉnh của Gôdel và nghịch lý của Russell áp dụng cho việc mở rộng.

Việc giải thích các định lý này kết hợp chúng theo cách hiểu khái niệm khái quát về lực entropic:

  • Các định lý không hoàn chỉnh của Gôdel : bất kỳ lý thuyết chính thức nào, trong đó tất cả các sự thật số học có thể được chứng minh, đều không nhất quán.
  • Nghịch lý của Russell : mọi quy tắc thành viên cho một tập hợp có thể chứa một tập hợp, liệt kê loại cụ thể của từng thành viên hoặc chứa chính nó. Do đó, các tập hợp không thể được mở rộng hoặc chúng là đệ quy không giới hạn. Ví dụ: tập hợp tất cả mọi thứ không phải là ấm trà, bao gồm chính nó, bao gồm chính nó, bao gồm chính nó, vv. Do đó, một quy tắc không nhất quán nếu nó (có thể chứa một tập hợp và) không liệt kê các loại cụ thể (nghĩa là cho phép tất cả các loại không xác định) và không cho phép mở rộng không giới hạn. Đây là tập hợp các bộ không phải là thành viên của chính họ. Điều này không có khả năng phù hợp và được liệt kê hoàn toàn trên tất cả các phần mở rộng có thể, là các định lý không hoàn chỉnh của Gôdel.
  • Nguyên tắc thay thế Liskov : nói chung đó là một vấn đề không thể giải quyết được cho dù bất kỳ tập hợp nào là tập hợp con của một tập hợp khác, tức là sự kế thừa nói chung là không thể giải quyết được.
  • Tham khảo Linsky : không thể chắc chắn tính toán của một cái gì đó là gì, khi nó được mô tả hoặc nhận thức, tức là nhận thức (thực tế) không có điểm tham chiếu tuyệt đối.
  • Định lý của Coase : không có điểm tham chiếu bên ngoài, do đó, mọi rào cản đối với các khả năng bên ngoài không bị ràng buộc sẽ thất bại.
  • Định luật thứ hai của nhiệt động lực học : toàn bộ vũ trụ (một hệ thống khép kín, tức là mọi thứ) có xu hướng rối loạn tối đa, tức là khả năng độc lập tối đa.

17
@Shelyby: Bạn đã pha trộn quá nhiều thứ. Mọi thứ không khó hiểu như bạn nêu chúng. Phần lớn các xác nhận lý thuyết của bạn đứng trên cơ sở mỏng manh, như 'Để kiến ​​thức tồn tại, nhiều khả năng bất ngờ tồn tại, .........' VÀ 'nói chung, đó là một vấn đề không thể giải quyết được cho dù bất kỳ tập hợp nào là tập hợp con của một tập hợp khác, tức là thừa kế nói chung là không thể giải quyết được '. Bạn có thể bắt đầu một blog riêng cho từng điểm này. Dù sao, khẳng định và giả định của bạn rất đáng nghi ngờ. Người ta không được sử dụng những thứ mà người ta không biết!
aknon

1
@aknon Tôi có một blog giải thích những vấn đề này sâu hơn. Mô hình TOE của tôi về không thời gian vô hạn là tần số không giới hạn. Tôi không nhầm lẫn rằng hàm quy nạp đệ quy có giá trị bắt đầu đã biết với giới hạn kết thúc vô hạn hoặc hàm cưỡng chế có giá trị cuối không xác định và ràng buộc bắt đầu đã biết. Thuyết tương đối là vấn đề một khi đệ quy được đưa ra. Đây là lý do tại sao Turing hoàn thành tương đương với đệ quy không giới hạn .
Shelby Moore III

4
@ShelbyMooreIII Bạn đang đi quá nhiều hướng. Đây không phải là một câu trả lời.
Soldalma

1
@Soldalma đó là một câu trả lời. Bạn không thấy nó trong phần Trả lời. Của bạn là một bình luận bởi vì nó là trong phần bình luận.
Shelby Moore III

1
Giống như sự pha trộn của bạn với thế giới scala!
Ehsan M. Kermani

24

Tôi thấy hình chữ nhật và hình vuông trong mỗi câu trả lời và cách vi phạm LSP.

Tôi muốn chỉ ra cách LSP có thể được tuân thủ với một ví dụ trong thế giới thực:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

Thiết kế này phù hợp với LSP vì hành vi vẫn không thay đổi bất kể việc triển khai chúng tôi chọn sử dụng.

Và có, bạn có thể vi phạm LSP trong cấu hình này khi thực hiện một thay đổi đơn giản như vậy:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

Bây giờ các kiểu con không thể được sử dụng theo cùng một cách vì chúng không tạo ra kết quả tương tự nữa.


6
Ví dụ này không vi phạm LSP miễn là chúng tôi giới hạn ngữ nghĩa Database::selectQueryđể chỉ hỗ trợ tập hợp con SQL được hỗ trợ bởi tất cả các công cụ DB. Điều đó hầu như không thực tế ... Điều đó nói rằng, ví dụ vẫn dễ nắm bắt hơn hầu hết những người khác được sử dụng ở đây.
Palec

5
Tôi thấy câu trả lời này là dễ nhất để nắm bắt phần còn lại.
Malcolm Salvador

23

Có một danh sách kiểm tra để xác định xem bạn có vi phạm Liskov hay không.

  • Nếu bạn vi phạm một trong các mục sau -> bạn vi phạm Liskov.
  • Nếu bạn không vi phạm bất kỳ -> không thể kết luận bất cứ điều gì.

Danh mục:

  • Không có ngoại lệ mới nào được ném trong lớp dẫn xuất : Nếu lớp cơ sở của bạn đã ném ArgumentNullException thì các lớp con của bạn chỉ được phép ném ngoại lệ của loại ArgumentNullException hoặc bất kỳ ngoại lệ nào có nguồn gốc từ ArgumentNullException. Ném IndexOutOfRangeException là vi phạm Liskov.
  • Điều kiện trước không thể được tăng cường : Giả sử lớp cơ sở của bạn hoạt động với một thành viên int. Bây giờ loại phụ của bạn yêu cầu int phải tích cực. Điều này được củng cố các điều kiện trước, và bây giờ bất kỳ mã nào hoạt động hoàn hảo trước đây với ints âm bị phá vỡ.
  • Điều kiện hậu kỳ không thể bị suy yếu : Giả sử lớp cơ sở của bạn yêu cầu tất cả các kết nối đến cơ sở dữ liệu phải được đóng trước khi phương thức được trả về. Trong lớp con của bạn, bạn áp dụng phương thức đó và mở kết nối mở để sử dụng lại. Bạn đã làm suy yếu các điều kiện hậu của phương pháp đó.
  • Bất biến phải được bảo tồn : Hạn chế khó khăn và đau đớn nhất để thực hiện. Bất biến là một số thời gian ẩn trong lớp cơ sở và cách duy nhất để tiết lộ chúng là đọc mã của lớp cơ sở. Về cơ bản, bạn phải chắc chắn khi bạn ghi đè một phương thức, mọi thứ không thể thay đổi phải được giữ nguyên sau khi phương thức được ghi đè của bạn được thực thi. Điều tốt nhất tôi có thể nghĩ đến là thực thi các ràng buộc bất biến này trong lớp cơ sở nhưng điều đó sẽ không dễ dàng.
  • Ràng buộc lịch sử : Khi ghi đè một phương thức, bạn không được phép sửa đổi một thuộc tính không thể sửa đổi trong lớp cơ sở. Hãy xem các mã này và bạn có thể thấy Tên được xác định là không thể sửa đổi (bộ riêng) nhưng SubType giới thiệu phương thức mới cho phép sửa đổi nó (thông qua sự phản chiếu):

    public class SuperType
    {
        public string Name { get; private set; }
        public SuperType(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
    public class SubType : SuperType
    {
        public void ChangeName(string newName)
        {
            var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
        }
    }
    

Có 2 mục khác: Chống chỉ định của các đối số phương thứcHiệp phương sai của các kiểu trả về . Nhưng điều này là không thể đối với C # (Tôi là nhà phát triển C #) vì vậy tôi không quan tâm đến họ.

Tài liệu tham khảo:


Tôi cũng là nhà phát triển C # và tôi sẽ nói rằng tuyên bố cuối cùng của bạn không đúng với Visual Studio 2010, với khung .Net 4.0. Hiệp phương sai của các kiểu trả về cho phép loại trả về có nguồn gốc nhiều hơn so với những gì được xác định bởi giao diện. Ví dụ: Ví dụ: IEnumerable <T> (T là covariant) IEnumerator <T> (T is covariant) IQueryable <T> (T is covariant) IGrouping <TKey, TEuity> (TKey và TEuity là covariant) là contravariant) IEqualityComparer <T> (T là contravariant) IComparable <T> (T là contravariant) msdn.microsoft.com/en-us/library/dd233059(v=vs.100).aspx
LCarter

1
Câu trả lời tuyệt vời và tập trung (mặc dù các câu hỏi ban đầu là về các ví dụ nhiều hơn các quy tắc).
Mike

22

LSP là một quy tắc về hợp đồng của các nhóm: nếu một lớp cơ sở thỏa mãn hợp đồng, thì các lớp dẫn xuất LSP cũng phải thỏa mãn hợp đồng đó.

Trong giả trăn

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

thỏa mãn LSP nếu mỗi lần bạn gọi Foo trên một đối tượng Derogen, nó sẽ cho kết quả chính xác giống như gọi Foo trên một đối tượng Base, miễn là arg giống nhau.


9
Nhưng ... nếu bạn luôn có cùng một hành vi, vậy thì điểm có lớp dẫn xuất là gì?
Leonid

2
Bạn đã bỏ lỡ một điểm: đó là hành vi được quan sát tương tự . Ví dụ, bạn có thể thay thế một cái gì đó bằng hiệu suất O (n) bằng một cái gì đó tương đương về chức năng, nhưng với hiệu suất O (lg n). Hoặc bạn có thể thay thế một cái gì đó truy cập dữ liệu được triển khai bằng MySQL và thay thế nó bằng cơ sở dữ liệu trong bộ nhớ.
Charlie Martin

@Charlie Martin, mã hóa giao diện chứ không phải triển khai - Tôi khai thác điều đó. Điều này không phải là duy nhất đối với OOP; các ngôn ngữ chức năng như Clojure cũng thúc đẩy điều đó. Ngay cả về mặt Java hay C #, tôi nghĩ rằng việc sử dụng một giao diện thay vì sử dụng một lớp trừu tượng cộng với hệ thống phân cấp lớp sẽ là điều tự nhiên đối với các ví dụ mà bạn cung cấp. Python không được gõ mạnh và không thực sự có giao diện, ít nhất là không rõ ràng. Khó khăn của tôi là tôi đã làm OOP được vài năm mà không tuân thủ RẮN. Bây giờ tôi đi qua nó, nó có vẻ hạn chế và gần như tự mâu thuẫn.
Hamish Grubijan

Chà, bạn cần quay lại và xem bài báo gốc của Barbara. báo cáo-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps Nó không thực sự được nêu trong các giao diện và đó là một mối quan hệ logic giữ (hoặc không) trong bất kỳ ngôn ngữ lập trình có một số hình thức thừa kế.
Charlie Martin

1
@ HamishGrubijan Tôi không biết ai đã nói với bạn rằng Python không được gõ mạnh, nhưng họ đã nói dối bạn (và nếu bạn không tin tôi, hãy kích hoạt trình thông dịch Python và thử 2 + "2"). Có lẽ bạn nhầm lẫn "gõ mạnh" với "gõ tĩnh"?
asmeker

21

Dài truyện ngắn, chúng ta hãy rời hình chữ nhật hình chữ nhật và hình vuông vuông, ví dụ thực tế khi mở rộng một lớp cha mẹ, bạn phải hoặc là PRESERVE API mẹ chính xác hoặc để mở rộng nó.

Giả sử bạn có Kho lưu trữ vật phẩm cơ sở .

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

Và một lớp con mở rộng nó:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

Sau đó, bạn có thể có một Máy khách làm việc với API Base ItemRep repository và dựa vào nó.

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

Các LSP được chia khi thay thế cha mẹ lớp học với một phụ phá vỡ lớp hợp đồng của API .

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

Bạn có thể tìm hiểu thêm về cách viết phần mềm có thể bảo trì trong khóa học của tôi: https://www.udemy.com/enterprise-php/


20

Các hàm sử dụng các con trỏ hoặc tham chiếu đến các lớp cơ sở phải có thể sử dụng các đối tượng của các lớp dẫn xuất mà không biết nó.

Khi tôi lần đầu tiên đọc về LSP, tôi đã cho rằng điều này có nghĩa rất nghiêm ngặt, về cơ bản là đánh đồng nó với việc thực hiện giao diện và truyền kiểu an toàn. Điều đó có nghĩa là LSP được đảm bảo hoặc không được đảm bảo bởi chính ngôn ngữ. Ví dụ, theo nghĩa chặt chẽ này, ThreeDBoard chắc chắn có thể thay thế cho Board, theo như trình biên dịch có liên quan.

Sau khi đọc thêm về khái niệm mặc dù tôi thấy rằng LSP thường được hiểu rộng hơn thế.

Nói tóm lại, mã máy khách có nghĩa là "biết" rằng đối tượng đằng sau con trỏ là loại dẫn xuất chứ không phải loại con trỏ không bị hạn chế đối với loại an toàn. Tuân thủ LSP cũng có thể kiểm tra được thông qua việc thăm dò hành vi thực tế của các đối tượng. Đó là, kiểm tra tác động của các đối số trạng thái và phương thức của đối tượng đối với kết quả của các cuộc gọi phương thức hoặc các loại ngoại lệ được ném ra từ đối tượng.

Quay trở lại ví dụ một lần nữa, về mặt lý thuyết, các phương thức Board có thể được thực hiện để hoạt động tốt trên ThreeDBoard. Tuy nhiên, trong thực tế, sẽ rất khó để ngăn chặn sự khác biệt trong hành vi mà khách hàng có thể không xử lý đúng cách, mà không làm hỏng chức năng mà ThreeDBoard dự định thêm vào.

Với kiến ​​thức này, việc đánh giá tuân thủ LSP có thể là một công cụ tuyệt vời để xác định khi nào thành phần là cơ chế phù hợp hơn để mở rộng chức năng hiện có, thay vì kế thừa.


19

Tôi đoán mọi người đều bảo vệ LSP là gì về mặt kỹ thuật: Về cơ bản, bạn muốn có thể trừu tượng hóa khỏi các chi tiết phụ và sử dụng siêu kiểu một cách an toàn.

Vì vậy, Liskov có 3 quy tắc cơ bản:

  1. Quy tắc chữ ký: Cần có một triển khai hợp lệ cho mọi hoạt động của siêu kiểu trong kiểu con theo cú pháp. Một cái gì đó một trình biên dịch sẽ có thể kiểm tra cho bạn. Có một quy tắc nhỏ về việc ném ít ngoại lệ hơn và ít nhất có thể truy cập được như các phương pháp siêu kiểu.

  2. Phương pháp quy tắc: Việc thực hiện các hoạt động đó là đúng đắn về mặt ngữ nghĩa.

    • Điều kiện tiên quyết của Weaker: Các hàm subtype nên lấy ít nhất những gì supertype lấy làm đầu vào, nếu không muốn nói là nhiều hơn.
    • Postconditions mạnh hơn: Họ nên tạo ra một tập hợp con của đầu ra các phương thức supertype được tạo ra.
  3. Quy tắc thuộc tính: Điều này vượt xa các cuộc gọi chức năng cá nhân.

    • Bất biến: Những điều luôn luôn đúng phải vẫn còn đúng. Ví dụ. Kích thước của Set không bao giờ âm.
    • Thuộc tính tiến hóa: Thông thường phải làm gì đó với tính bất biến hoặc loại trạng thái mà đối tượng có thể ở. Hoặc có thể đối tượng chỉ phát triển và không bao giờ co lại nên các phương thức phụ không nên thực hiện.

Tất cả các thuộc tính này cần được bảo tồn và chức năng phụ phụ không nên vi phạm các thuộc tính siêu kiểu.

Nếu ba điều này được quan tâm, bạn đã trừu tượng hóa khỏi những thứ cơ bản và bạn đang viết mã ghép lỏng lẻo.

Nguồn: Phát triển chương trình tại Java - Barbara Liskov


18

Một ví dụ quan trọng về việc sử dụng LSP là trong thử nghiệm phần mềm .

Nếu tôi có một lớp A là lớp con tuân thủ LSP của B, thì tôi có thể sử dụng lại bộ kiểm tra của B để kiểm tra A.

Để kiểm tra đầy đủ lớp con A, có lẽ tôi cần thêm một vài trường hợp thử nghiệm nữa, nhưng ở mức tối thiểu tôi có thể sử dụng lại tất cả các trường hợp thử nghiệm của siêu lớp B.

Một cách để nhận ra điều này là bằng cách xây dựng cái mà McGregor gọi là "Hệ thống phân cấp song song để thử nghiệm": ATestLớp của tôi sẽ kế thừa từ đó BTest. Một số hình thức tiêm sau đó là cần thiết để đảm bảo trường hợp thử nghiệm hoạt động với các đối tượng loại A thay vì loại B (một mẫu phương thức mẫu đơn giản sẽ làm).

Lưu ý rằng việc sử dụng lại bộ siêu kiểm thử cho tất cả các triển khai lớp con trên thực tế là một cách để kiểm tra các triển khai của lớp con này có tuân thủ LSP không. Do đó, người ta cũng có thể lập luận rằng người ta nên chạy bộ kiểm tra siêu lớp trong ngữ cảnh của bất kỳ lớp con nào.

Xem thêm câu trả lời cho câu hỏi Stackoverflow " Tôi có thể triển khai một loạt các thử nghiệm có thể sử dụng lại để kiểm tra việc triển khai giao diện không? "


14

Hãy minh họa bằng Java:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

Không có vấn đề ở đây, phải không? Một chiếc xe chắc chắn là một thiết bị vận chuyển, và ở đây chúng ta có thể thấy rằng nó ghi đè lên phương thức startEngine () của siêu lớp của nó.

Hãy thêm một thiết bị vận chuyển khác:

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

Mọi thứ không diễn ra như dự định bây giờ! Đúng, xe đạp là một thiết bị vận chuyển, tuy nhiên, nó không có động cơ và do đó, phương thức startEngine () không thể được thực hiện.

Đây là những loại vấn đề vi phạm Nguyên tắc thay thế Liskov dẫn đến và chúng thường có thể được nhận ra bằng một phương pháp không có gì, hoặc thậm chí không thể thực hiện được.

Giải pháp cho những vấn đề này là một hệ thống phân cấp thừa kế chính xác và trong trường hợp của chúng tôi, chúng tôi sẽ giải quyết vấn đề bằng cách phân biệt các lớp thiết bị vận chuyển có và không có động cơ. Mặc dù xe đạp là một thiết bị vận chuyển, nó không có động cơ. Trong ví dụ này định nghĩa của chúng tôi về thiết bị vận chuyển là sai. Nó không nên có một động cơ.

Chúng ta có thể cấu trúc lại lớp TransportDevice của mình như sau:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

Bây giờ chúng tôi có thể mở rộng TransportDevice cho các thiết bị không có động cơ.

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

Và mở rộng TransportDevice cho các thiết bị cơ giới. Ở đây là thích hợp hơn để thêm đối tượng Engine.

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

Do đó, lớp Xe của chúng tôi trở nên chuyên biệt hơn, đồng thời tuân thủ Nguyên tắc thay thế Liskov.

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

Và lớp Xe đạp của chúng tôi cũng tuân thủ Nguyên tắc Thay thế Liskov.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}

9

Công thức này của LSP quá mạnh:

Nếu với mỗi đối tượng o1 loại S có một đối tượng o2 thuộc loại T sao cho tất cả các chương trình được định nghĩa theo T, thì hành vi của P không thay đổi khi o1 được thay thế cho o2, thì S là một kiểu con của T.

Về cơ bản, điều đó có nghĩa là S là một người khác, hoàn toàn gói gọn việc thực hiện chính xác điều tương tự như T. Và tôi có thể táo bạo và quyết định rằng hiệu suất là một phần trong hành vi của P ...

Vì vậy, về cơ bản, bất kỳ việc sử dụng ràng buộc muộn nào đều vi phạm LSP. Đó là toàn bộ quan điểm của OO để có được một hành vi khác khi chúng ta thay thế một đối tượng thuộc loại này sang loại khác!

Công thức được trích dẫn bởi wikipedia là tốt hơn vì thuộc tính phụ thuộc vào ngữ cảnh và không nhất thiết bao gồm toàn bộ hành vi của chương trình.


2
Erm, công thức đó là của riêng Barbara Liskov. Barbara Liskov, Trừu tượng hóa và phân cấp dữ liệu, Thông báo SIGPLAN, 23,5 (tháng 5 năm 1988). Nó không phải là "quá mạnh", nó "hoàn toàn chính xác" và nó không có hàm ý mà bạn nghĩ nó có. Nó mạnh, nhưng có lượng sức mạnh vừa phải.
DrPizza

Sau đó, có rất ít kiểu con trong cuộc sống thực :)
Pollien Pollet

3
"Hành vi không thay đổi" không có nghĩa là một kiểu con sẽ cung cấp cho bạn (các) giá trị kết quả cụ thể chính xác. Điều đó có nghĩa là hành vi của kiểu con phù hợp với những gì được mong đợi trong loại cơ sở. Ví dụ: Kiểu cơ sở Shape có thể có phương thức draw () và quy định rằng phương thức này sẽ hiển thị hình dạng. Hai kiểu con của Shape (ví dụ: Square và Circle) sẽ thực hiện phương thức draw () và kết quả sẽ khác nhau. Nhưng miễn là hành vi (hiển thị hình dạng) khớp với hành vi đã chỉ định của Hình dạng, thì Hình vuông và Hình tròn sẽ là các kiểu con của Hình dạng theo LSP.
SteveT

9

Trong một câu rất đơn giản, chúng ta có thể nói:

Lớp con không được vi phạm các đặc điểm của lớp cơ sở. Nó phải có khả năng với nó. Chúng ta có thể nói nó giống như phân nhóm.


9

Nguyên tắc thay thế của Liskov (LSP)

Tất cả thời gian chúng tôi thiết kế một mô-đun chương trình và chúng tôi tạo ra một số phân cấp lớp. Sau đó, chúng tôi mở rộng một số lớp tạo ra một số lớp dẫn xuất.

Chúng ta phải đảm bảo rằng các lớp dẫn xuất mới chỉ mở rộng mà không thay thế chức năng của các lớp cũ. Mặt khác, các lớp mới có thể tạo ra các hiệu ứng không mong muốn khi chúng được sử dụng trong các mô-đun chương trình hiện có.

Nguyên tắc thay thế của Liskov nói rằng nếu một mô-đun chương trình đang sử dụng lớp Cơ sở, thì tham chiếu đến lớp Cơ sở có thể được thay thế bằng lớp Derogen mà không ảnh hưởng đến chức năng của mô-đun chương trình.

Thí dụ:

Dưới đây là ví dụ kinh điển mà Nguyên tắc thay thế của Liskov bị vi phạm. Trong ví dụ này, 2 lớp được sử dụng: Hình chữ nhật và Hình vuông. Giả sử rằng đối tượng Hình chữ nhật được sử dụng ở đâu đó trong ứng dụng. Chúng tôi mở rộng ứng dụng và thêm lớp Square. Lớp hình vuông được trả về bởi một mẫu nhà máy, dựa trên một số điều kiện và chúng tôi không biết chính xác loại đối tượng nào sẽ được trả về. Nhưng chúng tôi biết đó là một hình chữ nhật. Chúng ta có được đối tượng hình chữ nhật, đặt chiều rộng thành 5 và chiều cao thành 10 và lấy diện tích. Đối với hình chữ nhật có chiều rộng 5 và chiều cao 10, diện tích phải là 50. Thay vào đó, kết quả sẽ là 100

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

Phần kết luận:

Nguyên tắc này chỉ là một phần mở rộng của Nguyên tắc Đóng Đóng và điều đó có nghĩa là chúng ta phải đảm bảo rằng các lớp dẫn xuất mới đang mở rộng các lớp cơ sở mà không thay đổi hành vi của chúng.

Xem thêm: Nguyên tắc đóng

Một số khái niệm tương tự cho cấu trúc tốt hơn: Quy ước về cấu hình


8

Nguyên tắc thay thế Liskov

  • Phương thức ghi đè không nên để trống
  • Phương thức ghi đè không nên ném lỗi
  • Lớp cơ sở hoặc hành vi giao diện không nên đi sửa đổi (làm lại) vì các hành vi của lớp dẫn xuất.

7

Một số phụ lục:
Tôi tự hỏi tại sao không ai viết về Bất biến, điều kiện tiên quyết và điều kiện đăng bài của lớp cơ sở phải được tuân theo bởi các lớp dẫn xuất. Để lớp D có nguồn gốc hoàn toàn được duy trì bởi lớp B, lớp D phải tuân theo một số điều kiện:

  • Các biến thể của lớp cơ sở phải được bảo tồn bởi lớp dẫn xuất
  • Các điều kiện trước của lớp cơ sở không được củng cố bởi lớp dẫn xuất
  • Các điều kiện hậu của lớp cơ sở không được làm suy yếu bởi lớp dẫn xuất.

Vì vậy, dẫn xuất phải nhận thức được ba điều kiện trên do lớp cơ sở áp đặt. Do đó, các quy tắc của phân nhóm được quyết định trước. Điều đó có nghĩa là, mối quan hệ 'IS A' chỉ được tuân theo khi các quy tắc nhất định được tuân theo bởi tiểu loại. Các quy tắc này, dưới dạng bất biến, tiền đề và hậu điều kiện, nên được quyết định bởi một " hợp đồng thiết kế " chính thức .

Thảo luận thêm về điều này có sẵn tại blog của tôi: Nguyên tắc thay thế Liskov


6

Nói một cách đơn giản, LSP nói rằng các đối tượng của cùng một siêu lớp sẽ có thể được hoán đổi với nhau mà không phá vỡ bất cứ thứ gì.

Ví dụ, nếu chúng ta có một Catvà một Doglớp xuất phát từ một Animallớp, bất kỳ hàm nào sử dụng lớp Animal sẽ có thể sử dụng Cathoặc Doghoạt động bình thường.


4

Việc triển khai ThreeDBoard dưới dạng một mảng của Board có hữu ích không?

Có lẽ bạn có thể muốn coi các lát ThreeDBoard trong các mặt phẳng khác nhau như một Bảng. Trong trường hợp đó, bạn có thể muốn trừu tượng hóa một giao diện (hoặc lớp trừu tượng) cho Board để cho phép thực hiện nhiều lần.

Về giao diện bên ngoài, bạn có thể muốn tạo ra giao diện Board cho cả TwoDBoard và ThreeDBoard (mặc dù không có phương thức nào ở trên phù hợp).


1
Tôi nghĩ ví dụ này chỉ đơn giản là để chứng minh rằng việc kế thừa từ bảng không có ý nghĩa gì trong bối cảnh ThreeDBoard và tất cả các chữ ký phương thức đều vô nghĩa với trục Z.
Bản thân

4

Hình vuông là hình chữ nhật trong đó chiều rộng bằng chiều cao. Nếu hình vuông đặt hai kích thước khác nhau cho chiều rộng và chiều cao thì nó vi phạm bất biến hình vuông. Điều này được thực hiện xung quanh bằng cách giới thiệu các tác dụng phụ. Nhưng nếu hình chữ nhật có setSize (chiều cao, chiều rộng) với điều kiện tiên quyết 0 <height và 0 <width. Phương thức kiểu con dẫn xuất yêu cầu chiều cao == width; một điều kiện tiên quyết mạnh mẽ hơn (và điều đó vi phạm lsp). Điều này cho thấy rằng mặc dù hình vuông là một hình chữ nhật nhưng nó không phải là một kiểu con hợp lệ vì điều kiện tiên quyết được tăng cường. Các công việc xung quanh (nói chung là một điều xấu) gây ra tác dụng phụ và điều này làm suy yếu điều kiện bài (vi phạm lsp). setWidth trên cơ sở có điều kiện bài 0 <width. Nguồn gốc làm suy yếu nó với chiều cao == chiều rộng.

Do đó, một hình vuông có thể thay đổi kích thước không phải là một hình chữ nhật có thể thay đổi kích thước.


4

Nguyên tắc này được Barbara Liskov đưa ra vào năm 1987 và mở rộng Nguyên tắc Đóng mở bằng cách tập trung vào hành vi của một siêu lớp và các kiểu con của nó.

Tầm quan trọng của nó trở nên rõ ràng khi chúng ta xem xét hậu quả của việc vi phạm nó. Hãy xem xét một ứng dụng sử dụng lớp sau.

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

Hãy tưởng tượng rằng một ngày nào đó, khách hàng yêu cầu khả năng thao tác các hình vuông ngoài hình chữ nhật. Vì hình vuông là hình chữ nhật, nên lớp hình vuông nên được lấy từ lớp Hình chữ nhật.

public class Square : Rectangle
{
} 

Tuy nhiên, bằng cách đó chúng ta sẽ gặp hai vấn đề:

Một hình vuông không cần cả hai biến chiều cao và chiều rộng được kế thừa từ hình chữ nhật và điều này có thể tạo ra sự lãng phí đáng kể trong bộ nhớ nếu chúng ta phải tạo ra hàng trăm ngàn đối tượng hình vuông. Các thuộc tính setter chiều rộng và chiều cao được kế thừa từ hình chữ nhật là không phù hợp cho hình vuông vì chiều rộng và chiều cao của hình vuông là giống hệt nhau. Để đặt cả chiều cao và chiều rộng cho cùng một giá trị, chúng ta có thể tạo hai thuộc tính mới như sau:

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

Bây giờ, khi ai đó sẽ đặt chiều rộng của một đối tượng hình vuông, chiều cao của nó sẽ thay đổi tương ứng và ngược lại.

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

Hãy tiến về phía trước và xem xét chức năng khác này:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

Nếu chúng ta chuyển một tham chiếu đến một đối tượng vuông vào hàm này, chúng ta sẽ vi phạm LSP vì hàm này không hoạt động đối với các dẫn xuất của các đối số của nó. Các thuộc tính chiều rộng và chiều cao không đa hình vì chúng không được khai báo ảo trong hình chữ nhật (đối tượng hình vuông sẽ bị hỏng vì chiều cao sẽ không bị thay đổi).

Tuy nhiên, bằng cách tuyên bố các thuộc tính setter là ảo, chúng ta sẽ phải đối mặt với một vi phạm khác, OCP. Trong thực tế, việc tạo ra một hình vuông lớp dẫn xuất đang gây ra những thay đổi cho hình chữ nhật của lớp cơ sở.


3

Giải thích rõ ràng nhất cho LSP mà tôi tìm thấy cho đến nay là "Nguyên tắc thay thế Liskov nói rằng đối tượng của lớp dẫn xuất có thể thay thế một đối tượng của lớp cơ sở mà không gây ra bất kỳ lỗi nào trong hệ thống hoặc sửa đổi hành vi của lớp cơ sở "Từ đây . Bài viết đưa ra ví dụ mã cho việc vi phạm LSP và sửa nó.


1
Vui lòng cung cấp các ví dụ về mã trên stackoverflow.
sebenalern

3

Giả sử chúng ta sử dụng một hình chữ nhật trong mã của mình

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

Trong lớp hình học của chúng tôi, chúng tôi đã học được rằng hình vuông là một loại hình chữ nhật đặc biệt vì chiều rộng của nó có cùng chiều dài với chiều cao của nó. Hãy tạo một Squarelớp học dựa trên thông tin này:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

Nếu chúng ta thay thế Rectanglebằng Squaremã đầu tiên, thì nó sẽ bị hỏng:

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

Điều này là do Squaređiều kiện tiên quyết mới mà chúng tôi không có trong Rectanglelớp : width == height. Theo LSP, các Rectanglethể hiện nên được thay thế bằng các Rectanglethể hiện của lớp con. Điều này là do các trường hợp này vượt qua kiểm tra loại cho các Rectangletrường hợp và do đó chúng sẽ gây ra lỗi không mong muốn trong mã của bạn.

Đây là một ví dụ cho "điều kiện tiên quyết không thể được tăng cường trong một phần phụ" trong bài viết wiki . Vì vậy, để tổng hợp, vi phạm LSP có thể sẽ gây ra lỗi trong mã của bạn tại một số điểm.


3

LSP nói rằng '' Các đối tượng nên được thay thế bởi các kiểu con của chúng ''. Mặt khác, nguyên tắc này chỉ ra

Các lớp con không bao giờ nên phá vỡ các định nghĩa kiểu của lớp cha.

và ví dụ sau giúp hiểu rõ hơn về LSP.

Không có LSP:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Sửa lỗi bằng LSP:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

2

Tôi khuyến khích bạn đọc bài viết: Vi phạm nguyên tắc thay thế Liskov (LSP) .

Bạn có thể tìm thấy lời giải thích Nguyên tắc thay thế Liskov là gì, manh mối chung giúp bạn đoán xem bạn đã vi phạm hay chưa và một ví dụ về cách tiếp cận sẽ giúp bạn phân cấp lớp của mình an toàn hơn.


2

NGUYÊN TẮC GIAO DỊCH LISKOV (Từ cuốn sách Mark Seemann) nói rằng chúng ta sẽ có thể thay thế một triển khai giao diện này bằng một giao diện khác mà không phá vỡ ứng dụng khách hoặc triển khai. Đây là nguyên tắc cho phép giải quyết các yêu cầu xảy ra trong tương lai, ngay cả khi chúng ta có thể ' t thấy trước chúng ngày hôm nay.

Nếu chúng ta rút máy tính ra khỏi tường (Thực hiện), cả ổ cắm trên tường (Giao diện) cũng như máy tính (Máy khách) đều bị hỏng (thực tế, nếu đó là máy tính xách tay, nó thậm chí có thể chạy bằng pin trong một khoảng thời gian) . Tuy nhiên, với phần mềm, khách hàng thường mong đợi một dịch vụ có sẵn. Nếu dịch vụ bị xóa, chúng tôi sẽ nhận được NullReferenceException. Để đối phó với loại tình huống này, chúng ta có thể tạo ra một triển khai giao diện không có gì. Đây là một mẫu thiết kế được gọi là Null Object, [4] và nó tương ứng với việc rút máy tính ra khỏi tường. Bởi vì chúng tôi đang sử dụng khớp nối lỏng lẻo, chúng tôi có thể thay thế một triển khai thực sự bằng một thứ không có gì mà không gây rắc rối.


2

Nguyên tắc thay thế của Likov nói rằng nếu một mô-đun chương trình đang sử dụng lớp Cơ sở, thì tham chiếu đến lớp Cơ sở có thể được thay thế bằng lớp Derogen mà không ảnh hưởng đến chức năng của mô-đun chương trình.

Ý định - Các loại có nguồn gốc phải hoàn toàn có thể thay thế cho các loại cơ sở của chúng.

Ví dụ - Các kiểu trả về đồng biến thể trong java.


1

Đây là một đoạn trích từ bài đăng này để làm rõ mọi thứ độc đáo:

[..] để hiểu một số nguyên tắc, điều quan trọng là phải nhận ra khi nào nó bị vi phạm. Đây là những gì tôi sẽ làm bây giờ.

Việc vi phạm nguyên tắc này có ý nghĩa gì? Nó ngụ ý rằng một đối tượng không hoàn thành hợp đồng được áp đặt bởi một sự trừu tượng thể hiện với một giao diện. Nói cách khác, điều đó có nghĩa là bạn đã xác định được sự trừu tượng của mình sai.

Hãy xem xét ví dụ sau:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

Đây có phải là vi phạm LSP không? Đúng. Điều này là do hợp đồng của tài khoản cho chúng tôi biết rằng tài khoản sẽ bị rút, nhưng điều này không phải lúc nào cũng đúng. Vì vậy, tôi nên làm gì để khắc phục nó? Tôi chỉ sửa đổi hợp đồng:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

Voilà, bây giờ hợp đồng được thỏa mãn.

Vi phạm tinh vi này thường áp đặt một khách hàng với khả năng cho biết sự khác biệt giữa các đối tượng cụ thể được sử dụng. Ví dụ: được cung cấp hợp đồng của Tài khoản đầu tiên, nó có thể trông như sau:

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

Và, điều này tự động vi phạm nguyên tắc đóng mở [nghĩa là đối với yêu cầu rút tiền. Bởi vì bạn không bao giờ biết điều gì xảy ra nếu một đối tượng vi phạm hợp đồng không có đủ tiền. Có lẽ nó chỉ trả về không có gì, có lẽ một ngoại lệ sẽ được ném ra. Vì vậy, bạn phải kiểm tra xem nó hasEnoughMoney()- không phải là một phần của giao diện. Vì vậy, kiểm tra phụ thuộc vào lớp cụ thể bắt buộc này là vi phạm OCP].

Điểm này cũng giải quyết một quan niệm sai lầm mà tôi gặp phải khá thường xuyên về vi phạm LSP. Nó nói rằng nếu hành vi của cha mẹ thay đổi ở một đứa trẻ, thì nó vi phạm LSP. Tuy nhiên, điều đó không xảy ra - miễn là một đứa trẻ không vi phạm hợp đồng của cha mẹ.

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.